重整觀測台總覽新版視覺
All checks were successful
CD Pipeline / deploy (push) Successful in 58s

This commit is contained in:
OoO
2026-05-13 19:19:59 +08:00
parent bd6310365e
commit bc47f79a77
5 changed files with 137 additions and 87 deletions

View File

@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.120"
SYSTEM_VERSION = "V10.121"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -25,6 +25,7 @@ WEB_CSS_PATH = Path("web/static/css/observability-system.css")
SHELL_PATH = Path("templates/components/_ewoooc_shell.html")
BASE_PATH = Path("templates/ewoooc_base.html")
ROUTE_PATH = Path("routes/admin_observability_routes.py")
OVERVIEW_PATH = Path("templates/admin/observability_overview.html")
@dataclass(frozen=True)
@@ -136,6 +137,23 @@ FORBIDDEN_BASE_PATTERNS = [
),
]
FORBIDDEN_OVERVIEW_COPY = [
"AI Observability Command Room",
"Risk Signals",
"AI Calls / 24h",
"Host Cascade",
"AI Runtime",
"Learning Loop",
">Command<",
">Runtime<",
">Quality<",
"RAG hits",
"Cache hits",
"30d episodes",
"MCP calls",
"force-throttle",
]
def line_number(text: str, index: int) -> int:
return text.count("\n", 0, index) + 1
@@ -240,6 +258,21 @@ def scan_base_topbar() -> list[str]:
return findings
def scan_overview_copy() -> list[str]:
path = ROOT / OVERVIEW_PATH
if not path.exists():
return [f"{OVERVIEW_PATH}: missing required overview page"]
text = path.read_text(encoding="utf-8")
findings: list[str] = []
for snippet in FORBIDDEN_OVERVIEW_COPY:
if snippet in text:
findings.append(
f"{OVERVIEW_PATH}: legacy English overview copy `{snippet}` must be localized to the V2 workbench language"
)
return findings
def scan_shell() -> list[str]:
path = ROOT / SHELL_PATH
if not path.exists():
@@ -324,6 +357,7 @@ def main() -> int:
findings.extend(scan_css())
findings.extend(scan_shell())
findings.extend(scan_base_topbar())
findings.extend(scan_overview_copy())
findings.extend(scan_nav_contract())
if findings:
@@ -337,6 +371,7 @@ def main() -> int:
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) + len(FORBIDDEN_BASE_PATTERNS)}")
print(f"- overview copy guardrails checked: {len(FORBIDDEN_OVERVIEW_COPY)}")
print(f"- nav contract checked: {len(OBSERVABILITY_PAGES)} pages")
return 0

View File

@@ -1975,7 +1975,7 @@
.ppt-title
) {
color: var(--obs-ink) !important;
font-family: var(--momo-font-family, "Inter", "Noto Sans TC", system-ui, sans-serif) !important;
font-family: var(--momo-font-display, "JetBrains Mono", "Noto Sans TC", ui-monospace, monospace) !important;
font-size: var(--obs-title-size) !important;
font-weight: 800 !important;
letter-spacing: 0 !important;

View File

@@ -8,7 +8,7 @@
--obs-ink: var(--momo-ink, #302720);
--obs-muted: var(--momo-muted, #8b8077);
--obs-paper: var(--momo-paper, #fff8ef);
--obs-card: rgba(255, 252, 246, 0.92);
--obs-card: var(--momo-bg-surface, #faf7f0);
--obs-line: rgba(86, 64, 48, 0.14);
--obs-accent: var(--momo-accent, #c96442);
--obs-accent-soft: rgba(201, 100, 66, 0.12);
@@ -17,28 +17,31 @@
--obs-red: #b94b45;
--obs-blue: #4f6f8f;
color: var(--obs-ink);
font-family: var(--momo-font-family, "Inter", "Noto Sans TC", system-ui, sans-serif);
}
.obs-hero {
position: relative;
overflow: hidden;
border: 1px solid var(--obs-line);
border-radius: 28px;
padding: clamp(1.35rem, 3vw, 2.3rem);
background:
radial-gradient(circle at 12% 18%, rgba(201, 100, 66, 0.2), transparent 28%),
radial-gradient(circle at 84% 12%, rgba(79, 111, 143, 0.18), transparent 30%),
linear-gradient(135deg, #fff8ed 0%, #f7eadb 48%, #fffdf8 100%);
box-shadow: 0 22px 55px rgba(70, 46, 28, 0.1);
border-radius: var(--momo-radius-lg, 8px);
padding: clamp(1.05rem, 2.2vw, 1.65rem);
background-color: var(--momo-bg-surface, #faf7f0);
background-image: var(--obs-dot);
background-size: 13px 13px;
box-shadow: var(--momo-shadow-md, 0 2px 8px rgba(42, 37, 32, 0.06));
}
.obs-hero::after {
content: "";
position: absolute;
inset: auto -8% -42% 42%;
height: 260px;
background: repeating-linear-gradient(90deg, rgba(201, 100, 66, 0.1) 0 1px, transparent 1px 18px);
transform: rotate(-7deg);
inset: auto 1rem 1rem auto;
width: 8.5rem;
height: 8.5rem;
border: 1px solid color-mix(in srgb, var(--obs-accent) 22%, transparent);
background-image: var(--obs-dot);
background-size: 10px 10px;
opacity: 0.46;
pointer-events: none;
}
@@ -46,30 +49,36 @@
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.42rem 0.72rem;
padding: 0.36rem 0.58rem;
border: 1px solid rgba(201, 100, 66, 0.22);
border-radius: 999px;
background: rgba(255, 255, 255, 0.58);
border-radius: var(--momo-radius-lg, 8px);
background: var(--momo-bg-elevated, #fdfaf3);
color: var(--obs-accent);
font-size: 0.78rem;
letter-spacing: 0.12em;
text-transform: uppercase;
font-family: var(--momo-font-family-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: var(--momo-text-label, 0.6875rem);
font-weight: 800;
letter-spacing: 0.06em;
}
.obs-kicker-date {
color: var(--obs-muted);
}
.obs-title {
margin: 0.85rem 0 0.4rem;
margin: 0.7rem 0 0.35rem;
max-width: 820px;
font-family:'Noto Sans TC','Inter',sans-serif;
font-size: clamp(1.9rem, 3.2vw, 2.75rem);
line-height: 0.95;
letter-spacing: -0.055em;
font-family: var(--momo-font-display, "JetBrains Mono", "Noto Sans TC", ui-monospace, monospace);
font-size: var(--obs-title-size, 1.8rem);
line-height: 1.18;
letter-spacing: 0;
font-weight: 800;
}
.obs-lede {
max-width: 760px;
margin: 0;
color: var(--obs-muted);
font-size: 1rem;
font-size: var(--momo-text-body, 0.875rem);
line-height: 1.75;
}
@@ -83,28 +92,30 @@
.obs-signal {
position: relative;
z-index: 1;
min-height: 112px;
padding: 1rem;
min-height: 104px;
padding: 0.92rem;
border: 1px solid var(--obs-line);
border-radius: 20px;
background: rgba(255, 255, 255, 0.66);
border-radius: var(--momo-radius-lg, 8px);
background: var(--momo-bg-surface, #faf7f0);
backdrop-filter: blur(8px);
}
.obs-signal-label,
.obs-section-eyebrow,
.obs-route-code {
color: var(--obs-muted);
font-size: 0.72rem;
letter-spacing: 0.11em;
text-transform: uppercase;
color: color-mix(in srgb, var(--obs-accent) 76%, var(--obs-muted));
font-family: var(--momo-font-family-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: var(--momo-text-label, 0.6875rem);
font-weight: 800;
letter-spacing: 0.06em;
}
.obs-signal-value {
margin-top: 0.45rem;
font-size: clamp(1.65rem, 3vw, 2.35rem);
font-weight: 800;
letter-spacing: -0.04em;
font-family: var(--momo-font-family-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: var(--obs-value-size, 1.85rem);
font-weight: 850;
letter-spacing: 0;
}
.obs-signal-note {
@@ -122,9 +133,9 @@
.obs-panel {
border: 1px solid var(--obs-line);
border-radius: 24px;
border-radius: var(--momo-radius-lg, 8px);
background: var(--obs-card);
box-shadow: 0 14px 36px rgba(70, 46, 28, 0.07);
box-shadow: var(--momo-shadow-md, 0 2px 8px rgba(42, 37, 32, 0.06));
}
.obs-panel-head {
@@ -137,9 +148,9 @@
.obs-panel-title {
margin: 0.18rem 0 0;
font-size: 1.1rem;
font-size: var(--momo-text-title, 1.0625rem);
font-weight: 800;
letter-spacing: -0.02em;
letter-spacing: 0;
}
.obs-panel-body {
@@ -158,13 +169,13 @@
align-items: center;
padding: 0.9rem;
border: 1px solid var(--obs-line);
border-radius: 18px;
background: rgba(255, 255, 255, 0.62);
border-radius: var(--momo-radius-lg, 8px);
background: var(--momo-bg-elevated, #fdfaf3);
}
.obs-host-card.is-good { border-left: 5px solid var(--obs-green); }
.obs-host-card.is-warn { border-left: 5px solid var(--obs-amber); }
.obs-host-card.is-bad { border-left: 5px solid var(--obs-red); }
.obs-host-card.is-good { border-left: 4px solid var(--obs-green); }
.obs-host-card.is-warn { border-left: 4px solid var(--obs-amber); }
.obs-host-card.is-bad { border-left: 4px solid var(--obs-red); }
.obs-host-top {
display: flex;
@@ -178,9 +189,10 @@
}
.obs-host-pct {
font-family: var(--momo-font-family-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 1.35rem;
font-weight: 850;
letter-spacing: -0.04em;
letter-spacing: 0;
}
.obs-host-meta,
@@ -230,7 +242,7 @@
align-items: center;
gap: 0.35rem;
padding: 0.32rem 0.55rem;
border-radius: 999px;
border-radius: var(--momo-radius-lg, 8px);
background: var(--obs-accent-soft);
color: var(--obs-accent);
font-size: 0.76rem;
@@ -251,15 +263,16 @@
.obs-mini {
padding: 0.85rem;
border: 1px solid var(--obs-line);
border-radius: 18px;
background: rgba(255, 255, 255, 0.58);
border-radius: var(--momo-radius-lg, 8px);
background: var(--momo-bg-elevated, #fdfaf3);
}
.obs-mini strong {
display: block;
margin-top: 0.25rem;
font-family: var(--momo-font-family-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 1.45rem;
letter-spacing: -0.04em;
letter-spacing: 0;
}
.obs-status-good { color: var(--obs-green); }
@@ -273,8 +286,8 @@
gap: 0.45rem;
padding: 0.58rem 0.78rem;
border: 1px solid rgba(201, 100, 66, 0.25);
border-radius: 999px;
background: rgba(255, 255, 255, 0.65);
border-radius: var(--momo-radius-lg, 8px);
background: var(--momo-bg-elevated, #fdfaf3);
color: var(--obs-accent);
text-decoration: none;
font-size: 0.82rem;
@@ -298,16 +311,18 @@
.obs-route-group {
border: 1px solid var(--obs-line);
border-radius: 24px;
border-radius: var(--momo-radius-lg, 8px);
padding: 1rem;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.74), rgba(255, 248, 239, 0.82));
background-color: var(--momo-bg-surface, #faf7f0);
background-image: var(--obs-dot);
background-size: 13px 13px;
}
.obs-route-group h3 {
margin: 0.25rem 0 0.85rem;
font-size: 1.1rem;
font-size: var(--momo-text-title, 1.0625rem);
font-weight: 850;
letter-spacing: 0;
}
.obs-route-list {
@@ -322,10 +337,10 @@
align-items: center;
padding: 0.72rem;
border: 1px solid transparent;
border-radius: 16px;
border-radius: var(--momo-radius-lg, 8px);
color: var(--obs-ink);
text-decoration: none;
background: rgba(255, 255, 255, 0.58);
background: var(--momo-bg-elevated, #fdfaf3);
transition: border-color 160ms ease, transform 160ms ease, background 160ms ease;
}
@@ -341,7 +356,7 @@
place-items: center;
width: 2rem;
height: 2rem;
border-radius: 12px;
border-radius: 6px;
background: var(--obs-accent-soft);
color: var(--obs-accent);
}
@@ -403,32 +418,32 @@
<div class="obs-war-room">
<section class="obs-hero">
<span class="obs-kicker"><i class="fas fa-satellite-dish"></i> AI Observability Command Room · {{ today }}</span>
<span class="obs-kicker"><i class="fas fa-satellite-dish"></i> 01 指揮總覽 <span class="obs-kicker-date">· {{ today }}</span></span>
<h1 class="obs-title">AI 觀測戰情室</h1>
<p class="obs-lede">
這裡是 momo-pro 私有 AI 中樞的第一入口三主機、AI 呼叫、RAG 學習、MCP、AIOps、預算與 PPT 視覺審核全部收斂到同一張作戰圖。數字只讀正式資料來源;沒有資料時顯示診斷空狀態,不用假 KPI 撐場面
私有 AI 中樞的第一入口三主機、AI 呼叫、RAG 學習、MCP、AIOps、預算與 PPT 視覺審核收斂到同一張工作台。所有數字只讀正式資料來源;資料時呈現可診斷空狀態。
</p>
<div class="obs-command-strip">
<div class="obs-signal">
<div class="obs-signal-label">Risk Signals</div>
<div class="obs-signal-label">即時風險</div>
<div class="obs-signal-value {% if risk_count == 0 %}obs-status-good{% elif risk_count <= 2 %}obs-status-warn{% else %}obs-status-bad{% endif %}">{{ risk_count }}</div>
<div class="obs-signal-note">主機、預算、錯誤率、待審核的即時風險數</div>
</div>
<div class="obs-signal">
<div class="obs-signal-label">AI Calls / 24h</div>
<div class="obs-signal-label">24 小時呼叫</div>
<div class="obs-signal-value">{{ "{:,}".format(ai.total) if ai else '—' }}</div>
<div class="obs-signal-note">Token {{ "{:,}".format(ai.tokens) if ai else '—' }}</div>
<div class="obs-signal-note">權杖量:{{ "{:,}".format(ai.tokens) if ai else '—' }}</div>
</div>
<div class="obs-signal">
<div class="obs-signal-label">Cost</div>
<div class="obs-signal-label">成本水位</div>
<div class="obs-signal-value">${{ "%.2f"|format(ai.cost_24h) if ai else '0.00' }}</div>
<div class="obs-signal-note">當月累計 ${{ "%.2f"|format(summary.month_cost|default(0)) }}</div>
</div>
<div class="obs-signal">
<div class="obs-signal-label">RAG Hit Rate</div>
<div class="obs-signal-label">RAG 命中率</div>
<div class="obs-signal-value obs-status-blue">{{ "%.1f"|format(ai.rag_rate) if ai else '—' }}{% if ai %}%{% endif %}</div>
<div class="obs-signal-note">Cache {{ "%.0f"|format(ai.cache_rate) if ai else '—' }}{% if ai %}%{% endif %}</div>
<div class="obs-signal-note">快取命中 {{ "%.0f"|format(ai.cache_rate) if ai else '—' }}{% if ai %}%{% endif %}</div>
</div>
</div>
</section>
@@ -438,7 +453,7 @@
<article class="obs-panel">
<div class="obs-panel-head">
<div>
<div class="obs-section-eyebrow">Host Cascade</div>
<div class="obs-section-eyebrow">02 主機級聯</div>
<h2 class="obs-panel-title">三主機生命線</h2>
</div>
<a class="obs-link-button" href="/observability/host_health"><i class="fas fa-arrow-right"></i>主機健康</a>
@@ -453,7 +468,7 @@
<span class="obs-host-name">{{ h.label }}</span>
<span class="obs-host-pct {% if h.uptime_pct >= 99 %}obs-status-good{% elif h.uptime_pct >= 90 %}obs-status-warn{% else %}obs-status-bad{% endif %}">{{ "%.1f"|format(h.uptime_pct) }}%</span>
</div>
<div class="obs-host-meta">{{ h.up }}/{{ h.total }} probes · 平均 {{ h.avg_ms }} ms · 24h 視窗</div>
<div class="obs-host-meta">{{ h.up }}/{{ h.total }} 次探測 · 平均 {{ h.avg_ms }} ms · 24 小時視窗</div>
</div>
<div class="obs-sparkline">
{% if host_sparkline.get(h.label) %}
@@ -468,7 +483,7 @@
{% else %}
<div class="obs-empty">
<i class="fas fa-triangle-exclamation me-1"></i>
host_health_probes 無資料。請確認 migration 029 與 scheduler probe job 是否已啟動。
主機探測尚無資料。請確認第 029 號資料遷移與排程探測任務是否已啟動。
</div>
{% endif %}
</div>
@@ -478,7 +493,7 @@
<article class="obs-panel">
<div class="obs-panel-head">
<div>
<div class="obs-section-eyebrow">Budget Guard</div>
<div class="obs-section-eyebrow">預算守門</div>
<h2 class="obs-panel-title">預算告警</h2>
</div>
<a class="obs-link-button" href="/observability/budget"><i class="fas fa-bolt"></i>處理預算</a>
@@ -509,7 +524,7 @@
<article class="obs-panel">
<div class="obs-panel-head">
<div>
<div class="obs-section-eyebrow">AI Runtime</div>
<div class="obs-section-eyebrow">AI 呼叫</div>
<h2 class="obs-panel-title">呼叫品質</h2>
</div>
<a class="obs-link-button" href="/observability/ai_calls"><i class="fas fa-chart-bar"></i>詳情</a>
@@ -519,8 +534,8 @@
<div class="obs-metric-grid">
<div class="obs-mini"><span class="obs-microcopy">錯誤率</span><strong class="{% if ai.error_rate >= 15 %}obs-status-bad{% elif ai.error_rate >= 5 %}obs-status-warn{% else %}obs-status-good{% endif %}">{{ "%.1f"|format(ai.error_rate) }}%</strong></div>
<div class="obs-mini"><span class="obs-microcopy">失敗</span><strong>{{ ai.errors }}</strong></div>
<div class="obs-mini"><span class="obs-microcopy">RAG hits</span><strong class="obs-status-blue">{{ ai.rag_hits }}</strong></div>
<div class="obs-mini"><span class="obs-microcopy">Cache hits</span><strong>{{ ai.cache_hits }}</strong></div>
<div class="obs-mini"><span class="obs-microcopy">RAG 命中</span><strong class="obs-status-blue">{{ ai.rag_hits }}</strong></div>
<div class="obs-mini"><span class="obs-microcopy">快取命中</span><strong>{{ ai.cache_hits }}</strong></div>
</div>
{% else %}
<div class="obs-empty">ai_calls 無 24h 資料。</div>
@@ -531,7 +546,7 @@
<article class="obs-panel">
<div class="obs-panel-head">
<div>
<div class="obs-section-eyebrow">Learning Loop</div>
<div class="obs-section-eyebrow">學習閉環</div>
<h2 class="obs-panel-title">RAG 學習閘</h2>
</div>
<a class="obs-link-button" href="/observability/promotion_review"><i class="fas fa-brain"></i>審核</a>
@@ -540,7 +555,7 @@
{% if summary.episodes %}
<div class="obs-metric-grid">
<div class="obs-mini"><span class="obs-microcopy">待審核</span><strong class="{% if summary.episodes.pending > 0 %}obs-status-warn{% else %}obs-status-good{% endif %}">{{ summary.episodes.pending }}</strong></div>
<div class="obs-mini"><span class="obs-microcopy">30d episodes</span><strong>{{ summary.episodes.total_30d }}</strong></div>
<div class="obs-mini"><span class="obs-microcopy">30 日樣本</span><strong>{{ summary.episodes.total_30d }}</strong></div>
<div class="obs-mini"><span class="obs-microcopy">已晉升</span><strong class="obs-status-good">{{ summary.episodes.approved_30d }}</strong></div>
<div class="obs-mini"><span class="obs-microcopy">晉升率</span><strong>{{ "%.0f"|format(summary.episodes.approval_rate) }}%</strong></div>
</div>
@@ -553,7 +568,7 @@
<article class="obs-panel">
<div class="obs-panel-head">
<div>
<div class="obs-section-eyebrow">AIOps / MCP / PPT</div>
<div class="obs-section-eyebrow">自動化後勤</div>
<h2 class="obs-panel-title">自動化後勤</h2>
</div>
</div>
@@ -561,7 +576,7 @@
<div class="obs-metric-grid">
<div class="obs-mini"><span class="obs-microcopy">AIOps 未解</span><strong class="{% if summary.aiops and summary.aiops.incidents_open > 0 %}obs-status-bad{% else %}obs-status-good{% endif %}">{{ summary.aiops.incidents_open if summary.aiops else '—' }}</strong></div>
<div class="obs-mini"><span class="obs-microcopy">自癒成功率</span><strong>{{ "%.0f"|format(summary.aiops.heal_rate) if summary.aiops else '—' }}{% if summary.aiops %}%{% endif %}</strong></div>
<div class="obs-mini"><span class="obs-microcopy">MCP calls</span><strong>{{ "{:,}".format(summary.mcp.total) if summary.mcp else '—' }}</strong></div>
<div class="obs-mini"><span class="obs-microcopy">MCP 呼叫</span><strong>{{ "{:,}".format(summary.mcp.total) if summary.mcp else '—' }}</strong></div>
<div class="obs-mini"><span class="obs-microcopy">PPT 通過率</span><strong>{{ "%.0f"|format(summary.ppt.pass_rate) if summary.ppt and summary.ppt.total > 0 else '—' }}{% if summary.ppt and summary.ppt.total > 0 %}%{% endif %}</strong></div>
</div>
</div>
@@ -571,7 +586,7 @@
<section class="obs-route-map" aria-label="AI 觀測台子頁入口">
<div class="obs-route-group">
<div class="obs-section-eyebrow">Command</div>
<div class="obs-section-eyebrow">03 戰情指揮</div>
<h3>戰情室</h3>
<div class="obs-route-list">
<a class="obs-route-card" href="/observability/agent_orchestration"><span class="obs-route-icon"><i class="fas fa-network-wired"></i></span><span><span class="obs-route-title">Agent 編排矩陣</span><span class="obs-route-desc">四 Agent × 模型 × MCP × RAG 分工。</span></span><span class="obs-route-code">02</span></a>
@@ -580,17 +595,17 @@
</div>
<div class="obs-route-group">
<div class="obs-section-eyebrow">Runtime</div>
<div class="obs-section-eyebrow">系統成本</div>
<h3>系統與成本</h3>
<div class="obs-route-list">
<a class="obs-route-card" href="/observability/host_health"><span class="obs-route-icon"><i class="fas fa-heartbeat"></i></span><span><span class="obs-route-title">主機健康</span><span class="obs-route-desc">三主機、MCP、AIOps、AutoHeal。</span></span><span class="obs-route-code">04</span></a>
<a class="obs-route-card" href="/observability/ai_calls"><span class="obs-route-icon"><i class="fas fa-chart-bar"></i></span><span><span class="obs-route-title">AI 呼叫</span><span class="obs-route-desc">Token、成本、錯誤、RAG × MCP 矩陣。</span></span><span class="obs-route-code">05</span></a>
<a class="obs-route-card" href="/observability/budget"><span class="obs-route-icon"><i class="fas fa-wallet"></i></span><span><span class="obs-route-title">預算控管</span><span class="obs-route-desc">供應商成本與 force-throttle</span></span><span class="obs-route-code">06</span></a>
<a class="obs-route-card" href="/observability/ai_calls"><span class="obs-route-icon"><i class="fas fa-chart-bar"></i></span><span><span class="obs-route-title">AI 呼叫</span><span class="obs-route-desc">權杖量、成本、錯誤、RAG × MCP 矩陣。</span></span><span class="obs-route-code">05</span></a>
<a class="obs-route-card" href="/observability/budget"><span class="obs-route-icon"><i class="fas fa-wallet"></i></span><span><span class="obs-route-title">預算控管</span><span class="obs-route-desc">供應商成本與強制節流</span></span><span class="obs-route-code">06</span></a>
</div>
</div>
<div class="obs-route-group">
<div class="obs-section-eyebrow">Quality</div>
<div class="obs-section-eyebrow">RAG 品質</div>
<h3>RAG 與品質</h3>
<div class="obs-route-list">
<a class="obs-route-card" href="/observability/promotion_review"><span class="obs-route-icon"><i class="fas fa-brain"></i></span><span><span class="obs-route-title">RAG 晉升審核</span><span class="obs-route-desc">Promotion Gate 與人工審核。</span></span><span class="obs-route-code">07</span></a>
@@ -603,7 +618,7 @@
<p class="obs-microcopy mt-3">
<i class="fas fa-database me-1"></i>
資料來源:host_health_probes / ai_calls / ai_call_budgets / learning_episodes / rag_query_log / mcp_calls / incidents / heal_logs / ppt_audit_results
資料來源:主機探測、AI 呼叫、預算、學習事件、RAG 查詢、MCP 呼叫、事件與自癒、PPT 審核
</p>
</div>
@@ -636,7 +651,7 @@
legend: { display: false },
tooltip: {
displayColors: false,
callbacks: { label: c => `${c.label}: ${c.parsed.y.toFixed(0)}% uptime` }
callbacks: { label: c => `${c.label}: ${c.parsed.y.toFixed(0)}% 可用率` }
}
},
scales: {

View File

@@ -1975,7 +1975,7 @@
.ppt-title
) {
color: var(--obs-ink) !important;
font-family: var(--momo-font-family, "Inter", "Noto Sans TC", system-ui, sans-serif) !important;
font-family: var(--momo-font-display, "JetBrains Mono", "Noto Sans TC", ui-monospace, monospace) !important;
font-size: var(--obs-title-size) !important;
font-weight: 800 !important;
letter-spacing: 0 !important;