Files
ewoooc/scripts/check_observability_ui.py
OoO 3fca720fa1
Some checks failed
CD Pipeline / deploy (push) Failing after 2m11s
test(observability): guard sidebar navigation design
2026-05-05 15:41:39 +08:00

222 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Guardrail checks for AI observability UI templates.
This script is intentionally lightweight: it scans the observability templates
and shared CSS for regression signals that previously hurt the war-room UI,
including unreadable typography, raw SQL errors, oversized titles, and inline
chart sizing.
"""
from __future__ import annotations
import re
import sys
from dataclasses import dataclass
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TEMPLATE_PATHS = [
"templates/admin/observability_overview.html",
"templates/admin/agent_orchestration.html",
"templates/admin/business_intel.html",
"templates/admin/host_health.html",
"templates/admin/ai_calls_dashboard.html",
"templates/admin/budget.html",
"templates/admin/promotion_review.html",
"templates/admin/rag_queries.html",
"templates/admin/quality_trend.html",
"templates/admin/ppt_audit_history.html",
]
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)
class Rule:
code: str
pattern: re.Pattern[str]
message: str
TEMPLATE_RULES = [
Rule(
"legacy_serif_font",
re.compile(r"\b(Georgia|Times New Roman)\b", re.IGNORECASE),
"觀測台標題不得回退到 Times/Georgia使用 Noto Sans TC / Inter 與 CSS token。",
),
Rule(
"raw_error_copy",
re.compile(
r"(查詢失敗:|ProgrammingError|UndefinedError|Traceback|Internal Server Error|relation\s+"|relation\s+\")"
),
"不得把 SQL/Jinja exception 或 raw failure 文案直接顯示給使用者。",
),
Rule(
"inline_height",
re.compile(r"style=\"[^\"]*height\s*:", re.IGNORECASE),
"圖表/區塊高度請使用 .obs-chart-frame 系列或 CSS class不要 inline height。",
),
Rule(
"static_inline_width",
re.compile(r"style=\"(?=[^\"]*width\s*:)(?![^\"]*\{\{)[^\"]*\"", re.IGNORECASE),
"靜態寬度請移到 CSS class僅允許 progress 類動態 width={{ ... }}。",
),
Rule(
"oversized_title",
re.compile(r"font-size:\s*clamp\([^;]*(3\.8rem|4\.45rem)", re.IGNORECASE),
"頁面大標不得使用過大 clamp使用 --obs-title-size token。",
),
Rule(
"pure_white_surface",
re.compile(r"background\s*:\s*#fff\s*;", re.IGNORECASE),
"觀測台 hover/card surface 不用純白;使用暖色 paper/surface token。",
),
Rule(
"wrong_business_active_page",
re.compile(r"active_page\s*=\s*['\"]obs_business['\"]"),
"商業戰情頁 active_page 必須是 obs_business_intel否則觀測台 CSS 不會載入。",
),
]
REQUIRED_CSS_SNIPPETS = [
"--obs-title-size",
"--obs-value-size",
".momo-observability-mode",
".obs-chart-frame",
".obs-chart-frame-tall",
".obs-chart-frame-slim",
".obs-modal-preview",
".obs-progress-xs",
".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
def scan_file(relative_path: Path) -> list[str]:
path = ROOT / relative_path
if not path.exists():
return [f"{relative_path}: missing required observability file"]
text = path.read_text(encoding="utf-8")
findings: list[str] = []
for rule in TEMPLATE_RULES:
for match in rule.pattern.finditer(text):
line = line_number(text, match.start())
snippet = match.group(0).replace("\n", " ")[:90]
findings.append(
f"{relative_path}:{line}: [{rule.code}] {rule.message} ({snippet})"
)
return findings
def scan_css() -> list[str]:
path = ROOT / CSS_PATH
if not path.exists():
return [f"{CSS_PATH}: missing required observability CSS"]
text = path.read_text(encoding="utf-8")
findings: list[str] = []
for snippet in REQUIRED_CSS_SNIPPETS:
if snippet not in text:
findings.append(f"{CSS_PATH}: missing required token/class `{snippet}`")
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")
for finding in findings:
print(f"- {finding}")
return 1
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
if __name__ == "__main__":
sys.exit(main())