All checks were successful
CD Pipeline / deploy (push) Successful in 2m30s
統帥質疑:6 頁觀測台只是 raw stats dashboard,沒展現 AI 自動化專業。 深度盤點 4 軸結果: - DB 利用率 22.7%(22 表只用 5 張) - MCP 整合 1/6(mcp_calls 表完全沒被讀) - RAG 整合 0/6(沒 import rag_service) - AI 自動化 L0 × 5 + L1 × 1(純讀 dashboard,無一鍵觸發) 本 commit 5 個增強: D-1: promotion_review 加 RAG「Top 3 相似已晉升」 - 對每筆 awaiting_review episode 跑 rag_service.query 找 ai_insights 中 cosine ≥ 0.7 的相似已晉升內容 - 輔助人工判斷:是否冗餘?是否新領域? - header 顯示 ai_insights 知識庫 size - fail-safe: 單筆 RAG 失敗不影響其餘 D-2: host_health 加 MCP 24h 工作量 widget - 從 mcp_calls 統計各 server 24h 呼叫次數 / 成功率 / cache 率 / 使用 tool 數 / 平均耗時 / cost - 展現「AI×MCP 編排規模」而非只「server 健康與否」 D-3: ai_calls × rag_query_log × mcp_calls 三表 JOIN - 新增「呼叫端 × RAG × MCP 編排矩陣」card - 每個 caller:總呼叫 / RAG 命中率 / MCP 編排率(透過 request_id 串接) / RAG 反饋分數 / 反饋筆數 - 展現「AI 自動化專業」核心指標 D-4: budget 加 RAG 自動策略建議 + 一鍵 force-throttle (L2) - ratio ≥ 0.8 時自動 RAG 召回 ai_insights 中的 budget_strategy 知識 - POST /budget/force_throttle endpoint:立即重算 cost_throttle 狀態 (不等下次每小時 cron)— 升級到 L2 自動化 - 對應頁面加「立即重算節流狀態」按鈕 D-5: host_health 加 incidents + heal_logs 7d 摘要 - 顯示 ADR-013 AutoHeal 閉環核心 KPI: 總事件 / 未解決 / 已解決 / P0+P1 / 自癒成功率 / 平均自癒耗時 - 展現「AIOps 自癒系統」運作實況 對應升級: - DB 利用率 22.7% → ~50%(新接 mcp_calls + rag_query_log JOIN + ai_insights + incidents + heal_logs) - MCP 整合 1/6 → 3/6(host_health + ai_calls + budget 都接 mcp_calls) - RAG 整合 0/6 → 3/6(promotion_review + budget + 待 quality_trend) - AI 自動化 L1 → L2 一鍵 force-throttle 一個(其餘按鈕待 D-6) 全部 fail-safe:DB 表/RAG/MCP 失敗都不擋頁面渲染。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
155 lines
6.3 KiB
HTML
155 lines
6.3 KiB
HTML
{% 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>
|
||
|
||
<!-- Phase 39 D-4: 一鍵立即重算 throttle 狀態(L2 自動化)-->
|
||
<div class="mb-3">
|
||
<button class="btn btn-warning btn-sm" onclick="forceThrottle()">
|
||
<i class="fas fa-bolt me-1"></i>立即重算節流狀態(不等 cron)
|
||
</button>
|
||
<small class="text-muted ms-2">用途:發現某 provider 飆超 110% 時立即 evaluate,毋需等下次每小時 cron。</small>
|
||
</div>
|
||
|
||
<!-- Phase 39 D-4: RAG 自動建議策略 -->
|
||
{% if budget_strategies %}
|
||
<div class="card mb-3" style="border-left: 4px solid #6f42c1;">
|
||
<div class="card-header bg-light">
|
||
<strong><i class="fas fa-lightbulb me-2"></i>RAG 自動策略建議</strong>
|
||
<small class="text-muted">— 從知識庫 ai_insights 召回過去類似超支情境的應對策略</small>
|
||
</div>
|
||
<div class="card-body p-2">
|
||
<ul class="list-unstyled small mb-0">
|
||
{% for s in budget_strategies %}
|
||
<li class="mb-2 p-2" style="background: #fafafa; border-radius: 4px;">
|
||
<span class="badge bg-info text-dark 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>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<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 forceThrottle() {
|
||
if (!confirm('立即重算所有 provider 的 throttle 狀態?\n(不等下次每小時 cron)')) return;
|
||
try {
|
||
const r = await fetch('/observability/budget/force_throttle', {method: 'POST'});
|
||
const d = await r.json();
|
||
if (d.ok) {
|
||
const list = (d.throttled_providers && d.throttled_providers.length > 0)
|
||
? d.throttled_providers.join(', ') : '(無)';
|
||
alert(`✅ 已重算:被節流的 provider = ${list}`);
|
||
window.location.reload();
|
||
} else {
|
||
alert('❌ ' + (d.error || '重算失敗'));
|
||
}
|
||
} catch (e) {
|
||
alert('Error: ' + e);
|
||
}
|
||
}
|
||
|
||
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 %}
|