Files
ewoooc/scripts/check_observability_ui.py
OoO 07c9e200d0
Some checks failed
CD Pipeline / deploy (push) Failing after 1m39s
test(observability): add UI regression guard
2026-05-05 15:04:21 +08:00

152 lines
4.5 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")
@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",
]
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 main() -> int:
findings: list[str] = []
for template_path in TEMPLATE_PATHS:
findings.extend(scan_file(Path(template_path)))
findings.extend(scan_css())
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)}")
return 0
if __name__ == "__main__":
sys.exit(main())