Files
ewoooc/templates/admin/observability_overview.html
OoO 668d98cd3c
Some checks failed
CD Pipeline / deploy (push) Failing after 9m49s
fix(observability): 清理硬編碼樣式與圖表容器
2026-05-05 14:41:00 +08:00

652 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "ewoooc_base.html" %}
{% block title %}AI 觀測戰情室{% endblock %}
{% block ewooo_content %}
<style>
.obs-war-room {
--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-line: rgba(86, 64, 48, 0.14);
--obs-accent: var(--momo-accent, #c96442);
--obs-accent-soft: rgba(201, 100, 66, 0.12);
--obs-green: #4f8a5b;
--obs-amber: #b8792f;
--obs-red: #b94b45;
--obs-blue: #4f6f8f;
color: var(--obs-ink);
}
.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);
}
.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);
pointer-events: none;
}
.obs-kicker {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.42rem 0.72rem;
border: 1px solid rgba(201, 100, 66, 0.22);
border-radius: 999px;
background: rgba(255, 255, 255, 0.58);
color: var(--obs-accent);
font-size: 0.78rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.obs-title {
margin: 0.85rem 0 0.4rem;
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;
}
.obs-lede {
max-width: 760px;
margin: 0;
color: var(--obs-muted);
font-size: 1rem;
line-height: 1.75;
}
.obs-command-strip {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.75rem;
margin-top: 1.45rem;
}
.obs-signal {
position: relative;
z-index: 1;
min-height: 112px;
padding: 1rem;
border: 1px solid var(--obs-line);
border-radius: 20px;
background: rgba(255, 255, 255, 0.66);
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;
}
.obs-signal-value {
margin-top: 0.45rem;
font-size: clamp(1.65rem, 3vw, 2.35rem);
font-weight: 800;
letter-spacing: -0.04em;
}
.obs-signal-note {
margin-top: 0.25rem;
color: var(--obs-muted);
font-size: 0.82rem;
}
.obs-grid {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.65fr);
gap: 1rem;
margin-top: 1rem;
}
.obs-panel {
border: 1px solid var(--obs-line);
border-radius: 24px;
background: var(--obs-card);
box-shadow: 0 14px 36px rgba(70, 46, 28, 0.07);
}
.obs-panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1.1rem 1.15rem 0.25rem;
}
.obs-panel-title {
margin: 0.18rem 0 0;
font-size: 1.1rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.obs-panel-body {
padding: 1rem 1.15rem 1.15rem;
}
.obs-host-list {
display: grid;
gap: 0.75rem;
}
.obs-host-card {
display: grid;
grid-template-columns: minmax(0, 1fr) 150px;
gap: 0.85rem;
align-items: center;
padding: 0.9rem;
border: 1px solid var(--obs-line);
border-radius: 18px;
background: rgba(255, 255, 255, 0.62);
}
.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-top {
display: flex;
justify-content: space-between;
gap: 0.7rem;
align-items: baseline;
}
.obs-host-name {
font-weight: 800;
}
.obs-host-pct {
font-size: 1.35rem;
font-weight: 850;
letter-spacing: -0.04em;
}
.obs-host-meta,
.obs-microcopy {
color: var(--obs-muted);
font-size: 0.82rem;
}
.obs-sparkline {
height: 54px;
}
.obs-alert-table {
width: 100%;
border-collapse: separate;
border-spacing: 0 0.48rem;
}
.obs-alert-table th {
color: var(--obs-muted);
font-size: 0.7rem;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 700;
}
.obs-alert-table td {
padding: 0.62rem;
background: rgba(255, 255, 255, 0.68);
border-top: 1px solid var(--obs-line);
border-bottom: 1px solid var(--obs-line);
}
.obs-alert-table td:first-child {
border-left: 1px solid var(--obs-line);
border-radius: 14px 0 0 14px;
}
.obs-alert-table td:last-child {
border-right: 1px solid var(--obs-line);
border-radius: 0 14px 14px 0;
text-align: right;
}
.obs-pill {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.32rem 0.55rem;
border-radius: 999px;
background: var(--obs-accent-soft);
color: var(--obs-accent);
font-size: 0.76rem;
font-weight: 750;
}
.obs-stack {
display: grid;
gap: 1rem;
}
.obs-metric-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.7rem;
}
.obs-mini {
padding: 0.85rem;
border: 1px solid var(--obs-line);
border-radius: 18px;
background: rgba(255, 255, 255, 0.58);
}
.obs-mini strong {
display: block;
margin-top: 0.25rem;
font-size: 1.45rem;
letter-spacing: -0.04em;
}
.obs-status-good { color: var(--obs-green); }
.obs-status-warn { color: var(--obs-amber); }
.obs-status-bad { color: var(--obs-red); }
.obs-status-blue { color: var(--obs-blue); }
.obs-link-button {
display: inline-flex;
align-items: center;
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);
color: var(--obs-accent);
text-decoration: none;
font-size: 0.82rem;
font-weight: 750;
transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease;
}
.obs-link-button:hover {
transform: translateY(-1px);
background: rgba(255, 252, 247, 0.96);
box-shadow: 0 10px 24px rgba(201, 100, 66, 0.14);
color: var(--obs-accent);
}
.obs-route-map {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.9rem;
margin-top: 1rem;
}
.obs-route-group {
border: 1px solid var(--obs-line);
border-radius: 24px;
padding: 1rem;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.74), rgba(255, 248, 239, 0.82));
}
.obs-route-group h3 {
margin: 0.25rem 0 0.85rem;
font-size: 1.1rem;
font-weight: 850;
}
.obs-route-list {
display: grid;
gap: 0.55rem;
}
.obs-route-card {
display: grid;
grid-template-columns: 2rem minmax(0, 1fr) auto;
gap: 0.72rem;
align-items: center;
padding: 0.72rem;
border: 1px solid transparent;
border-radius: 16px;
color: var(--obs-ink);
text-decoration: none;
background: rgba(255, 255, 255, 0.58);
transition: border-color 160ms ease, transform 160ms ease, background 160ms ease;
}
.obs-route-card:hover {
border-color: rgba(201, 100, 66, 0.28);
background: rgba(255, 252, 247, 0.96);
color: var(--obs-ink);
transform: translateX(3px);
}
.obs-route-icon {
display: inline-grid;
place-items: center;
width: 2rem;
height: 2rem;
border-radius: 12px;
background: var(--obs-accent-soft);
color: var(--obs-accent);
}
.obs-route-title {
display: block;
font-weight: 800;
}
.obs-route-desc {
display: block;
margin-top: 0.08rem;
color: var(--obs-muted);
font-size: 0.76rem;
line-height: 1.45;
}
.obs-empty {
padding: 1rem;
border: 1px dashed rgba(184, 121, 47, 0.38);
border-radius: 18px;
background: rgba(184, 121, 47, 0.08);
color: var(--obs-amber);
}
@media (max-width: 1180px) {
.obs-command-strip,
.obs-route-map {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.obs-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.obs-command-strip,
.obs-route-map,
.obs-metric-grid {
grid-template-columns: 1fr;
}
.obs-host-card {
grid-template-columns: 1fr;
}
}
</style>
{% set ai = summary.ai_calls if summary.ai_calls else none %}
{% set host_count = summary.hosts|length if summary.hosts else 0 %}
{% set host_bad = namespace(value=0) %}
{% if summary.hosts %}
{% for h in summary.hosts %}
{% if h.uptime_pct < 90 %}{% set host_bad.value = host_bad.value + 1 %}{% endif %}
{% endfor %}
{% endif %}
{% set risk_count = (summary.budget_alerts|length if summary.budget_alerts else 0) + host_bad.value + (1 if ai and ai.error_rate >= 15 else 0) + (1 if summary.episodes and summary.episodes.pending > 0 else 0) %}
<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>
<h1 class="obs-title">AI 觀測戰情室</h1>
<p class="obs-lede">
這裡是 momo-pro 私有 AI 中樞的第一入口三主機、AI 呼叫、RAG 學習、MCP、AIOps、預算與 PPT 視覺審核全部收斂到同一張作戰圖。數字只讀正式資料來源;沒有資料時顯示診斷空狀態,不用假 KPI 撐場面。
</p>
<div class="obs-command-strip">
<div class="obs-signal">
<div class="obs-signal-label">Risk Signals</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-value">{{ "{:,}".format(ai.total) if ai else '—' }}</div>
<div class="obs-signal-note">Token {{ "{:,}".format(ai.tokens) if ai else '—' }}</div>
</div>
<div class="obs-signal">
<div class="obs-signal-label">Cost</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-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>
</div>
</section>
<section class="obs-grid">
<div class="obs-stack">
<article class="obs-panel">
<div class="obs-panel-head">
<div>
<div class="obs-section-eyebrow">Host Cascade</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>
</div>
<div class="obs-panel-body">
{% if summary.hosts %}
<div class="obs-host-list">
{% for h in summary.hosts %}
<div class="obs-host-card {% if h.uptime_pct >= 99 %}is-good{% elif h.uptime_pct >= 90 %}is-warn{% else %}is-bad{% endif %}">
<div>
<div class="obs-host-top">
<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>
<div class="obs-sparkline">
{% if host_sparkline.get(h.label) %}
<canvas data-host-sparkline="{{ h.label }}"></canvas>
{% else %}
<div class="obs-microcopy">尚無趨勢資料</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="obs-empty">
<i class="fas fa-triangle-exclamation me-1"></i>
host_health_probes 無資料。請確認 migration 029 與 scheduler probe job 是否已啟動。
</div>
{% endif %}
</div>
</article>
{% if summary.budget_alerts %}
<article class="obs-panel">
<div class="obs-panel-head">
<div>
<div class="obs-section-eyebrow">Budget Guard</div>
<h2 class="obs-panel-title">預算告警</h2>
</div>
<a class="obs-link-button" href="/observability/budget"><i class="fas fa-bolt"></i>處理預算</a>
</div>
<div class="obs-panel-body">
<table class="obs-alert-table">
<thead>
<tr><th>週期</th><th>供應商</th><th>已花費</th><th>預算</th><th>使用率</th></tr>
</thead>
<tbody>
{% for b in summary.budget_alerts %}
<tr>
<td><span class="obs-pill">{{ b.period }}</span></td>
<td><code>{{ b.provider }}</code></td>
<td>${{ "%.2f"|format(b.spent) }}</td>
<td>${{ "%.2f"|format(b.budget) }}</td>
<td><strong class="{% if b.ratio >= 1.0 %}obs-status-bad{% else %}obs-status-warn{% endif %}">{{ "%.0f"|format(b.ratio * 100) }}%</strong></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</article>
{% endif %}
</div>
<aside class="obs-stack">
<article class="obs-panel">
<div class="obs-panel-head">
<div>
<div class="obs-section-eyebrow">AI Runtime</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>
</div>
<div class="obs-panel-body">
{% if ai %}
<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>
{% else %}
<div class="obs-empty">ai_calls 無 24h 資料。</div>
{% endif %}
</div>
</article>
<article class="obs-panel">
<div class="obs-panel-head">
<div>
<div class="obs-section-eyebrow">Learning Loop</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>
</div>
<div class="obs-panel-body">
{% 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">已晉升</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>
{% else %}
<div class="obs-empty">learning_episodes 無 30d 資料。</div>
{% endif %}
</div>
</article>
<article class="obs-panel">
<div class="obs-panel-head">
<div>
<div class="obs-section-eyebrow">AIOps / MCP / PPT</div>
<h2 class="obs-panel-title">自動化後勤</h2>
</div>
</div>
<div class="obs-panel-body">
<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">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>
</article>
</aside>
</section>
<section class="obs-route-map" aria-label="AI 觀測台子頁入口">
<div class="obs-route-group">
<div class="obs-section-eyebrow">Command</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>
<a class="obs-route-card" href="/observability/business_intel"><span class="obs-route-icon"><i class="fas fa-briefcase"></i></span><span><span class="obs-route-title">商業面 × AI</span><span class="obs-route-desc">AI 決策是否真的轉成生意動作。</span></span><span class="obs-route-code">03</span></a>
</div>
</div>
<div class="obs-route-group">
<div class="obs-section-eyebrow">Runtime</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>
</div>
</div>
<div class="obs-route-group">
<div class="obs-section-eyebrow">Quality</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>
<a class="obs-route-card" href="/observability/rag_queries"><span class="obs-route-icon"><i class="fas fa-magnifying-glass-chart"></i></span><span><span class="obs-route-title">RAG 召回詳情</span><span class="obs-route-desc">Query hits、節省呼叫、反饋追蹤。</span></span><span class="obs-route-code">08</span></a>
<a class="obs-route-card" href="/observability/quality_trend"><span class="obs-route-icon"><i class="fas fa-comments"></i></span><span><span class="obs-route-title">反饋趨勢</span><span class="obs-route-desc">Caller 品質、蒸餾池、根因建議。</span></span><span class="obs-route-code">09</span></a>
<a class="obs-route-card" href="/observability/ppt_audit_history"><span class="obs-route-icon"><i class="fas fa-search"></i></span><span><span class="obs-route-title">PPT 視覺審核</span><span class="obs-route-desc">PPT audit、RAG 修法、AiderHeal。</span></span><span class="obs-route-code">10</span></a>
</div>
</div>
</section>
<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。
</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function() {
const data = {{ host_sparkline | tojson }};
document.querySelectorAll('canvas[data-host-sparkline]').forEach(el => {
const label = el.getAttribute('data-host-sparkline');
const sp = data[label];
if (!sp || !sp.hours || !sp.hours.length) return;
new Chart(el, {
type: 'line',
data: {
labels: sp.hours,
datasets: [{
data: sp.uptime_pct,
borderColor: '#c96442',
backgroundColor: 'rgba(201, 100, 66, 0.14)',
borderWidth: 1.8,
fill: true,
tension: 0.42,
pointRadius: 0,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
displayColors: false,
callbacks: { label: c => `${c.label}: ${c.parsed.y.toFixed(0)}% uptime` }
}
},
scales: {
x: { display: false },
y: { display: false, min: 0, max: 100 }
}
}
});
});
})();
</script>
{% endblock %}