feat(p52): topbar 觀測台健康指示燈 + RAG 反饋圓餅圖
All checks were successful
CD Pipeline / deploy (push) Successful in 2m30s

P-1: topbar AI 觀測台 indicator(全頁可見)
- ewoooc_base.html topbar 加「🛰 AI 觀測台」icon button
- 紅色 badge 顯示告警數量(4 維度任一觸發即計數):
  • 三主機任一掛掉
  • 待審 episode > 0
  • 過去 1h 錯誤率 ≥ 30%
  • 預算任一 ≥ 90%
- 新 GET /observability/api/health_indicator
  輕量 JSON API(4 query 跨 host_health_probes/learning_episodes/
  ai_calls/ai_call_budgets)
- topbar polling 每 60s 自動刷新 + tooltip 顯示具體告警內容
- 全部頁面(包括 / 商品看板、所有觀測頁)topbar 都看得到健康狀態

P-2: quality_trend RAG 反饋圓餅圖(doughnut)
- 取代原本卡片網格佈局
- 1-5 星依綠→紅漸層著色(5=綠、3=黃、1=紅)
- 圓餅 + 右側表格雙視角(chart 配對 raw 數字)
- chart.js doughnut + tooltip 顯示筆數+佔比

效益:
- 統帥從任何頁面(不限觀測台)都能瞄一眼右上角看當前 AI 健康
- 快樂路徑:「正常」綠色 icon · 異常路徑:「紅色 badge + 數字」立即吸睛
- 圓餅圖比原網格更直觀「分布」感

Phase 38→52 累計 17 commits / 10 觀測頁 / DB 100% / 4 chart.js / 全頁 indicator。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
OoO
2026-05-04 20:20:34 +08:00
parent e0a8d87c2c
commit 2a3ea6f581
3 changed files with 214 additions and 15 deletions

View File

@@ -150,27 +150,38 @@
</div>
</div>
<!-- Phase 47 K-5: RAG 整體 feedback 分布 -->
<!-- Phase 47 K-5 + Phase 52 P-2: RAG 整體 feedback 圓餅圖 -->
{% if rag_overall_dist %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-poll me-2"></i>RAG 整體反饋分布(過去 {{ days }} 日)</strong>
<small class="text-muted">資料來源rag_query_log.feedback_score含全 caller1-5 分)</small>
</div>
<div class="card-body">
<div class="row g-2">
{% set total_fb = (rag_overall_dist | sum(attribute='count')) or 1 %}
{% for r in rag_overall_dist %}
<div class="col-md-2 col-sm-4">
<div class="border rounded p-2 text-center">
<small class="text-muted d-block">
{% for _ in range(r.score) %}<i class="fas fa-star text-warning"></i>{% endfor %}
{% for _ in range(5 - r.score) %}<i class="far fa-star text-muted"></i>{% endfor %}
</small>
<strong style="font-size: 1.4em;">{{ r.count }}</strong>
<small class="d-block text-muted">{{ "%.1f"|format(r.count / total_fb * 100) }}%</small>
</div>
<div class="row g-2 align-items-center">
<div class="col-md-5">
<canvas id="ragFeedbackPieChart" height="180"></canvas>
</div>
<div class="col-md-7">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr><th>星等</th><th class="text-end">筆數</th><th class="text-end">佔比</th></tr>
</thead>
<tbody>
{% set total_fb = (rag_overall_dist | sum(attribute='count')) or 1 %}
{% for r in rag_overall_dist %}
<tr>
<td>
{% for _ in range(r.score) %}<i class="fas fa-star text-warning"></i>{% endfor %}
{% for _ in range(5 - r.score) %}<i class="far fa-star text-muted"></i>{% endfor %}
<small class="ms-1 text-muted">{{ r.score }} 分</small>
</td>
<td class="text-end"><strong>{{ r.count }}</strong></td>
<td class="text-end">{{ "%.1f"|format(r.count / total_fb * 100) }}%</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
</div>
</div>
</div>
@@ -240,8 +251,39 @@
{% endif %}
<p class="text-muted mt-3"><small>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 47 — Caller 反饋趨勢
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 52 — Caller 反饋趨勢(含 RAG 圓餅圖)
6 表深挖rag_query_log / learning_episodes / ai_insights / action_plans / action_outcomes / agent_strategy_weights
</small></p>
</div>
{% if rag_overall_dist %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function() {
const data = {{ rag_overall_dist | tojson }};
const el = document.getElementById('ragFeedbackPieChart');
if (!el || !data.length) return;
// 1-5 分對應綠→紅漸層
const colorMap = {1: '#dc3545', 2: '#fd7e14', 3: '#ffc107', 4: '#84c454', 5: '#198754'};
new Chart(el, {
type: 'doughnut',
data: {
labels: data.map(r => `${r.score}`),
datasets: [{
data: data.map(r => r.count),
backgroundColor: data.map(r => colorMap[r.score] || '#6c757d'),
borderWidth: 1, borderColor: '#fff',
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: {
legend: { position: 'right', labels: { font: { size: 12 } } },
tooltip: { callbacks: { label: c => `${c.label}: ${c.parsed} 筆 (${(c.parsed / data.reduce((a,r)=>a+r.count,0) * 100).toFixed(1)}%)` } }
}
}
});
})();
</script>
{% endif %}
{% endblock %}