From 3fca720fa156099ae3b8cb20afdfaa7ea77dbee5 Mon Sep 17 00:00:00 2001 From: OoO Date: Tue, 5 May 2026 15:41:39 +0800 Subject: [PATCH] test(observability): guard sidebar navigation design --- docs/guides/observability_ui_governance.md | 3 + scripts/check_observability_ui.py | 70 ++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/docs/guides/observability_ui_governance.md b/docs/guides/observability_ui_governance.md index 64c571f..f4bfe23 100644 --- a/docs/guides/observability_ui_governance.md +++ b/docs/guides/observability_ui_governance.md @@ -63,6 +63,9 @@ Guard 會檢查: - 圖表 `style="height:..."` 與靜態 `style="width:..."`。 - 過大標題 clamp 與純白 hover/card surface。 - 觀測台 CSS 必要 token / utility class 是否仍存在。 +- 側欄是否維持 `AI 中樞 → AI 觀測台 → 分組頁面` 的收納結構。 +- 側欄是否維持暖深咖啡背景與第二/三層足夠對比。 +- Topbar 是否仍載入觀測台 CSS 與健康 indicator。 ### 2. Production 10 頁 HTTP 巡檢 diff --git a/scripts/check_observability_ui.py b/scripts/check_observability_ui.py index a716e7d..d52f811 100644 --- a/scripts/check_observability_ui.py +++ b/scripts/check_observability_ui.py @@ -31,6 +31,8 @@ TEMPLATE_PATHS = [ ] CSS_PATH = Path("static/css/observability-system.css") +SHELL_PATH = Path("templates/components/_ewoooc_shell.html") +BASE_PATH = Path("templates/ewoooc_base.html") @dataclass(frozen=True) @@ -92,6 +94,40 @@ REQUIRED_CSS_SNIPPETS = [ ".obs-table-shell", ] +REQUIRED_SHELL_SNIPPETS = [ + "AI 中樞", + "AI 觀測台", + "戰情室", + "系統與成本", + "RAG 與品質", + "momo-nav-tree", + "momo-nav-subtree", + "momo-nav-subtitle", + "momo-nav-sublink", + "rgba(255, 248, 238, 0.72)", + "linear-gradient(180deg", +] + +FORBIDDEN_SHELL_PATTERNS = [ + Rule( + "pure_black_sidebar", + re.compile(r"\.momo-sidebar\s*\{[^}]*background\s*:\s*(#000|black)\b", re.IGNORECASE | re.DOTALL), + "側欄不得回退成純黑背景;必須維持暖深咖啡漸層。", + ), + Rule( + "low_contrast_sublink", + re.compile(r"\.momo-nav-sublink\s*\{[^}]*color\s*:\s*rgba\([^)]*,\s*0\.(?:[0-5][0-9]|60)\)", re.IGNORECASE | re.DOTALL), + "第二/三層導覽文字透明度過低,會看不清楚;需維持足夠對比。", + ), +] + +REQUIRED_BASE_SNIPPETS = [ + "observability-system.css", + "momo-observability-mode", + "/observability/overview", + "/observability/api/health_indicator", +] + def line_number(text: str, index: int) -> int: return text.count("\n", 0, index) + 1 @@ -129,11 +165,43 @@ def scan_css() -> list[str]: return findings +def scan_required_snippets(relative_path: Path, snippets: list[str], label: str) -> list[str]: + path = ROOT / relative_path + if not path.exists(): + return [f"{relative_path}: missing required {label} file"] + + text = path.read_text(encoding="utf-8") + findings: list[str] = [] + for snippet in snippets: + if snippet not in text: + findings.append(f"{relative_path}: missing required {label} snippet `{snippet}`") + return findings + + +def scan_shell() -> list[str]: + path = ROOT / SHELL_PATH + if not path.exists(): + return [f"{SHELL_PATH}: missing required shell file"] + + text = path.read_text(encoding="utf-8") + findings = scan_required_snippets(SHELL_PATH, REQUIRED_SHELL_SNIPPETS, "sidebar/nav") + for rule in FORBIDDEN_SHELL_PATTERNS: + for match in rule.pattern.finditer(text): + line = line_number(text, match.start()) + snippet = match.group(0).replace("\n", " ")[:90] + findings.append( + f"{SHELL_PATH}:{line}: [{rule.code}] {rule.message} ({snippet})" + ) + return findings + + def main() -> int: findings: list[str] = [] for template_path in TEMPLATE_PATHS: findings.extend(scan_file(Path(template_path))) findings.extend(scan_css()) + findings.extend(scan_shell()) + findings.extend(scan_required_snippets(BASE_PATH, REQUIRED_BASE_SNIPPETS, "base/topbar")) if findings: print("Observability UI guard: FAIL") @@ -144,6 +212,8 @@ def main() -> int: print("Observability UI guard: PASS") print(f"- templates checked: {len(TEMPLATE_PATHS)}") 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)}") return 0