Files
ewoooc/templates/admin/budget.html
OoO 19f1340f5c
All checks were successful
CD Pipeline / deploy (push) Successful in 2m42s
feat(p38): admin 觀測台 6 頁完整繁中化 + 加入導航選單
問題:
1. 6 個 /observability/* 頁面標題與欄位英文殘留(違反設計憲法繁中要求)
2. 6 頁完全沒掛 navbar,使用者進不去(只能彼此 footer link 互連)
3. emoji 取代 Font Awesome,違反設計規範

修補:
- _navbar.html 新增「AI 觀測台」dropdown(位於 AI 助手 與 雲端匯入 之間)
  - AI 監控組:AI 呼叫總覽 / 主機健康監控 / 預算控管
  - AI 學習組:RAG 學習晉升審核 / Caller 反饋趨勢 / PPT 視覺審核歷史
- 6 個 admin/observability template 全面繁中化:
  - 標題、表格欄位、按鈕、badge 文字、JS alert 文案
  - emoji → Font Awesome icon(fa-heartbeat / fa-chart-bar / fa-wallet / fa-brain / fa-comments / fa-search 等)
  - 移除 5 處 footer 手寫 link 條(已由 navbar 取代,避免雙寫)
- routes/admin_observability_routes.py 6 個 render_template 加 active_page='obs_*'
  讓 navbar dropdown 正確高亮

完整覆蓋:host_health / ai_calls_dashboard / budget / promotion_review / quality_trend / ppt_audit_history

設計規範對齊:仍待 Phase 後續工作(ewoooc_base.html 框架升級 + --momo-* design token)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 18:49:44 +08:00

108 lines
4.4 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 "base.html" %}
{% block title %}預算控管{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<h2 class="mb-3"><i class="fas fa-wallet me-2"></i>預算控管
<small class="text-muted">ai_call_budgets × 當月實際支出即時對比</small>
</h2>
{% if error %}<div class="alert alert-warning"><strong><i class="fas fa-exclamation-triangle me-1"></i></strong> {{ error }}</div>{% endif %}
<p class="text-muted small">
依 ADR-028 預算 + Phase 20 成本節流:每小時 cron 檢查當月支出,
線性外推月底成本超 110% → 自動節流claude→gemini fallback
手動編輯預算後立即生效(不需重啟)。
</p>
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>週期</th><th>供應商</th>
<th class="text-end">已花費 (USD)</th>
<th>預算 (USD)</th><th>警示閾值 %</th>
<th class="text-end">使用率</th><th>狀態</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 }}" style="width: 110px;">
</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 }}" style="width: 80px;">
</td>
<td class="text-end">
<strong class="{% if r.ratio >= 1.10 %}text-danger{% elif r.ratio >= 0.8 %}text-warning{% else %}text-success{% endif %}">
{{ "%.0f"|format(r.ratio * 100) }}%
</strong>
</td>
<td>
{% if r.throttled %}
<span class="badge bg-danger"><i class="fas fa-exclamation-triangle me-1"></i>已節流</span>
{% elif r.ratio >= 0.8 %}
<span class="badge bg-warning"><i class="fas fa-exclamation me-1"></i>接近上限</span>
{% else %}
<span class="badge bg-success"><i class="fas fa-check me-1"></i>正常</span>
{% endif %}
</td>
<td><small>{{ r.updated_at }}</small></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="9" class="text-center text-muted">無預算資料(需先跑 migrations/025</td></tr>
{% endfor %}
</tbody>
</table>
<p class="text-muted mt-3"><small>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 29 — 預算控管
</small></p>
</div>
<script>
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 || 'unknown'));
btn.disabled = false; btn.innerHTML = '<i class="fas fa-save me-1"></i>儲存';
}
} catch (e) {
alert('Error' + e);
btn.disabled = false; btn.innerHTML = '<i class="fas fa-save me-1"></i>儲存';
}
}
</script>
{% endblock %}