From 5c5816aaa6ff0de6d49f33ea7ec8208c438b6b6f Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Tue, 31 Mar 2026 16:23:46 -0400 Subject: [PATCH 01/12] do not display popup on multi-endpoint disabled to users in workspaces --- application/single_app/config.py | 2 +- .../js/workspace/workspace_model_endpoints.js | 33 +++++++++ application/single_app/templates/chats.html | 11 ++- .../templates/group_workspaces.html | 2 + .../single_app/templates/workspace.html | 2 + .../WORKSPACE_ENDPOINTS_DISABLED_TOAST_FIX.md | 45 +++++++++++++ ...multi_endpoint_notice_template_fallback.py | 46 +++++++++++++ ...workspace_endpoint_disabled_state_quiet.py | 58 ++++++++++++++++ ..._chat_page_multi_endpoint_notice_render.py | 47 +++++++++++++ ..._workspace_page_endpoint_disabled_quiet.py | 67 +++++++++++++++++++ 10 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 docs/explanation/fixes/WORKSPACE_ENDPOINTS_DISABLED_TOAST_FIX.md create mode 100644 functional_tests/test_chat_multi_endpoint_notice_template_fallback.py create mode 100644 functional_tests/test_workspace_endpoint_disabled_state_quiet.py create mode 100644 ui_tests/test_chat_page_multi_endpoint_notice_render.py create mode 100644 ui_tests/test_workspace_page_endpoint_disabled_quiet.py diff --git a/application/single_app/config.py b/application/single_app/config.py index 8bdcc4b0..7d60ed03 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.240.003" +VERSION = "0.240.005" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/static/js/workspace/workspace_model_endpoints.js b/application/single_app/static/js/workspace/workspace_model_endpoints.js index 3f9e3337..612ea15d 100644 --- a/application/single_app/static/js/workspace/workspace_model_endpoints.js +++ b/application/single_app/static/js/workspace/workspace_model_endpoints.js @@ -53,6 +53,8 @@ const modelsListEl = document.getElementById("model-endpoint-models-list"); const addModelBtn = document.getElementById("model-endpoint-add-model-btn"); const scope = window.modelEndpointScope || "user"; +const endpointsContainerId = scope === "group" ? "group-multi-endpoint-configuration" : "workspace-multi-endpoint-configuration"; +const endpointsContainer = document.getElementById(endpointsContainerId); const endpointsApi = scope === "group" ? "/api/group/model-endpoints" : "/api/user/model-endpoints"; const modelsFetchApi = scope === "group" ? "/api/group/models/fetch" : "/api/user/models/fetch"; const modelsTestApi = scope === "group" ? "/api/group/models/test-model" : "/api/user/models/test-model"; @@ -60,6 +62,21 @@ const modelsTestApi = scope === "group" ? "/api/group/models/test-model" : "/api let workspaceEndpoints = Array.isArray(window.workspaceModelEndpoints) ? [...window.workspaceModelEndpoints] : []; let modalModels = []; +function hasEndpointManagementUi() { + return Boolean(endpointsWrapper && endpointsTbody); +} + +function hideEndpointManagementUi() { + if (endpointsContainer) { + endpointsContainer.classList.add("d-none"); + } +} + +function isEndpointsFeatureDisabled(error) { + const message = typeof error?.message === "string" ? error.message.toLowerCase() : ""; + return message.includes("custom endpoints") && message.includes("is disabled"); +} + function generateId() { if (window.crypto && window.crypto.randomUUID) { return window.crypto.randomUUID(); @@ -620,6 +637,10 @@ function escapeHtml(value) { } async function loadEndpoints() { + if (!hasEndpointManagementUi()) { + return; + } + try { const response = await fetch(endpointsApi); const payload = await response.json().catch(() => ({})); @@ -629,12 +650,24 @@ async function loadEndpoints() { workspaceEndpoints = Array.isArray(payload.endpoints) ? payload.endpoints : []; renderEndpoints(); } catch (error) { + if (isEndpointsFeatureDisabled(error)) { + console.info("[WorkspaceEndpoints] Endpoint management is disabled; skipping endpoint load."); + workspaceEndpoints = []; + renderEndpoints(); + hideEndpointManagementUi(); + return; + } + console.error("Failed to load endpoints", error); showToast(error.message || "Failed to load endpoints.", "danger"); } } function initialize() { + if (!hasEndpointManagementUi()) { + return; + } + if (enableMultiEndpointToggle) { enableMultiEndpointToggle.checked = Boolean(window.enableMultiModelEndpoints); } diff --git a/application/single_app/templates/chats.html b/application/single_app/templates/chats.html index 73d26ff3..559ca949 100644 --- a/application/single_app/templates/chats.html +++ b/application/single_app/templates/chats.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% set multi_endpoint_notice_data = multi_endpoint_notice|default({}) %} {% block title %} Chats - {{ app_settings.app_title }} {% endblock %} @@ -289,19 +290,17 @@
-
@@ -1166,7 +1165,7 @@ window.workspaceModelEndpoints = JSON.parse('{{ group_model_endpoints|default([])|tojson|safe }}' || '[]'); window.globalModelEndpoints = JSON.parse('{{ global_model_endpoints|default([])|tojson|safe }}' || '[]'); +{% if settings.enable_semantic_kernel and settings.allow_group_custom_endpoints and settings.enable_multi_model_endpoints %} +{% endif %} + {% if settings.per_user_semantic_kernel and settings.enable_semantic_kernel and settings.allow_user_custom_endpoints and settings.enable_multi_model_endpoints %} + {% endif %} diff --git a/docs/explanation/fixes/WORKSPACE_ENDPOINTS_DISABLED_TOAST_FIX.md b/docs/explanation/fixes/WORKSPACE_ENDPOINTS_DISABLED_TOAST_FIX.md new file mode 100644 index 00000000..40cb4d65 --- /dev/null +++ b/docs/explanation/fixes/WORKSPACE_ENDPOINTS_DISABLED_TOAST_FIX.md @@ -0,0 +1,45 @@ +# WORKSPACE_ENDPOINTS_DISABLED_TOAST_FIX + +Fixed in version: **0.240.005** + +## Issue Description + +The personal and group workspace pages always loaded the endpoint-management JavaScript module, even when custom endpoints or multi-endpoint support were intentionally disabled by the administrator. + +## Root Cause Analysis + +The endpoints tab markup was correctly gated in the templates, but the shared workspace endpoint module was still included unconditionally in the page scripts. That module immediately called the disabled endpoint API and surfaced the backend `Allow User Custom Endpoints is disabled.` error as a toast. + +## Technical Details + +Files modified: + +- `application/single_app/static/js/workspace/workspace_model_endpoints.js` +- `application/single_app/templates/workspace.html` +- `application/single_app/templates/group_workspaces.html` +- `application/single_app/config.py` +- `functional_tests/test_workspace_endpoint_disabled_state_quiet.py` +- `ui_tests/test_workspace_page_endpoint_disabled_quiet.py` + +Code changes summary: + +- Added a DOM guard so the workspace endpoint module skips initialization when the endpoints UI is not rendered. +- Added disabled-feature detection so a stale or race-condition `custom endpoints is disabled` response is handled quietly instead of showing a user-facing error toast. +- Gated the workspace and group endpoint module include behind the same template conditions that render the endpoints tabs. + +Testing approach: + +- Added a functional regression test that verifies the JS guard and template gating. +- Added a UI regression test that confirms the workspace page does not request the disabled endpoints API or emit the disabled-feature error when the tab is absent. + +## Validation + +Before: + +- Workspace pages without endpoint management still requested `/api/user/model-endpoints`. +- Users saw a red toast saying custom endpoints were disabled, even though the feature was intentionally off. + +After: + +- The endpoint module only initializes when the endpoints UI is present. +- Disabled configurations stay quiet and do not show the endpoint error toast to end users. \ No newline at end of file diff --git a/functional_tests/test_chat_multi_endpoint_notice_template_fallback.py b/functional_tests/test_chat_multi_endpoint_notice_template_fallback.py new file mode 100644 index 00000000..c98444a4 --- /dev/null +++ b/functional_tests/test_chat_multi_endpoint_notice_template_fallback.py @@ -0,0 +1,46 @@ +# test_chat_multi_endpoint_notice_template_fallback.py +#!/usr/bin/env python3 +""" +Functional test for chat multi-endpoint notice template fallback. +Version: 0.240.004 +Implemented in: 0.240.004 + +This test ensures the chats template safely defaults the multi-endpoint notice +context, renders the notice markup only when enabled, and avoids Jinja +undefined errors when the route does not pass the notice object. +""" + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +CHAT_TEMPLATE = REPO_ROOT / "application" / "single_app" / "templates" / "chats.html" +CONFIG_FILE = REPO_ROOT / "application" / "single_app" / "config.py" + + +def test_chat_multi_endpoint_notice_template_fallback(): + """Verify the chats template safely handles a missing notice context.""" + template_content = CHAT_TEMPLATE.read_text(encoding="utf-8") + config_content = CONFIG_FILE.read_text(encoding="utf-8") + + assert 'VERSION = "0.240.004"' in config_content, "Expected config.py version 0.240.004" + assert '{% set multi_endpoint_notice_data = multi_endpoint_notice|default({}) %}' in template_content, ( + "Expected chats.html to define a safe default for multi_endpoint_notice." + ) + assert '{% if multi_endpoint_notice_data.enabled %}' in template_content, ( + "Expected the chat multi-endpoint notice markup to be conditionally rendered." + ) + assert 'id="multi-endpoint-notice"' in template_content, ( + "Expected chats.html to keep the multi-endpoint notice container for chat-onload.js." + ) + assert 'window.multiEndpointNotice = JSON.parse(\'{{ multi_endpoint_notice_data|tojson()|safe }}\');' in template_content, ( + "Expected chats.html to serialize the safe default notice object for chat-onload.js." + ) + assert ' -