Files
ewoooc/templates/admin/budget.html
OoO 69ccf8029b
All checks were successful
CD Pipeline / deploy (push) Successful in 2m23s
feat(p29): 預算管理頁 + PPT vision 歷史頁 — 完成 6 個 admin 觀測頁
承接 Phase 27/28(48b8fda)剩 2 個前端頁:

1. /admin/budget — 預算編輯器
   - GET: ai_call_budgets × 當月 spent 即時對比 + throttle 狀態
   - POST /admin/budget/update/<id>: AJAX 編輯 budget_usd / alert_pct
   - 不需 restart 立即生效(cost_throttle hourly cron 自動讀新值)
   - ratio ≥80% 黃 / ≥110% 紅 / throttled 標 ⚠️ THROTTLED

2. /admin/ppt_audit_history — PPT 視覺審核歷史
   - 掃 reports/ 過去 7 日 .pptx 檔(檔名/大小/修改時間)
   - 顯示 PPT_VISION_ENABLED 狀態(true=daily 22:00 cron 自動跑)
   - 手動觸發 SOP 提示(SSH 188 跑單檔審核)

完工里程碑:6 個 admin 頁 + 1 個導覽
- /admin/ai_calls          (Phase 27)
- /admin/promotion_review  (Phase 27)
- /admin/quality_trend     (Phase 28)
- /admin/host_health       (Phase 28)
- /admin/budget            (Phase 29) ← 新增
- /admin/ppt_audit_history (Phase 29) ← 新增

Operation Ollama-First v5.0 — 前端互補互動系列收官
2026-05-04 13:44:08 +08:00

111 lines
4.1 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 %}Budget Manager{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<h2 class="mb-3">💰 Budget Manager
<small class="text-muted">ai_call_budgets × 當月 spent 即時對比</small>
</h2>
{% if error %}<div class="alert alert-warning"><strong>⚠️</strong> {{ error }}</div>{% endif %}
<p class="text-muted small">
依 ADR-028 預算 + Phase 20 cost_throttle每小時 cron 檢查當月 spent
線性外推月底成本超 110% → 自動 throttleclaude→gemini fallback
手動編輯 budget 後立即生效(不需 restart
</p>
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Period</th><th>Provider</th>
<th class="text-end">Spent (USD)</th>
<th>Budget (USD)</th><th>Alert %</th>
<th class="text-end">Ratio</th><th>狀態</th>
<th>Last Update</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">⚠️ THROTTLED</span>
{% elif r.ratio >= 0.8 %}
<span class="badge bg-warning">⚠ 接近上限</span>
{% else %}
<span class="badge bg-success">✅ 正常</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 }})">
💾 儲存
</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>
🤖 Operation Ollama-First v5.0 / Phase 29 — Budget Manager
| <a href="/admin/ai_calls">AI Calls</a>
| <a href="/admin/host_health">Host Health</a>
| <a href="/admin/promotion_review">Promotion Review</a>
</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.innerText = '⏳';
try {
const r = await fetch(`/admin/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.innerText = '✅';
setTimeout(() => { btn.innerText = '💾 儲存'; btn.disabled = false; }, 1500);
} else {
alert('更新失敗: ' + (d.error || 'unknown'));
btn.disabled = false; btn.innerText = '💾 儲存';
}
} catch (e) {
alert('Error: ' + e);
btn.disabled = false; btn.innerText = '💾 儲存';
}
}
</script>
{% endblock %}