Files
ewoooc/templates/admin/budget.html
OoO 99d2f3c543
All checks were successful
CD Pipeline / deploy (push) Successful in 2m25s
fix(p32): admin URL prefix /admin → /observability — 避開 188 nginx SPA shadow
Root cause(curl 實證):
  prod 188 nginx 對 /admin/* 設 try_files → SPA index.html fallback
  → Phase 27-31 的 6 個 Flask admin 路由全被 nginx 攔截
  → 外部 GET /admin/ai_calls 回 7480 byte 靜態 HTML(同 etag = SPA shell)
  → 我之前說「6 admin 頁 prod 200」是回了 200,但 body 不是 Flask 渲染

修法:
  Blueprint url_prefix /admin → /observability
  → 6 個觀測頁實際生效在 /observability/* 不被 SPA 遮蔽
  → SPA frontend 仍擁有 /admin/* 命名空間(不破壞既有前端)

更新範圍:
  - routes/admin_observability_routes.py: url_prefix + 註解全改
  - 6 templates: 所有 href / fetch() 路徑改 /observability/
  - tests/test_admin_observability_routes.py: client.get/post 路徑改
  - 10/10 smoke tests 仍 PASS

統帥訪問新路徑:
  http://192.168.0.188/observability/ai_calls
  http://192.168.0.188/observability/host_health
  http://192.168.0.188/observability/budget
  http://192.168.0.188/observability/promotion_review
  http://192.168.0.188/observability/quality_trend
  http://192.168.0.188/observability/ppt_audit_history
2026-05-04 14:13:27 +08:00

111 lines
4.2 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="/observability/ai_calls">AI Calls</a>
| <a href="/observability/host_health">Host Health</a>
| <a href="/observability/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(`/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.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 %}