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 %}
-
+
-
+