Files
ewoooc/templates/admin/observability_overview.html
OoO 250dd58172
All checks were successful
CD Pipeline / deploy (push) Successful in 59s
統一觀測台新版工作台規範
2026-05-13 19:39:33 +08:00

667 lines
24 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: 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);
--obs-green: #4f8a5b;
--obs-amber: #b8792f;
--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: 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 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;
}
.obs-kicker {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.36rem 0.58rem;
border: 1px solid rgba(201, 100, 66, 0.22);
border-radius: var(--momo-radius-lg, 8px);
background: var(--momo-bg-elevated, #fdfaf3);
color: var(--obs-accent);
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.7rem 0 0.35rem;
max-width: 820px;
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: var(--momo-text-body, 0.875rem);
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: 104px;
padding: 0.92rem;
border: 1px solid var(--obs-line);
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: 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-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 {
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: var(--momo-radius-lg, 8px);
background: var(--obs-card);
box-shadow: var(--momo-shadow-md, 0 2px 8px rgba(42, 37, 32, 0.06));
}
.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: var(--momo-text-title, 1.0625rem);
font-weight: 800;
letter-spacing: 0;
}
.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: var(--momo-radius-lg, 8px);
background: var(--momo-bg-elevated, #fdfaf3);
}
.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;
justify-content: space-between;
gap: 0.7rem;
align-items: baseline;
}
.obs-host-name {
font-weight: 800;
}
.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;
}
.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: var(--momo-radius-lg, 8px);
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: 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;
}
.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: var(--momo-radius-lg, 8px);
background: var(--momo-bg-elevated, #fdfaf3);
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: var(--momo-radius-lg, 8px);
padding: 1rem;
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: var(--momo-text-title, 1.0625rem);
font-weight: 850;
letter-spacing: 0;
}
.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: var(--momo-radius-lg, 8px);
color: var(--obs-ink);
text-decoration: none;
background: var(--momo-bg-elevated, #fdfaf3);
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: 6px;
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> 01 指揮總覽 <span class="obs-kicker-date">· {{ today }}</span></span>
<h1 class="obs-title">AI 觀測戰情室</h1>
<p class="obs-lede">
私有 AI 中樞的第一入口三主機、AI 呼叫、RAG 學習、MCP、AIOps、預算與 PPT 視覺審核收斂到同一張工作台。所有數字只讀正式資料來源;缺資料時呈現可診斷空狀態。
</p>
<div class="obs-command-strip">
<div class="obs-signal">
<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">24 小時呼叫</div>
<div class="obs-signal-value">{{ "{:,}".format(ai.total) 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">成本水位</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 命中率</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">快取命中 {{ "%.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">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>
</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 }} 次探測 · 平均 {{ h.avg_ms }} ms · 24 小時視窗</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>
主機探測尚無資料。請確認第 029 號資料遷移與排程探測任務是否已啟動。
</div>
{% endif %}
</div>
</article>
{% if summary.budget_alerts %}
<article class="obs-panel">
<div class="obs-panel-head">
<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>
</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 呼叫</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 命中</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>
{% endif %}
</div>
</article>
<article class="obs-panel">
<div class="obs-panel-head">
<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>
</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">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>
{% 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">自動化後勤</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 呼叫</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">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>
<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">系統成本</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">權杖量、成本、錯誤、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">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">晉升守門與人工審核。</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">呼叫端品質、蒸餾池、根因建議。</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>
資料來源主機探測、AI 呼叫、預算、學習事件、RAG 查詢、MCP 呼叫、事件與自癒、PPT 審核。
</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)}% 可用率` }
}
},
scales: {
x: { display: false },
y: { display: false, min: 0, max: 100 }
}
}
});
});
})();
</script>
{% endblock %}