Files
ewoooc/templates/admin/budget.html
ogt 26496bfbe8
All checks were successful
CD Pipeline / deploy (push) Successful in 1m7s
fix: shorten governance pages around growth decisions
2026-06-25 11:22:32 +08:00

97 lines
10 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>
.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 花費與節流狀態,把預算留給能推動業績的任務。</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">預算超線時立即重算節流。</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 %}