diff --git a/scripts/check_observability_ui.py b/scripts/check_observability_ui.py index 9381326..781f306 100644 --- a/scripts/check_observability_ui.py +++ b/scripts/check_observability_ui.py @@ -140,6 +140,22 @@ REQUIRED_BASE_SNIPPETS = [ "momo-observability-mode", "/observability/overview", "/observability/api/health_indicator", + "momo-obs-link", + "momo-obs-badge", + "classList.add('is-alert')", +] + +FORBIDDEN_BASE_PATTERNS = [ + Rule( + "inline_obs_badge_style", + re.compile(r"id=\"momo-obs-badge\"[^>]*style=", re.IGNORECASE | re.DOTALL), + "Topbar 觀測台 badge 不得使用 inline style;請使用 .momo-obs-badge。", + ), + Rule( + "hardcoded_obs_alert_color", + re.compile(r"(#dc3545|link\.style\.color|badge\.style\.display)", re.IGNORECASE), + "Topbar 觀測台告警狀態不得用 JS/inline 硬改色;請切換 is-alert / hidden。", + ), ] @@ -192,6 +208,23 @@ def scan_required_snippets(relative_path: Path, snippets: list[str], label: str) return findings +def scan_base_topbar() -> list[str]: + path = ROOT / BASE_PATH + if not path.exists(): + return [f"{BASE_PATH}: missing required base/topbar file"] + + text = path.read_text(encoding="utf-8") + findings = scan_required_snippets(BASE_PATH, REQUIRED_BASE_SNIPPETS, "base/topbar") + for rule in FORBIDDEN_BASE_PATTERNS: + for match in rule.pattern.finditer(text): + line = line_number(text, match.start()) + snippet = match.group(0).replace("\n", " ")[:90] + findings.append( + f"{BASE_PATH}:{line}: [{rule.code}] {rule.message} ({snippet})" + ) + return findings + + def scan_shell() -> list[str]: path = ROOT / SHELL_PATH if not path.exists(): @@ -273,7 +306,7 @@ def main() -> int: 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")) + findings.extend(scan_base_topbar()) findings.extend(scan_nav_contract()) if findings: @@ -286,7 +319,7 @@ def main() -> int: 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)}") + print(f"- base/topbar guardrails checked: {len(REQUIRED_BASE_SNIPPETS) + len(FORBIDDEN_BASE_PATTERNS)}") print(f"- nav contract checked: {len(OBSERVABILITY_NAV_ITEMS)} pages") return 0 diff --git a/templates/components/_ewoooc_shell.html b/templates/components/_ewoooc_shell.html index 65d6d66..f62d016 100644 --- a/templates/components/_ewoooc_shell.html +++ b/templates/components/_ewoooc_shell.html @@ -123,6 +123,36 @@ opacity: 0.68; font-size: 0.68rem; } + .momo-obs-link { + position: relative; + text-decoration: none; + } + .momo-obs-link.is-alert { + color: #c96442; + box-shadow: 0 0 0 3px rgba(201, 100, 66, 0.12); + } + .momo-obs-badge { + position: absolute; + top: -0.22rem; + right: -0.22rem; + min-width: 1.05rem; + height: 1.05rem; + padding: 0 0.28rem; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(255, 248, 238, 0.86); + background: linear-gradient(135deg, #d76a45, #9d4a32); + color: #fff8ee; + box-shadow: 0 8px 18px rgba(132, 58, 34, 0.26); + font-size: 0.62rem; + line-height: 1; + font-weight: 900; + } + .momo-obs-badge[hidden] { + display: none; + } {% set _scheduler = scheduler_stats|default({}) %} {% set _momo_runs = _scheduler.get('momo_task', []) if _scheduler is mapping else [] %} diff --git a/templates/ewoooc_base.html b/templates/ewoooc_base.html index 8934e36..fff38db 100644 --- a/templates/ewoooc_base.html +++ b/templates/ewoooc_base.html @@ -59,14 +59,9 @@ {% endif %} - + - +