From 0ed48201bf027adf584d89df454e8866302a2ac6 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Thu, 20 Aug 2026 08:40:27 -0400 Subject: [PATCH] Fix Admin Settings 500 from cross-pane Jinja variable scope The Document Action Capabilities card moved into admin/_panes/actions.html but the two {% set %} statements feeding it stayed in admin/_panes/agents.html. admin_settings.html includes those panes as siblings, and Jinja gives each include its own context copy, so analyze_capability and comparison_capability were always Undefined. Attribute access on Undefined raised jinja2.exceptions.UndefinedError and every GET /admin/settings returned 500. Both statements now live in the pane that renders the card. The traceback never reached the App Service console because configure_azure_monitor attaches a handler to the root logger, which makes logging.basicConfig a no-op and makes Flask skip its own stderr handler, so unhandled exceptions went to Application Insights and nowhere else. ensure_console_error_logging now keeps a stderr handler on app.logger. An AST scan of all 44 panes surfaced two more names that always resolved to Undefined: enable_dai_debug in cosmos.html, which kept the Document Access Index diagnostics hidden even when enabled, and the dead window.enableDocumentClassification and window.enableExternalLinks globals, whose only consumers were two never-read declarations in admin_settings.js. Those globals are removed rather than repaired, because "False" is truthy in JavaScript and would have flipped the || false fallback. The composed-template test helpers flatten every include into one string, which is why the existing suite could not see this. The new test reads each pane in isolation and derives its allowlist by parsing the route's render_template call and the inject_settings context processor, so it cannot go stale. Version 0.260.018 -> 0.260.019. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- application/single_app/app.py | 4 + application/single_app/config.py | 2 +- .../single_app/functions_appinsights.py | 54 ++++ .../static/js/admin/admin_settings.js | 2 - .../templates/admin/_panes/actions.html | 5 + .../templates/admin/_panes/agents.html | 2 - .../templates/admin/_panes/cosmos.html | 12 +- .../single_app/templates/admin_settings.html | 2 - .../ADMIN_SETTINGS_PANE_VARIABLE_SCOPE_FIX.md | 240 ++++++++++++++ docs/explanation/fixes/index.md | 1 + docs/explanation/release_notes.md | 18 ++ ...test_admin_settings_pane_variable_scope.py | 292 ++++++++++++++++++ ...est_admin_settings_template_composition.py | 12 + .../test_flask_exception_console_logging.py | 206 ++++++++++++ ...admin_document_action_capabilities_card.py | 112 +++++-- 15 files changed, 921 insertions(+), 43 deletions(-) create mode 100644 docs/explanation/fixes/ADMIN_SETTINGS_PANE_VARIABLE_SCOPE_FIX.md create mode 100644 functional_tests/test_admin_settings_pane_variable_scope.py create mode 100644 functional_tests/test_flask_exception_console_logging.py diff --git a/application/single_app/app.py b/application/single_app/app.py index d8f837e33..ac56a87e3 100644 --- a/application/single_app/app.py +++ b/application/single_app/app.py @@ -321,6 +321,10 @@ def initialize_application(force=False): print("Setting up Application Insights logging...") setup_appinsights_logging(settings) logging.basicConfig(level=logging.DEBUG) + # basicConfig above is a no-op once Azure Monitor owns the root logger, + # and that same root handler stops Flask attaching its stderr handler, + # so unhandled tracebacks would otherwise never reach the container log. + ensure_console_error_logging(app.logger) ensure_default_global_agent_exists() start_background_tasks() diff --git a/application/single_app/config.py b/application/single_app/config.py index 0c548824e..3fc8cbf7d 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -96,7 +96,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.260.018" +VERSION = "0.260.019" IS_DEVELOPMENT = is_development_env_enabled() SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax') diff --git a/application/single_app/functions_appinsights.py b/application/single_app/functions_appinsights.py index 4409556ab..05101ee29 100644 --- a/application/single_app/functions_appinsights.py +++ b/application/single_app/functions_appinsights.py @@ -3,6 +3,7 @@ import logging import os import re +import sys import threading from datetime import date, datetime from typing import Any, Dict, Optional, Tuple @@ -14,6 +15,8 @@ _appinsights_logger = None _azure_monitor_configured = False _logging_settings_load_state = threading.local() +CONSOLE_ERROR_HANDLER_ATTRIBUTE = "_simplechat_console_error_handler" +CONSOLE_ERROR_LOG_FORMAT = "[%(asctime)s] %(levelname)s in %(name)s: %(message)s" REDACTED_LOG_VALUE = "***REDACTED***" MAX_LOG_STRING_LENGTH = 8192 SENSITIVE_LOG_KEY_FRAGMENTS = ( @@ -629,3 +632,54 @@ def setup_appinsights_logging(settings): print(f"[AZURE_MONITOR] Failed to setup Application Insights: {e}") _azure_monitor_configured = False # Don't re-raise the exception, just continue without Application Insights + + +def ensure_console_error_logging(logger): + """ + Guarantee that a logger writes ERROR records to stderr. + + Flask only attaches its own stderr handler when ``logging.Logger`` finds no + level handler anywhere up the hierarchy. ``configure_azure_monitor`` attaches + a handler to the root logger, which also turns ``logging.basicConfig`` into a + no-op, so unhandled request tracebacks are delivered to Application Insights + and never printed. On App Service that leaves nothing but the access-log line + in the container log, which makes 500s effectively invisible. + + The handler is attached to the supplied logger rather than to root, and is + pinned at ERROR, so library DEBUG output is never echoed to the console. + + Args: + logger (logging.Logger): Logger to attach the stderr handler to, + typically ``app.logger``. + + Returns: + logging.Handler: The console handler now attached to the logger, or + ``None`` when no logger was supplied. + + Raises: + None: Handler setup failures are reported and swallowed so that logging + configuration can never prevent the application from starting. + """ + if logger is None: + return None + + try: + for existing_handler in logger.handlers: + if getattr(existing_handler, CONSOLE_ERROR_HANDLER_ATTRIBUTE, False): + return existing_handler + + console_handler = logging.StreamHandler(sys.stderr) + console_handler.setLevel(logging.ERROR) + console_handler.setFormatter(logging.Formatter(CONSOLE_ERROR_LOG_FORMAT)) + setattr(console_handler, CONSOLE_ERROR_HANDLER_ATTRIBUTE, True) + logger.addHandler(console_handler) + + # A logger left at NOTSET defers to the root level, which Azure Monitor + # may have raised above ERROR, so pin it low enough to emit tracebacks. + if logger.level == logging.NOTSET or logger.level > logging.ERROR: + logger.setLevel(logging.ERROR) + + return console_handler + except Exception as handler_error: + print(f"[AZURE_MONITOR] Failed to attach console error handler: {handler_error}") + return None diff --git a/application/single_app/static/js/admin/admin_settings.js b/application/single_app/static/js/admin/admin_settings.js index 722a3d49f..55ee8fc7d 100644 --- a/application/single_app/static/js/admin/admin_settings.js +++ b/application/single_app/static/js/admin/admin_settings.js @@ -12,10 +12,8 @@ let imageSelected = window.imageSelected || []; let imageAll = window.imageAll || []; let classificationCategories = window.classificationCategories || []; -let enableDocumentClassification = window.enableDocumentClassification || false; let externalLinks = window.externalLinks || []; -let enableExternalLinks = window.enableExternalLinks || false; let externalLinksMenuName = window.externalLinksMenuName || 'External Links'; let agentsPagePromotedPopularAgents = Array.isArray(window.agentsPagePromotedPopularAgents) ? window.agentsPagePromotedPopularAgents diff --git a/application/single_app/templates/admin/_panes/actions.html b/application/single_app/templates/admin/_panes/actions.html index 221cfac5b..4f2a29f6d 100644 --- a/application/single_app/templates/admin/_panes/actions.html +++ b/application/single_app/templates/admin/_panes/actions.html @@ -1,4 +1,9 @@
+ {# These live here, not in the Agents pane, because sibling + {% include %} panes each render in their own scope: a value + set in one pane is not visible to the next one. #} + {% set analyze_capability = settings.document_action_capabilities.analyze %} + {% set comparison_capability = settings.document_action_capabilities.comparison %}
diff --git a/application/single_app/templates/admin/_panes/agents.html b/application/single_app/templates/admin/_panes/agents.html index 889caa9cc..86164afba 100644 --- a/application/single_app/templates/admin/_panes/agents.html +++ b/application/single_app/templates/admin/_panes/agents.html @@ -21,8 +21,6 @@ Note: All changes to global agents and actions require a restart of the web app to take effect.
- {% set analyze_capability = settings.document_action_capabilities.analyze %} - {% set comparison_capability = settings.document_action_capabilities.comparison %}
Agents Configuration diff --git a/application/single_app/templates/admin/_panes/cosmos.html b/application/single_app/templates/admin/_panes/cosmos.html index 6163be982..a3caaaa57 100644 --- a/application/single_app/templates/admin/_panes/cosmos.html +++ b/application/single_app/templates/admin/_panes/cosmos.html @@ -13,7 +13,7 @@
- {% if enable_dai_debug %} + {% if settings.enable_dai_debug %} @@ -27,7 +27,7 @@
Document access projection maintenance is automatic. The background scheduler repairs fail-open projection records first, then runs bounded backfill batches repeatedly while work remains. Production read metrics below show DAI-served reads, Redis cache hits, source fallbacks, RU, and latency without requiring shadow validation. - {% if enable_dai_debug %}Debug controls and shadow validation diagnostics are visible because enable_dai_debug is enabled in app settings.{% endif %} + {% if settings.enable_dai_debug %}Debug controls and shadow validation diagnostics are visible because enable_dai_debug is enabled in app settings.{% endif %}
@@ -57,7 +57,7 @@
Not loaded
- {% if enable_dai_debug %} + {% if settings.enable_dai_debug %}
Shadow Validation
@@ -91,7 +91,7 @@
- {% if enable_dai_debug %} + {% if settings.enable_dai_debug %}
Automatic Maintenance and Diagnostics
@@ -327,7 +327,7 @@
Default Read Path
Production read metrics are lightweight in-process counters for the current app worker. Application Insights logs remain the durable fleet-wide source for fallback warnings and query failures.
- {% if enable_dai_debug %} + {% if settings.enable_dai_debug %}
Last Shadow Result
@@ -653,7 +653,7 @@
- {% if enable_dai_debug %} + {% if settings.enable_dai_debug %}