96 lines
13 KiB
HTML
96 lines
13 KiB
HTML
{% 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:'Noto Sans TC','Inter',sans-serif; font-size:var(--obs-title-size); letter-spacing:-.055em; 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:-.045em; }
|
||
.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:-.025em; }
|
||
.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:-.04em; }
|
||
.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>
|
||
|
||
{% 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 Cost Governance · Budget / Throttle / RAG Strategy</div>
|
||
<h1 class="gov-title">AI 成本治理艙</h1>
|
||
<p class="gov-subtitle">這頁回答一個問題:AI 中樞花錢是否仍在治理邊界內?預算、實際支出、月底推估、節流狀態與 RAG 策略建議集中在同一個 cockpit。</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% 時不用等 cron,直接 evaluate provider throttle。</span></div>
|
||
<div class="gov-command">
|
||
<div class="gov-signal"><div class="gov-label">Month Spend</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">Budget Ratio</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">全 provider 加總</div></div>
|
||
<div class="gov-signal"><div class="gov-label">Warnings</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">Throttled</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">Budget Lines</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>{{ 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">30d Cost Trend</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">Provider Mix</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">Burn Rate</div><h2 class="gov-panel-title">Top 5 燒錢 caller</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) }} calls · {{ "{:,}".format(c.tokens) }} 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 Strategy</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">{{ 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">Business Output</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">{{ 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>Operation Ollama-First v5.0 — AI 成本治理艙</small></p>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||
<script>
|
||
(function() { const data = {{ provider_cost_month | default([]) | tojson }}; const el = document.getElementById('providerCostPieChart'); if (!el || !data.length) return; const colors = {'gcp_ollama':'#4f8a5b','ollama_secondary':'#7aaa82','ollama_111':'#a3cfa8','gemini':'#b8792f','claude':'#4f6f8f','nim':'#6aa6a6','openrouter':'#8b8077','nim_via_elephant':'#c96442'}; new Chart(el,{type:'doughnut',data:{labels:data.map(d=>d.provider),datasets:[{data:data.map(d=>d.cost),backgroundColor:data.map((d,i)=>colors[d.provider]||`hsl(${(i*47)%360},55%,55%)`),borderWidth:1,borderColor:'#fff'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:11}}}}}}); })();
|
||
(function() { const raw = {{ cost_trend_30d | tojson }}; if (!raw || !raw.length) return; const dateSet = [...new Set(raw.map(r=>r.date))].sort(); const providerSet = [...new Set(raw.map(r=>r.provider))]; const palette = ['#c96442','#b8792f','#4f8a5b','#4f6f8f','#6aa6a6','#8b8077','#a66a4a']; const datasets = providerSet.map((p,i)=>({label:p,data:dateSet.map(d=>{const row=raw.find(r=>r.date===d&&r.provider===p);return row?row.cost:0;}),backgroundColor:palette[i%palette.length]})); const el=document.getElementById('costTrend30dChart'); if(!el)return; new Chart(el,{type:'bar',data:{labels:dateSet,datasets},options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},scales:{x:{stacked:true},y:{stacked:true,beginAtZero:true,title:{display:true,text:'USD'}}}}}); })();
|
||
async function forceThrottle(){if(!confirm('立即重算所有 provider 的 throttle 狀態?'))return;try{const r=await fetch('/observability/budget/force_throttle',{method:'POST'});const d=await r.json();if(d.ok){alert(`✅ 已重算:被節流的 provider = ${(d.throttled_providers&&d.throttled_providers.length>0)?d.throttled_providers.join(', '):'(無)'}`);window.location.reload();}else{alert('❌ '+(d.error||'重算失敗'));}}catch(e){console.warn('budget_force_throttle_failed',e);alert('操作暫時無法完成,請稍後再試或查看系統日誌。');}}
|
||
async function saveBudget(id){const budgetInput=document.querySelector(`.budget-input[data-budget-id="${id}"]`);const alertInput=document.querySelector(`.alert-input[data-budget-id="${id}"]`);const btn=document.querySelector(`.save-budget-btn[data-budget-id="${id}"]`);btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i>';try{const r=await fetch(`/observability/budget/update/${id}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({budget_usd:parseFloat(budgetInput.value),alert_pct:parseInt(alertInput.value)})});const d=await r.json();if(d.ok){btn.innerHTML='<i class="fas fa-check"></i> 已儲存';setTimeout(()=>{btn.innerHTML='<i class="fas fa-save me-1"></i>儲存';btn.disabled=false;},1500);}else{alert('更新失敗:'+(d.error||'請稍後再試'));btn.disabled=false;btn.innerHTML='<i class="fas fa-save me-1"></i>儲存';}}catch(e){console.warn('budget_save_failed',e);alert('操作暫時無法完成,請稍後再試或查看系統日誌。');btn.disabled=false;btn.innerHTML='<i class="fas fa-save me-1"></i>儲存';}}
|
||
</script>
|
||
{% endblock %}
|