feat(p38): admin 觀測台 6 頁完整繁中化 + 加入導航選單
All checks were successful
CD Pipeline / deploy (push) Successful in 2m42s
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:
@@ -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,
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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% → 自動 throttle(claude→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>
|
||||
|
||||
@@ -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 Servers(Phase 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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 無回應自動 expired(weight 降為 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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user