fix(observability): polish topbar alert indicator
All checks were successful
CD Pipeline / deploy (push) Successful in 1m33s

This commit is contained in:
OoO
2026-05-05 21:52:45 +08:00
parent 422137efa8
commit d93ad659ba
3 changed files with 71 additions and 13 deletions

View File

@@ -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

View File

@@ -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;
}
</style>
{% set _scheduler = scheduler_stats|default({}) %}
{% set _momo_runs = _scheduler.get('momo_task', []) if _scheduler is mapping else [] %}

View File

@@ -59,14 +59,9 @@
</div>
{% endif %}
<a class="momo-icon-button" href="/observability/overview" title="AI 觀測台" id="momo-obs-link"
style="text-decoration: none; position: relative;">
<a class="momo-icon-button momo-obs-link" href="/observability/overview" title="AI 觀測台" id="momo-obs-link">
<i class="fas fa-satellite-dish"></i>
<span id="momo-obs-badge"
style="display: none; position: absolute; top: -2px; right: -2px;
background: #dc3545; color: white; border-radius: 50%;
font-size: 0.6em; padding: 2px 5px; min-width: 16px;
text-align: center; line-height: 1;"></span>
<span id="momo-obs-badge" class="momo-obs-badge" hidden></span>
</a>
<button class="momo-icon-button" type="button" title="說明">
<i class="fas fa-circle-question"></i>
@@ -360,11 +355,11 @@
link.title = d.tooltip || 'AI 觀測台';
if (d.alert_count > 0) {
badge.textContent = d.alert_count;
badge.style.display = 'inline-block';
link.style.color = '#dc3545';
badge.hidden = false;
link.classList.add('is-alert');
} else {
badge.style.display = 'none';
link.style.color = '';
badge.hidden = true;
link.classList.remove('is-alert');
}
} catch (e) { /* silent */ }
}