From 8643ed12ad45946454740be8b5cd0ec25a05bdaf Mon Sep 17 00:00:00 2001 From: OoO Date: Tue, 5 May 2026 15:48:39 +0800 Subject: [PATCH] test(observability): validate nav active page contract --- docs/guides/observability_ui_governance.md | 1 + scripts/check_observability_ui.py | 65 ++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/docs/guides/observability_ui_governance.md b/docs/guides/observability_ui_governance.md index f4bfe23..5f89094 100644 --- a/docs/guides/observability_ui_governance.md +++ b/docs/guides/observability_ui_governance.md @@ -66,6 +66,7 @@ Guard 會檢查: - 側欄是否維持 `AI 中樞 → AI 觀測台 → 分組頁面` 的收納結構。 - 側欄是否維持暖深咖啡背景與第二/三層足夠對比。 - Topbar 是否仍載入觀測台 CSS 與健康 indicator。 +- 10 個觀測頁的 `active_page`、側欄 URL、側欄 label、`momo-observability-mode` 掛載清單是否一一對齊。 ### 2. Production 10 頁 HTTP 巡檢 diff --git a/scripts/check_observability_ui.py b/scripts/check_observability_ui.py index d52f811..a8e1da9 100644 --- a/scripts/check_observability_ui.py +++ b/scripts/check_observability_ui.py @@ -30,9 +30,23 @@ TEMPLATE_PATHS = [ "templates/admin/ppt_audit_history.html", ] +OBSERVABILITY_NAV_ITEMS = [ + ("templates/admin/observability_overview.html", "obs_overview", "/observability/overview", "觀測台總覽"), + ("templates/admin/agent_orchestration.html", "obs_agent_orchestration", "/observability/agent_orchestration", "Agent 編排矩陣"), + ("templates/admin/business_intel.html", "obs_business_intel", "/observability/business_intel", "商業面 × AI"), + ("templates/admin/host_health.html", "obs_host_health", "/observability/host_health", "主機健康"), + ("templates/admin/ai_calls_dashboard.html", "obs_ai_calls", "/observability/ai_calls", "AI 呼叫"), + ("templates/admin/budget.html", "obs_budget", "/observability/budget", "預算控管"), + ("templates/admin/promotion_review.html", "obs_promotion_review", "/observability/promotion_review", "RAG 晉升審核"), + ("templates/admin/rag_queries.html", "obs_rag_queries", "/observability/rag_queries", "RAG 召回詳情"), + ("templates/admin/quality_trend.html", "obs_quality_trend", "/observability/quality_trend", "反饋趨勢"), + ("templates/admin/ppt_audit_history.html", "obs_ppt_audit", "/observability/ppt_audit_history", "PPT 視覺審核"), +] + CSS_PATH = Path("static/css/observability-system.css") SHELL_PATH = Path("templates/components/_ewoooc_shell.html") BASE_PATH = Path("templates/ewoooc_base.html") +ROUTE_PATH = Path("routes/admin_observability_routes.py") @dataclass(frozen=True) @@ -195,6 +209,55 @@ def scan_shell() -> list[str]: return findings +def scan_nav_contract() -> list[str]: + shell_path = ROOT / SHELL_PATH + base_path = ROOT / BASE_PATH + route_path = ROOT / ROUTE_PATH + if not shell_path.exists() or not base_path.exists(): + return [] + + shell_text = shell_path.read_text(encoding="utf-8") + base_text = base_path.read_text(encoding="utf-8") + route_text = route_path.read_text(encoding="utf-8") if route_path.exists() else "" + findings: list[str] = [] + + for template_path, active_page, url, label in OBSERVABILITY_NAV_ITEMS: + template = ROOT / template_path + if not template.exists(): + findings.append(f"{template_path}: missing observability template for `{label}`") + continue + + template_text = template.read_text(encoding="utf-8") + active_pattern = re.compile( + r"set\s+active_page\s*=\s*['\"]" + re.escape(active_page) + r"['\"]" + ) + route_has_contract = template_path.replace("templates/", "") in route_text and active_page in route_text + if not active_pattern.search(template_text) and not route_has_contract: + findings.append( + f"{template_path}: active_page must be `{active_page}` in template or route for `{label}`" + ) + + if active_page not in shell_text: + findings.append(f"{SHELL_PATH}: sidebar missing active_page `{active_page}`") + if url not in shell_text: + findings.append(f"{SHELL_PATH}: sidebar missing URL `{url}` for `{label}`") + if label not in shell_text: + findings.append(f"{SHELL_PATH}: sidebar missing label `{label}`") + if active_page not in base_text: + findings.append(f"{BASE_PATH}: momo-observability-mode list missing `{active_page}`") + + sidebar_observability_links = set(re.findall(r'href="(/observability/[^"]+)"', shell_text)) + expected_links = {url for _, _, url, _ in OBSERVABILITY_NAV_ITEMS} + extra_links = sorted(sidebar_observability_links - expected_links) + missing_links = sorted(expected_links - sidebar_observability_links) + for url in extra_links: + findings.append(f"{SHELL_PATH}: unexpected observability sidebar URL `{url}`") + for url in missing_links: + findings.append(f"{SHELL_PATH}: missing observability sidebar URL `{url}`") + + return findings + + def main() -> int: findings: list[str] = [] for template_path in TEMPLATE_PATHS: @@ -202,6 +265,7 @@ def main() -> int: findings.extend(scan_css()) findings.extend(scan_shell()) findings.extend(scan_required_snippets(BASE_PATH, REQUIRED_BASE_SNIPPETS, "base/topbar")) + findings.extend(scan_nav_contract()) if findings: print("Observability UI guard: FAIL") @@ -214,6 +278,7 @@ def main() -> int: print(f"- css guardrails checked: {len(REQUIRED_CSS_SNIPPETS)}") print(f"- sidebar/nav guardrails checked: {len(REQUIRED_SHELL_SNIPPETS)}") print(f"- base/topbar guardrails checked: {len(REQUIRED_BASE_SNIPPETS)}") + print(f"- nav contract checked: {len(OBSERVABILITY_NAV_ITEMS)} pages") return 0