Files
ewoooc/templates/admin/budget.html
OoO f2aece5b71
All checks were successful
CD Pipeline / deploy (push) Successful in 1m9s
perf: 外部化觀測台圖表腳本
2026-05-18 09:30:34 +08:00

97 lines
10 KiB
HTML
Raw Permalink 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>
.gov-hero, .gov-panel, .gov-table-shell { border:1px solid var(--obs-line); border-radius:26px; background:var(--obs-card); box-shadow:0 16px 38px rgba(70,46,28,.08); }
.gov-hero { padding:clamp(1.2rem,2.4vw,2rem); background:radial-gradient(circle at 12% 14%, rgba(201,100,66,.18), transparent 24rem), radial-gradient(circle at 90% 8%, rgba(184,121,47,.16), transparent 22rem), linear-gradient(135deg,rgba(255,248,239,.98),rgba(255,255,255,.74)); }
.gov-kicker { color:var(--obs-accent); font-size:.76rem; letter-spacing:.13em; text-transform:uppercase; font-weight:850; }
.gov-title { margin:.45rem 0 .25rem; font-family: var(--momo-font-display, "Inter", "Noto Sans TC", system-ui, sans-serif); font-size:var(--obs-title-size); letter-spacing: 0; line-height:.98; }
.gov-subtitle { color:var(--obs-muted); max-width:850px; line-height:1.7; }
.gov-actions { display:flex; flex-wrap:wrap; gap:.6rem; align-items:center; margin-top:1rem; }
.gov-command { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:.75rem; margin-top:1rem; }
.gov-signal { padding:.95rem; border:1px solid var(--obs-line); border-radius:20px; background:rgba(255,255,255,.62); }
.gov-label { color:var(--obs-muted); font-size:.72rem; letter-spacing:.1em; text-transform:uppercase; }
.gov-value { display:block; margin-top:.28rem; font-size:var(--obs-value-size); font-weight:880; letter-spacing: 0; }
.gov-note { color:var(--obs-muted); font-size:.8rem; margin-top:.25rem; }
.gov-grid { display:grid; grid-template-columns:minmax(0,1.18fr) minmax(330px,.82fr); gap:1rem; margin-top:1rem; }
.gov-stack { display:grid; gap:1rem; }
.gov-panel-head, .gov-table-title { display:flex; justify-content:space-between; align-items:flex-start; gap:1rem; padding:1.05rem 1.1rem .25rem; }
.gov-panel-title, .gov-table-title h3 { margin:.15rem 0 0; font-size:1.1rem; font-weight:850; letter-spacing: 0; }
.gov-panel-body { padding:1rem 1.1rem 1.1rem; }
.gov-mini-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.7rem; }
.gov-mini { padding:.85rem; border:1px solid var(--obs-line); border-radius:18px; background:rgba(255,255,255,.58); }
.gov-mini strong { display:block; margin-top:.24rem; font-size:1.35rem; letter-spacing: 0; }
.gov-table-shell { overflow:hidden; margin-top:1rem; }
.gov-chart { height:280px; }
.strategy-card { padding:.8rem; border:1px solid var(--obs-line); border-radius:18px; background:rgba(255,255,255,.6); margin-bottom:.65rem; }
.status-good { color:var(--obs-green); } .status-warn { color:var(--obs-amber); } .status-bad { color:var(--obs-red); } .status-blue { color:var(--obs-blue); }
@media (max-width:1100px){ .gov-command{grid-template-columns:repeat(2,minmax(0,1fr));}.gov-grid{grid-template-columns:1fr;} }
@media (max-width:720px){ .gov-command,.gov-mini-grid{grid-template-columns:1fr;} }
</style>
{% import "admin/_observability_labels.html" as obs_label %}
{% set total_budget = namespace(value=0) %}{% set total_spent = namespace(value=0) %}{% set warn_count = namespace(value=0) %}{% set throttled_count = namespace(value=0) %}
{% for r in rows %}{% set total_budget.value = total_budget.value + (r.budget_usd or 0) %}{% set total_spent.value = total_spent.value + (r.spent or 0) %}{% if r.ratio >= 0.8 %}{% set warn_count.value = warn_count.value + 1 %}{% endif %}{% if r.throttled %}{% set throttled_count.value = throttled_count.value + 1 %}{% endif %}{% endfor %}
{% set total_ratio = (total_spent.value / total_budget.value * 100) if total_budget.value > 0 else 0 %}
<div class="container-fluid mt-3">
<section class="gov-hero">
<div class="gov-kicker"><i class="fas fa-wallet me-1"></i> AI 成本治理 · 預算 / 節流 / RAG 策略</div>
<h1 class="gov-title">AI 成本治理艙</h1>
<p class="gov-subtitle">這頁回答一個問題AI 中樞花錢是否仍在治理邊界內?預算、實際支出、月底推估、節流狀態與 RAG 策略建議集中在同一個工作台。</p>
<div class="gov-actions"><button class="btn btn-warning btn-sm" onclick="forceThrottle()"><i class="fas fa-bolt me-1"></i>立即重算節流狀態</button><span class="text-muted small">超過 110% 時不用等排程,直接重算供應商節流。</span></div>
<div class="gov-command">
<div class="gov-signal"><div class="gov-label">當月花費</div><span class="gov-value">${{ "%.2f"|format(total_spent.value) }}</span><div class="gov-note">預算 ${{ "%.2f"|format(total_budget.value) }}</div></div>
<div class="gov-signal"><div class="gov-label">預算使用率</div><span class="gov-value {% if total_ratio >= 110 %}status-bad{% elif total_ratio >= 80 %}status-warn{% else %}status-good{% endif %}">{{ "%.0f"|format(total_ratio) }}%</span><div class="gov-note">全供應商加總</div></div>
<div class="gov-signal"><div class="gov-label">預警</div><span class="gov-value {% if warn_count.value > 0 %}status-warn{% else %}status-good{% endif %}">{{ warn_count.value }}</span><div class="gov-note">使用率 ≥ 80%</div></div>
<div class="gov-signal"><div class="gov-label">已節流</div><span class="gov-value {% if throttled_count.value > 0 %}status-bad{% else %}status-good{% endif %}">{{ throttled_count.value }}</span><div class="gov-note">已啟動成本節流</div></div>
</div>
</section>
{% if error %}<div class="alert alert-warning mt-3"><strong><i class="fas fa-triangle-exclamation me-1"></i></strong>{{ error }}</div>{% endif %}
<section class="gov-grid">
<div class="gov-stack">
<article class="gov-table-shell">
<div class="gov-table-title"><div><div class="gov-label">預算線</div><h3>預算線與節流狀態</h3></div></div>
<div class="table-responsive"><table class="table table-hover mb-0"><thead class="table-light"><tr><th>週期</th><th>供應商</th><th class="text-end">已花費</th><th>預算</th><th>閾值</th><th class="text-end">使用率</th><th>狀態</th><th>動作</th></tr></thead><tbody>{% for r in rows %}<tr {% if r.throttled %}class="table-danger"{% elif r.ratio >= 0.8 %}class="table-warning"{% endif %}><td><span class="badge bg-secondary">{{ r.period }}</span></td><td><code>{{ obs_label.provider(r.provider) }}</code></td><td class="text-end">${{ "%.2f"|format(r.spent) }}</td><td><input type="number" step="0.01" min="0.01" value="{{ "%.2f"|format(r.budget_usd) }}" class="form-control form-control-sm budget-input" data-budget-id="{{ r.id }}" ></td><td><input type="number" min="1" max="100" value="{{ r.alert_pct }}" class="form-control form-control-sm alert-input" data-budget-id="{{ r.id }}" ></td><td class="text-end"><strong class="{% if r.ratio >= 1.10 %}status-bad{% elif r.ratio >= 0.8 %}status-warn{% else %}status-good{% endif %}">{{ "%.0f"|format(r.ratio * 100) }}%</strong></td><td>{% if r.throttled %}<span class="badge bg-danger">已節流</span>{% elif r.ratio >= 0.8 %}<span class="badge bg-warning">接近上限</span>{% else %}<span class="badge bg-success">正常</span>{% endif %}</td><td><button class="btn btn-primary btn-sm save-budget-btn" data-budget-id="{{ r.id }}" onclick="saveBudget({{ r.id }})"><i class="fas fa-save me-1"></i>儲存</button></td></tr>{% else %}<tr><td colspan="8" class="text-center text-muted">無預算資料(需先跑 migrations/025</td></tr>{% endfor %}</tbody></table></div>
</article>
{% if cost_trend_30d %}
<article class="gov-panel"><div class="gov-panel-head"><div><div class="gov-label">30 日成本趨勢</div><h2 class="gov-panel-title">每日成本堆疊趨勢</h2></div></div><div class="gov-panel-body"><div class="gov-chart"><canvas id="costTrend30dChart"></canvas></div></div></article>
{% endif %}
</div>
<aside class="gov-stack">
{% if provider_cost_month %}
<article class="gov-panel"><div class="gov-panel-head"><div><div class="gov-label">供應商分布</div><h2 class="gov-panel-title">當月成本分布</h2></div></div><div class="gov-panel-body"><div class="obs-chart-frame"><canvas id="providerCostPieChart"></canvas></div></div></article>
{% endif %}
{% if top_cost_callers %}
<article class="gov-panel"><div class="gov-panel-head"><div><div class="gov-label">燃燒率</div><h2 class="gov-panel-title">Top 5 燒錢呼叫端</h2></div></div><div class="gov-panel-body">{% set max_cost = (top_cost_callers | map(attribute='cost') | max) or 1 %}{% for c in top_cost_callers %}<div class="gov-mini mb-2"><div class="d-flex justify-content-between"><code>{{ c.caller }}</code><strong>${{ "%.2f"|format(c.cost) }}</strong></div><div class="progress mt-2 obs-progress-xs"><div class="progress-bar" style="width: {{ (c.cost / max_cost * 100) | round | int }}%;"></div></div><small class="text-muted">{{ "{:,}".format(c.calls) }} 次呼叫 · {{ "{:,}".format(c.tokens) }} 權杖</small></div>{% endfor %}</div></article>
{% endif %}
</aside>
</section>
{% if budget_strategies %}
<section class="gov-panel mt-3"><div class="gov-panel-head"><div><div class="gov-label">RAG 策略</div><h2 class="gov-panel-title">RAG 自動策略建議</h2></div></div><div class="gov-panel-body">{% for s in budget_strategies %}<div class="strategy-card"><span class="badge bg-info me-1">{{ obs_label.insight(s.insight_type) }}</span><span class="badge bg-secondary me-1">相似度 {{ "%.2f"|format(s.similarity) }}</span><span>{{ s.content }}{% if s.content|length >= 240 %}…{% endif %}</span></div>{% endfor %}</div></section>
{% endif %}
{% if price_rec_7d %}
<section class="gov-panel mt-3"><div class="gov-panel-head"><div><div class="gov-label">商業產出</div><h2 class="gov-panel-title">AI 價格決策 7 日</h2></div></div><div class="gov-panel-body"><div class="gov-mini-grid">{% for p in price_rec_7d %}<div class="gov-mini"><span class="gov-label">{{ obs_label.strategy(p.strategy) }}</span><strong>{{ p.count }}</strong><small class="text-muted">信心 {{ "%.2f"|format(p.avg_confidence) }}</small></div>{% endfor %}</div></div></section>
{% endif %}
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — AI 成本治理艙</small></p>
</div>
{% set budget_payload = {
'providerCostMonth': provider_cost_month | default([]),
'costTrend30d': cost_trend_30d
} %}
<template id="obs-budget-data">{{ budget_payload | tojson }}</template>
<script src="{{ url_for('static', filename='js/analysis-chart-theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/observability-charts.js') }}"></script>
{% endblock %}