test(observability): add UI regression guard
Some checks failed
CD Pipeline / deploy (push) Failing after 1m39s

This commit is contained in:
OoO
2026-05-05 15:04:21 +08:00
parent fa3e0884ad
commit 07c9e200d0
3 changed files with 171 additions and 0 deletions

View File

@@ -36,6 +36,24 @@ AI 觀測台是「AI 中樞控制室」,不是 Bootstrap 報表頁。任何新
## 驗收指令
### 1. Repo 靜態 UI guard
先跑本地 guard避免把已知 UI/UX 回歸重新帶進 repo
```bash
python3 scripts/check_observability_ui.py
```
Guard 會檢查:
- Times / Georgia 等非規範標題字型。
- SQL/Jinja exception 文案外露。
- 圖表 `style="height:..."` 與靜態 `style="width:..."`
- 過大標題 clamp 與純白 hover/card surface。
- 觀測台 CSS 必要 token / utility class 是否仍存在。
### 2. Production 10 頁 HTTP 巡檢
```bash
python3 - <<'PY'
import urllib.request

View File

@@ -0,0 +1,151 @@
#!/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+&quot;|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())

View File

@@ -259,6 +259,7 @@
.momo-observability-mode .rag-panel,
.momo-observability-mode .quality-panel,
.momo-observability-mode .ppt-panel,
.momo-observability-mode .obs-table-shell,
.momo-observability-mode [class$="-table-shell"] {
position: relative;
border: 1px solid var(--obs-line) !important;
@@ -453,6 +454,7 @@
}
/* Data surface polish: tables, matrices, lists */
.momo-observability-mode .obs-table-shell,
.momo-observability-mode [class$="-table-shell"],
.momo-observability-mode .table-responsive {
background: