feat(p38): admin 觀測台 6 頁完整繁中化 + 加入導航選單
All checks were successful
CD Pipeline / deploy (push) Successful in 2m42s

問題:
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>
This commit is contained in:
OoO
2026-05-04 18:49:44 +08:00
parent 9bc6664dc0
commit 19f1340f5c
8 changed files with 173 additions and 147 deletions

View File

@@ -111,6 +111,7 @@ def ai_calls_dashboard():
return render_template(
'admin/ai_calls_dashboard.html',
active_page='obs_ai_calls',
hours=hours,
caller_filter=caller_filter,
provider_filter=provider_filter,
@@ -144,6 +145,7 @@ def ai_calls_dashboard():
except Exception as e:
return render_template(
'admin/ai_calls_dashboard.html',
active_page='obs_ai_calls',
hours=hours, caller_filter=caller_filter,
provider_filter=provider_filter,
summary={}, by_provider=[], recent=[], callers=[],
@@ -187,12 +189,14 @@ def promotion_review_list():
return render_template(
'admin/promotion_review.html',
active_page='obs_promotion_review',
episodes=episodes,
error=None,
)
except Exception as e:
return render_template(
'admin/promotion_review.html',
active_page='obs_promotion_review',
episodes=[],
error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}',
)
@@ -254,6 +258,7 @@ def quality_trend_dashboard():
return render_template(
'admin/quality_trend.html',
active_page='obs_quality_trend',
days=days,
trends=[(c, info) for c, info in sorted_trends],
recommendations=recommendations,
@@ -262,6 +267,7 @@ def quality_trend_dashboard():
except Exception as e:
return render_template(
'admin/quality_trend.html',
active_page='obs_quality_trend',
days=days, trends=[], recommendations=[],
error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}',
)
@@ -322,9 +328,9 @@ def budget_dashboard():
'updated_at': b[5].strftime('%Y-%m-%d %H:%M') if b[5] else '-',
})
return render_template('admin/budget.html', rows=rows, error=None)
return render_template('admin/budget.html', active_page='obs_budget', rows=rows, error=None)
except Exception as e:
return render_template('admin/budget.html', rows=[],
return render_template('admin/budget.html', active_page='obs_budget', rows=[],
error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}')
finally:
session.close()
@@ -408,6 +414,7 @@ def ppt_audit_history():
return render_template(
'admin/ppt_audit_history.html',
active_page='obs_ppt_audit',
files=files,
vision_enabled=vision_enabled,
error=error,
@@ -467,6 +474,7 @@ def host_health_dashboard():
return render_template(
'admin/host_health.html',
active_page='obs_host_health',
ollama_hosts=ollama_hosts,
mcp_status=mcp_status,
throttle_state=throttle_state,

View File

@@ -1,15 +1,15 @@
{% extends "base.html" %}
{% block title %}AI Calls Dashboard{% endblock %}
{% block title %}AI 呼叫總覽{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<h2 class="mb-3">📊 AI Calls Dashboard
<h2 class="mb-3"><i class="fas fa-chart-bar me-2"></i>AI 呼叫總覽
<small class="text-muted">過去 {{ hours }} 小時</small>
</h2>
{% if error %}
<div class="alert alert-warning"><strong>⚠️</strong> {{ error }}</div>
<div class="alert alert-warning"><strong><i class="fas fa-exclamation-triangle me-1"></i></strong> {{ error }}</div>
{% endif %}
<!-- 篩選 bar -->
@@ -25,7 +25,7 @@
</div>
<div class="col-auto">
<select name="caller" class="form-select form-select-sm">
<option value="">全部 caller</option>
<option value="">全部呼叫端</option>
{% for c in callers %}
<option value="{{ c }}" {% if caller_filter == c %}selected{% endif %}>{{ c }}</option>
{% endfor %}
@@ -33,7 +33,7 @@
</div>
<div class="col-auto">
<select name="provider" class="form-select form-select-sm">
<option value="">全部 provider</option>
<option value="">全部供應商</option>
{% for p in ['gcp_ollama','ollama_secondary','ollama_111','gemini','claude','nim','openrouter','nim_via_elephant'] %}
<option value="{{ p }}" {% if provider_filter == p %}selected{% endif %}>{{ p }}</option>
{% endfor %}
@@ -46,21 +46,21 @@
<!-- 總覽 KPI -->
<div class="row g-2 mb-3">
<div class="col-md-2"><div class="card p-2"><small>Total Calls</small><h4>{{ "{:,}".format(summary.total_calls or 0) }}</h4></div></div>
<div class="col-md-2"><div class="card p-2"><small>Tokens</small><h4>{{ "{:,}".format(summary.total_tokens or 0) }}</h4></div></div>
<div class="col-md-2"><div class="card p-2"><small>Cost USD</small><h4>${{ "%.2f"|format(summary.total_cost or 0) }}</h4></div></div>
<div class="col-md-2"><div class="card p-2"><small>Avg Duration</small><h4>{{ summary.avg_duration or 0 }} ms</h4></div></div>
<div class="col-md-2"><div class="card p-2"><small>RAG Hits</small><h4 class="text-success">{{ summary.rag_hits or 0 }}</h4></div></div>
<div class="col-md-2"><div class="card p-2"><small>Errors</small><h4 class="{% if (summary.error_calls or 0) > 0 %}text-danger{% endif %}">{{ summary.error_calls or 0 }}</h4></div></div>
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>呼叫次數</small><h4>{{ "{:,}".format(summary.total_calls or 0) }}</h4></div></div>
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>Token 用量</small><h4>{{ "{:,}".format(summary.total_tokens or 0) }}</h4></div></div>
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>成本 (USD)</small><h4>${{ "%.2f"|format(summary.total_cost or 0) }}</h4></div></div>
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>平均耗時</small><h4>{{ summary.avg_duration or 0 }} ms</h4></div></div>
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>RAG 命中</small><h4 class="text-success">{{ summary.rag_hits or 0 }}</h4></div></div>
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>錯誤次數</small><h4 class="{% if (summary.error_calls or 0) > 0 %}text-danger{% endif %}">{{ summary.error_calls or 0 }}</h4></div></div>
</div>
<!-- by provider -->
<div class="card mb-3">
<div class="card-header"><strong>By Provider</strong></div>
<div class="card-header"><strong>依供應商分組</strong></div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr><th>Provider</th><th class="text-end">Calls</th><th class="text-end">Tokens</th><th class="text-end">Cost USD</th></tr>
<tr><th>供應商</th><th class="text-end">呼叫數</th><th class="text-end">Token 用量</th><th class="text-end">成本 (USD)</th></tr>
</thead>
<tbody>
{% for row in by_provider %}
@@ -78,14 +78,14 @@
<!-- recent calls -->
<div class="card">
<div class="card-header"><strong>Recent Calls (TOP 100)</strong></div>
<div class="card-header"><strong>最近呼叫(最新 100 筆)</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0" style="font-size: 0.85em;">
<thead class="table-light">
<tr>
<th>ID</th><th>Time</th><th>Caller</th><th>Provider</th><th>Model</th>
<th class="text-end">In</th><th class="text-end">Out</th><th class="text-end">ms</th>
<th>Status</th><th class="text-end">$</th><th>Flags</th>
<th>編號</th><th>時間</th><th>呼叫端</th><th>供應商</th><th>模型</th>
<th class="text-end">輸入</th><th class="text-end">輸出</th><th class="text-end">耗時 ms</th>
<th>狀態</th><th class="text-end">成本 $</th><th>標記</th>
</tr>
</thead>
<tbody>
@@ -102,8 +102,8 @@
<td><small>{{ r.status }}</small></td>
<td class="text-end">${{ "%.4f"|format(r.cost) }}</td>
<td>
{% if r.cache_hit %}<span class="badge bg-success">cache</span>{% endif %}
{% if r.rag_hit %}<span class="badge bg-info">rag</span>{% endif %}
{% if r.cache_hit %}<span class="badge bg-success">快取</span>{% endif %}
{% if r.rag_hit %}<span class="badge bg-info">RAG</span>{% endif %}
</td>
</tr>
{% endfor %}
@@ -113,12 +113,7 @@
</div>
<p class="text-muted mt-2"><small>
🤖 Operation Ollama-First v5.0 / Phase 29 — Admin Observability
| <a href="/observability/promotion_review">Promotion Review</a>
| <a href="/observability/quality_trend">Quality Trend</a>
| <a href="/observability/host_health">Host Health</a>
| <a href="/observability/budget">Budget</a>
| <a href="/observability/ppt_audit_history">PPT Audit</a>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 29 — AI 呼叫總覽
</small></p>
</div>
{% endblock %}

View File

@@ -1,29 +1,29 @@
{% extends "base.html" %}
{% block title %}Budget Manager{% endblock %}
{% block title %}預算控管{% 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 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>⚠️</strong> {{ error }}</div>{% endif %}
{% 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 cost_throttle:每小時 cron 檢查當月 spent
線性外推月底成本超 110% → 自動 throttleclaude→gemini fallback
手動編輯 budget 後立即生效(不需 restart)。
依 ADR-028 預算 + Phase 20 成本節流:每小時 cron 檢查當月支出
線性外推月底成本超 110% → 自動節流claude→gemini fallback
手動編輯預算後立即生效(不需重啟)。
</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>
<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>
@@ -49,32 +49,29 @@
</td>
<td>
{% if r.throttled %}
<span class="badge bg-danger">⚠️ THROTTLED</span>
<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">接近上限</span>
<span class="badge bg-warning"><i class="fas fa-exclamation me-1"></i>接近上限</span>
{% else %}
<span class="badge bg-success">正常</span>
<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>
<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>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 29 — 預算控管
</small></p>
</div>
@@ -83,7 +80,7 @@ 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 = '';
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
try {
const r = await fetch(`/observability/budget/update/${id}`, {
method: 'POST',
@@ -95,15 +92,15 @@ async function saveBudget(id) {
});
const d = await r.json();
if (d.ok) {
btn.innerText = '';
setTimeout(() => { btn.innerText = '💾 儲存'; btn.disabled = false; }, 1500);
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.innerText = '💾 儲存';
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.innerText = '💾 儲存';
alert('Error' + e);
btn.disabled = false; btn.innerHTML = '<i class="fas fa-save me-1"></i>儲存';
}
}
</script>

View File

@@ -1,20 +1,20 @@
{% extends "base.html" %}
{% block title %}Host Health Dashboard{% endblock %}
{% block title %}主機健康監控{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<h2 class="mb-3">🏥 Host Health Dashboard
<small class="text-muted">三主機 + MCP + Cost Throttle 即時狀態</small>
<h2 class="mb-3"><i class="fas fa-heartbeat me-2"></i>主機健康監控
<small class="text-muted">三主機 Ollama + MCP + 成本節流即時狀態</small>
</h2>
<!-- Ollama 三主機 -->
<div class="card mb-3">
<div class="card-header"><strong>🤖 Ollama 三主機HTTP /api/tags 即時 probe</strong></div>
<div class="card-header"><strong><i class="fas fa-server me-2"></i>Ollama 三主機HTTP /api/tags 即時 probe</strong></div>
<div class="card-body p-0">
<table class="table mb-0">
<thead class="table-light">
<tr><th>角色</th><th>主機</th><th>HTTP 健康</th><th>unhealthy mark</th><th>已載入模型</th></tr>
<tr><th>角色</th><th>主機</th><th>HTTP 健康</th><th>異常標記</th><th>已載入模型</th></tr>
</thead>
<tbody>
{% for h in ollama_hosts %}
@@ -23,14 +23,14 @@
<td><code>{{ h.host }}</code></td>
<td>
{% if h.healthy %}
<span class="badge bg-success">HTTP OK</span>
<span class="badge bg-success"><i class="fas fa-check me-1"></i>HTTP 正常</span>
{% else %}
<span class="badge bg-danger">❌ DOWN</span>
<span class="badge bg-danger"><i class="fas fa-times me-1"></i>離線</span>
{% endif %}
</td>
<td>
{% if h.unhealthy_mark %}
<span class="badge bg-warning">⚠️ marked unhealthy (30s)</span>
<span class="badge bg-warning"><i class="fas fa-exclamation-triangle me-1"></i>已標記異常30 秒)</span>
{% else %}
<span class="badge bg-light text-dark"></span>
{% endif %}
@@ -50,11 +50,11 @@
<!-- MCP servers -->
<div class="card mb-3">
<div class="card-header"><strong>🔌 MCP ServersPhase 10/10.5</strong></div>
<div class="card-header"><strong><i class="fas fa-plug me-2"></i>MCP 服務Phase 10/10.5</strong></div>
<div class="card-body p-0">
<table class="table mb-0">
<thead class="table-light">
<tr><th>Server</th><th>狀態</th></tr>
<tr><th>服務名稱</th><th>狀態</th></tr>
</thead>
<tbody>
{% for server, healthy in mcp_status.items() %}
@@ -62,14 +62,14 @@
<td><code>{{ server }}</code></td>
<td>
{% if healthy %}
<span class="badge bg-success">✅ healthy</span>
<span class="badge bg-success"><i class="fas fa-check me-1"></i>正常</span>
{% else %}
<span class="badge bg-secondary">— 未啟用 / DOWN</span>
<span class="badge bg-secondary">— 未啟用 / 離線</span>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="2" class="text-muted small">MCP_ROUTER_ENABLED=false 或 mcp-stack 未 deploy</td></tr>
<tr><td colspan="2" class="text-muted small">MCP_ROUTER_ENABLED=false 或 mcp-stack 未部署</td></tr>
{% endfor %}
</tbody>
</table>
@@ -78,12 +78,12 @@
<!-- Cost Throttle 狀態Phase 20 -->
<div class="card mb-3">
<div class="card-header"><strong>💰 Cost Throttle 狀態Phase 20</strong></div>
<div class="card-header"><strong><i class="fas fa-dollar-sign me-2"></i>成本節流狀態Phase 20</strong></div>
<div class="card-body p-0">
{% if throttle_state %}
<table class="table mb-0">
<thead class="table-light">
<tr><th>Provider</th><th>Spent</th><th>Budget</th><th>月底推估</th><th>Ratio</th><th>狀態</th></tr>
<tr><th>供應商</th><th>已花費</th><th>預算</th><th>月底推估</th><th>使用率</th><th>狀態</th></tr>
</thead>
<tbody>
{% for provider, info in throttle_state.items() %}
@@ -95,9 +95,9 @@
<td>{{ "%.0f"|format(info.ratio * 100) }}%</td>
<td>
{% if info.throttled %}
<span class="badge bg-danger">⚠️ THROTTLED</span>
<span class="badge bg-danger"><i class="fas fa-exclamation-triangle me-1"></i>已節流</span>
{% else %}
<span class="badge bg-success">正常</span>
<span class="badge bg-success"><i class="fas fa-check me-1"></i>正常</span>
{% endif %}
</td>
</tr>
@@ -106,19 +106,14 @@
</table>
{% else %}
<p class="text-muted m-3 small">
COST_THROTTLE_ENABLED=false 或尚未首次 evaluate(每小時 cron
COST_THROTTLE_ENABLED=false 或尚未首次評估(每小時 cron 執行
</p>
{% endif %}
</div>
</div>
<p class="text-muted mt-3"><small>
🤖 Operation Ollama-First v5.0 / Phase 29 — Host Health Dashboard
| <a href="/observability/ai_calls">AI Calls</a>
| <a href="/observability/promotion_review">Promotion Review</a>
| <a href="/observability/quality_trend">Quality Trend</a>
| <a href="/observability/budget">Budget</a>
| <a href="/observability/ppt_audit_history">PPT Audit</a>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 29 — 主機健康監控
</small></p>
</div>
{% endblock %}

View File

@@ -1,21 +1,21 @@
{% extends "base.html" %}
{% block title %}PPT Audit History{% endblock %}
{% block title %}PPT 視覺審核歷史{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<h2 class="mb-3">🔍 PPT 視覺審核歷史
<small class="text-muted">reports/ 過去 7 日 .pptx</small>
<h2 class="mb-3"><i class="fas fa-search me-2"></i>PPT 視覺審核歷史
<small class="text-muted">reports/ 目錄過去 7 日 .pptx</small>
</h2>
{% if error %}<div class="alert alert-warning"><strong>⚠️</strong> {{ error }}</div>{% endif %}
{% if error %}<div class="alert alert-warning"><strong><i class="fas fa-exclamation-triangle me-1"></i></strong> {{ error }}</div>{% endif %}
<div class="alert {% if vision_enabled %}alert-success{% else %}alert-secondary{% endif %} small">
<strong>PPT_VISION_ENABLED:</strong>
<strong>PPT_VISION_ENABLED</strong>
{% if vision_enabled %}
已啟用 — daily 22:00 cron 自動跑 minicpm-v 視覺檢查,有 issues 推 Telegram
<i class="fas fa-check me-1"></i>已啟用 — 每日 22:00 cron 自動跑 minicpm-v 視覺檢查,發現問題推送 Telegram
{% else %}
未啟用 — 設 PPT_VISION_ENABLED=true + 188 安裝 LibreOffice 即生效
<i class="fas fa-pause me-1"></i>未啟用 — 設 PPT_VISION_ENABLED=true 並在 188 主機安裝 LibreOffice 即生效
{% endif %}
</div>
@@ -33,7 +33,7 @@
<td class="text-end">{{ f.size_kb }}</td>
<td><small>{{ f.mtime }}</small></td>
<td>
<small class="text-muted">audit cron 22:00 自動</small>
<small class="text-muted">audit cron 22:00 自動執行</small>
</td>
</tr>
{% else %}
@@ -43,16 +43,13 @@
</table>
<p class="text-muted mt-2 small">
審核結果:<strong> issues 才推 Telegram</strong>(避免靜默無問題洗版)。
手動觸發單檔審核需 SSH 188
審核結果:<strong>問題才推 Telegram</strong>(避免靜默無問題洗版)。
手動觸發單檔審核需 SSH 188 主機執行
<code>python3 -c "from services.ppt_vision_service import ppt_vision_service; print(ppt_vision_service.check_ppt_file('reports/xxx.pptx'))"</code>
</p>
<p class="text-muted mt-3"><small>
🤖 Operation Ollama-First v5.0 / Phase 29 — PPT Audit History
| <a href="/observability/ai_calls">AI Calls</a>
| <a href="/observability/host_health">Host Health</a>
| <a href="/observability/budget">Budget</a>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 29 — PPT 視覺審核歷史
</small></p>
</div>
{% endblock %}

View File

@@ -1,34 +1,34 @@
{% extends "base.html" %}
{% block title %}Promotion Review · RAG 自主學習{% endblock %}
{% block title %}RAG 學習晉升審核{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<h2 class="mb-3">🧠 RAG 學習晉升審核
<small class="text-muted">awaiting_review × {{ episodes|length }} 筆</small>
<h2 class="mb-3"><i class="fas fa-brain me-2"></i>RAG 學習晉升審核
<small class="text-muted">待審核 × {{ episodes|length }} 筆</small>
</h2>
{% if error %}
<div class="alert alert-warning"><strong>⚠️</strong> {{ error }}</div>
<div class="alert alert-warning"><strong><i class="fas fa-exclamation-triangle me-1"></i></strong> {{ error }}</div>
{% endif %}
{% if episodes %}
<p class="text-muted small">
⚠️ Phase 11 PromotionGate Stage 4 強制門檻weight >= 0.8 的 episode 必經統帥審核,
24h 無回應自動 expiredweight 降為 0.5 不晉升)。
✅ 通過 → 寫入 ai_insights 供 RAG 檢索;點拒絕 → 永不晉升learning_episodes 留存)。
<i class="fas fa-info-circle me-1"></i>Phase 11 PromotionGate Stage 4 強制門檻weight 0.8 的 episode 必經統帥審核,
24 小時無回應自動過期weight 降為 0.5 不晉升)。
「通過晉升」→ 寫入 ai_insights 供 RAG 檢索;點拒絕→ 永不晉升learning_episodes 留存)。
</p>
{% for ep in episodes %}
<div class="card mb-3 episode-card" data-episode-id="{{ ep.id }}">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong>Episode #{{ ep.id }}</strong>
<strong>學習片段 #{{ ep.id }}</strong>
<span class="badge bg-secondary ms-2">{{ ep.episode_type }}</span>
{% if ep.source_table %}<span class="badge bg-light text-dark ms-1">
{{ ep.source_table }}#{{ ep.source_id }}</span>{% endif %}
<span class="badge bg-info ms-1">weight: {{ "%.2f"|format(ep.weight) }}</span>
<span class="badge bg-info ms-1">quality: {{ "%.2f"|format(ep.quality_score) }}</span>
<span class="badge bg-info ms-1">權重:{{ "%.2f"|format(ep.weight) }}</span>
<span class="badge bg-info ms-1">品質:{{ "%.2f"|format(ep.quality_score) }}</span>
</div>
<small class="text-muted">{{ ep.created_at }}</small>
</div>
@@ -37,34 +37,29 @@
</div>
<div class="card-footer text-end">
<button class="btn btn-success btn-sm me-2" onclick="approveEpisode({{ ep.id }}, this)">
通過晉升
<i class="fas fa-check me-1"></i>通過晉升
</button>
<button class="btn btn-outline-danger btn-sm" onclick="rejectEpisode({{ ep.id }}, this)">
拒絕
<i class="fas fa-times me-1"></i>拒絕
</button>
</div>
</div>
{% endfor %}
{% else %}
<div class="alert alert-info">
✨ 目前無 awaiting_review episodes
<small>RAG 未啟用 / 無高 weight episode / 全部已 24h 過期)</small>
<i class="fas fa-sparkles me-1"></i>目前無待審核片段
<small>RAG 未啟用 / 無高權重片段 / 全部已 24 小時過期)</small>
</div>
{% endif %}
<p class="text-muted mt-3"><small>
🤖 Operation Ollama-First v5.0 / Phase 29 — PromotionGate Web 審核
| <a href="/observability/ai_calls">AI Calls</a>
| <a href="/observability/quality_trend">Quality Trend</a>
| <a href="/observability/host_health">Host Health</a>
| <a href="/observability/budget">Budget</a>
| <a href="/observability/ppt_audit_history">PPT Audit</a>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 29 — RAG 學習晉升審核
</small></p>
</div>
<script>
async function approveEpisode(id, btn) {
btn.disabled = true; btn.innerText = ' 處理中...';
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 處理中...';
try {
const r = await fetch(`/observability/promotion_review/approve/${id}`, {method: 'POST'});
const d = await r.json();
@@ -72,20 +67,20 @@ async function approveEpisode(id, btn) {
const card = document.querySelector(`.episode-card[data-episode-id="${id}"]`);
card.classList.add('border-success');
card.querySelector('.card-footer').innerHTML =
`<span class="text-success">已晉升 → ai_insights #${d.insight_id} (approver=${d.approver})</span>`;
`<span class="text-success"><i class="fas fa-check me-1"></i>已晉升 → ai_insights #${d.insight_id}(審核者:${d.approver}</span>`;
} else {
alert('晉升失敗: ' + (d.error || 'unknown'));
btn.disabled = false; btn.innerText = '通過晉升';
alert('晉升失敗' + (d.error || 'unknown'));
btn.disabled = false; btn.innerHTML = '<i class="fas fa-check me-1"></i>通過晉升';
}
} catch (e) {
alert('Error: ' + e);
btn.disabled = false; btn.innerText = '通過晉升';
alert('Error' + e);
btn.disabled = false; btn.innerHTML = '<i class="fas fa-check me-1"></i>通過晉升';
}
}
async function rejectEpisode(id, btn) {
if (!confirm(`拒絕 Episode #${id}?此筆將永不晉升(保留在 learning_episodes 不刪除)`)) return;
btn.disabled = true; btn.innerText = ' 處理中...';
if (!confirm(`拒絕學習片段 #${id}?此筆將永不晉升(保留在 learning_episodes 不刪除)`)) return;
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 處理中...';
try {
const r = await fetch(`/observability/promotion_review/reject/${id}`, {method: 'POST'});
const d = await r.json();
@@ -93,14 +88,14 @@ async function rejectEpisode(id, btn) {
const card = document.querySelector(`.episode-card[data-episode-id="${id}"]`);
card.classList.add('border-danger');
card.querySelector('.card-footer').innerHTML =
`<span class="text-danger">已拒絕 (rejected_human)</span>`;
`<span class="text-danger"><i class="fas fa-times me-1"></i>已拒絕rejected_human</span>`;
} else {
alert('拒絕失敗: ' + (d.error || 'unknown'));
btn.disabled = false; btn.innerText = '拒絕';
alert('拒絕失敗' + (d.error || 'unknown'));
btn.disabled = false; btn.innerHTML = '<i class="fas fa-times me-1"></i>拒絕';
}
} catch (e) {
alert('Error: ' + e);
btn.disabled = false; btn.innerText = '拒絕';
alert('Error' + e);
btn.disabled = false; btn.innerHTML = '<i class="fas fa-times me-1"></i>拒絕';
}
}
</script>

View File

@@ -1,15 +1,15 @@
{% extends "base.html" %}
{% block title %}Caller Quality Trend{% endblock %}
{% block title %}Caller 反饋趨勢{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<h2 class="mb-3">💬 Caller 反饋趨勢
<h2 class="mb-3"><i class="fas fa-comments me-2"></i>Caller 反饋趨勢
<small class="text-muted">過去 {{ days }} 日</small>
</h2>
{% if error %}
<div class="alert alert-warning"><strong>⚠️</strong> {{ error }}</div>
<div class="alert alert-warning"><strong><i class="fas fa-exclamation-triangle me-1"></i></strong> {{ error }}</div>
{% endif %}
<form method="get" class="row g-2 mb-3">
@@ -25,13 +25,13 @@
{% if recommendations %}
<div class="card mb-3">
<div class="card-header bg-warning"><strong>🔮 智能建議</strong></div>
<div class="card-header bg-warning"><strong><i class="fas fa-lightbulb me-2"></i>智能建議</strong></div>
<div class="card-body">
<ul class="mb-0">
{% for rec in recommendations %}
<li>
{% if rec.action == 'review' %}⚠️{% else %}✅{% endif %}
<code>{{ rec.caller }}</code>: {{ rec.reason }}
{% if rec.action == 'review' %}<i class="fas fa-exclamation-triangle text-warning me-1"></i>{% else %}<i class="fas fa-check text-success me-1"></i>{% endif %}
<code>{{ rec.caller }}</code>{{ rec.reason }}
</li>
{% endfor %}
</ul>
@@ -40,16 +40,17 @@
{% endif %}
<div class="card">
<div class="card-header"><strong>Caller × 反饋分佈</strong>
<small class="text-muted">avg_score 升序,最差先看)</small>
<div class="card-header"><strong>呼叫端 × 反饋分佈</strong>
<small class="text-muted">平均分數升序排列,最差先看)</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr>
<th>Caller</th><th class="text-end">Avg</th>
<th class="text-end">👍</th><th class="text-end">👎</th>
<th class="text-end">N</th><th>Trend</th><th>Bar</th>
<th>呼叫端</th><th class="text-end">平均</th>
<th class="text-end"><i class="fas fa-thumbs-up text-success"></i></th>
<th class="text-end"><i class="fas fa-thumbs-down text-danger"></i></th>
<th class="text-end">總數</th><th>趨勢</th><th>分布</th>
</tr>
</thead>
<tbody>
@@ -64,13 +65,13 @@
<td class="text-end">{{ info.total_feedback }}</td>
<td>
{% if info.trend == 'positive' %}
<span class="badge bg-success">✅ Positive</span>
<span class="badge bg-success"><i class="fas fa-arrow-up me-1"></i>正向</span>
{% elif info.trend == 'negative' %}
<span class="badge bg-danger">⚠️ Negative</span>
<span class="badge bg-danger"><i class="fas fa-arrow-down me-1"></i>負向</span>
{% elif info.trend == 'neutral' %}
<span class="badge bg-secondary"> Neutral</span>
<span class="badge bg-secondary"><i class="fas fa-minus me-1"></i>中性</span>
{% else %}
<span class="badge bg-light text-dark">❓ No Data</span>
<span class="badge bg-light text-dark"><i class="fas fa-question me-1"></i>無資料</span>
{% endif %}
</td>
<td style="width: 200px;">
@@ -93,12 +94,7 @@
</div>
<p class="text-muted mt-3"><small>
🤖 Operation Ollama-First v5.0 / Phase 29 — Caller Quality Trend
| <a href="/observability/ai_calls">AI Calls</a>
| <a href="/observability/promotion_review">Promotion Review</a>
| <a href="/observability/host_health">Host Health</a>
| <a href="/observability/budget">Budget</a>
| <a href="/observability/ppt_audit_history">PPT Audit</a>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 29 — Caller 反饋趨勢
</small></p>
</div>
{% endblock %}

View File

@@ -110,6 +110,49 @@
</ul>
</li>
<!-- AI 觀測台 -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if active_page in ['obs_ai_calls', 'obs_host_health', 'obs_budget', 'obs_promotion_review', 'obs_quality_trend', 'obs_ppt_audit'] %}active{% endif %}"
href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-satellite-dish me-1"></i>AI 觀測台
</a>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">AI 監控</h6></li>
<li>
<a class="dropdown-item" href="/observability/ai_calls">
<i class="fas fa-chart-bar me-2"></i>AI 呼叫總覽
</a>
</li>
<li>
<a class="dropdown-item" href="/observability/host_health">
<i class="fas fa-heartbeat me-2"></i>主機健康監控
</a>
</li>
<li>
<a class="dropdown-item" href="/observability/budget">
<i class="fas fa-wallet me-2"></i>預算控管
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">AI 學習與品質</h6></li>
<li>
<a class="dropdown-item" href="/observability/promotion_review">
<i class="fas fa-brain me-2"></i>RAG 學習晉升審核
</a>
</li>
<li>
<a class="dropdown-item" href="/observability/quality_trend">
<i class="fas fa-comments me-2"></i>Caller 反饋趨勢
</a>
</li>
<li>
<a class="dropdown-item" href="/observability/ppt_audit_history">
<i class="fas fa-search me-2"></i>PPT 視覺審核歷史
</a>
</li>
</ul>
</li>
<!-- 雲端匯入 -->
<li class="nav-item">
<a class="nav-link {% if active_page == 'auto_import' %}active{% endif %}" href="/auto_import">