All checks were successful
CD Pipeline / deploy (push) Successful in 2m25s
Operation Ollama-First v5.0 / Phase 27 + 28 — 戰役觀測前端化
routes/admin_observability_routes.py (新檔, 200+ 行)
- admin_observability_bp blueprint,url_prefix='/admin'
- /admin/ai_calls — Phase 27 主入口(KPI / by provider / TOP 100)
- /admin/promotion_review — Phase 28 PromotionGate 待審列表 + 通過/拒絕按鈕
- /admin/quality_trend — Phase 25 caller 反饋趨勢視覺化
- /admin/host_health — 三主機 + MCP + cost throttle 即時健康
- 失敗安全:DB 查詢失敗回空清單 + 警告 banner(不 raise)
- promotion_review_approve/reject 走 hash_human_approver SHA1[:8] 不存原 username
templates/admin/ (4 個新檔)
- ai_calls_dashboard.html 篩選 bar + 6 KPI cards + by provider + recent 100
- promotion_review.html 卡片列表 + 通過/拒絕 AJAX 按鈕(即時 UI feedback)
- quality_trend.html avg score 升序排列 + 進度條 bar + 智能建議區
- host_health.html 三主機 HTTP probe + 已載入模型 + MCP + throttle
統帥提問「需要哪些前端讓兩者互補互動」答覆:
6 項最該前端化(已實作 4 項,剩 2 項為後續):
✅ ai_calls 即時查詢 → /admin/ai_calls
✅ PromotionGate 待審核 → /admin/promotion_review (互動最強)
✅ caller 反饋趨勢 → /admin/quality_trend
✅ 三主機 + MCP + throttle → /admin/host_health
⏳ ai_call_budgets 預算管理 → Phase 29 補
⏳ PPT 視覺審核結果列表 → Phase 29 補
互補 Telegram 哲學:
Telegram = push(重要事件主動通知)
Web = pull(統帥隨時可查 / 互動審核 / 找問題)
PromotionGate Stage 4:Telegram 推 awaiting_review + Web 批次審核(兩者皆可)
app.py blueprint 註冊 + CSRF exempt(AJAX POST 走 server-side check)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
5.1 KiB
HTML
123 lines
5.1 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}AI Calls Dashboard{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid mt-3">
|
|
<h2 class="mb-3">📊 AI Calls Dashboard
|
|
<small class="text-muted">過去 {{ hours }} 小時</small>
|
|
</h2>
|
|
|
|
{% if error %}
|
|
<div class="alert alert-warning"><strong>⚠️</strong> {{ error }}</div>
|
|
{% endif %}
|
|
|
|
<!-- 篩選 bar -->
|
|
<form method="get" class="row g-2 mb-3">
|
|
<div class="col-auto">
|
|
<select name="hours" class="form-select form-select-sm">
|
|
{% for h in [1, 6, 24, 72, 168] %}
|
|
<option value="{{ h }}" {% if hours == h %}selected{% endif %}>
|
|
{% if h < 24 %}{{ h }} 小時{% else %}{{ (h//24) }} 天{% endif %}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-auto">
|
|
<select name="caller" class="form-select form-select-sm">
|
|
<option value="">全部 caller</option>
|
|
{% for c in callers %}
|
|
<option value="{{ c }}" {% if caller_filter == c %}selected{% endif %}>{{ c }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-auto">
|
|
<select name="provider" class="form-select form-select-sm">
|
|
<option value="">全部 provider</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 %}
|
|
</select>
|
|
</div>
|
|
<div class="col-auto">
|
|
<button class="btn btn-primary btn-sm">篩選</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- 總覽 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>
|
|
|
|
<!-- by provider -->
|
|
<div class="card mb-3">
|
|
<div class="card-header"><strong>By Provider</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>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in by_provider %}
|
|
<tr>
|
|
<td><code>{{ row.provider }}</code></td>
|
|
<td class="text-end">{{ "{:,}".format(row.calls) }}</td>
|
|
<td class="text-end">{{ "{:,}".format(row.tokens) }}</td>
|
|
<td class="text-end">${{ "%.2f"|format(row.cost) }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- recent calls -->
|
|
<div class="card">
|
|
<div class="card-header"><strong>Recent Calls (TOP 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>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for r in recent %}
|
|
<tr {% if r.status not in ['ok','cache_only'] %}class="table-warning"{% endif %}>
|
|
<td>{{ r.id }}</td>
|
|
<td><small>{{ r.called_at }}</small></td>
|
|
<td><code>{{ r.caller }}</code></td>
|
|
<td><small>{{ r.provider }}</small></td>
|
|
<td><small>{{ r.model[:25] }}</small></td>
|
|
<td class="text-end">{{ r.in_tokens }}</td>
|
|
<td class="text-end">{{ r.out_tokens }}</td>
|
|
<td class="text-end">{{ r.duration_ms }}</td>
|
|
<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 %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="text-muted mt-2"><small>
|
|
🤖 Operation Ollama-First v5.0 / Phase 27 — Admin Observability
|
|
| <a href="/admin/promotion_review">Promotion Review</a>
|
|
| <a href="/admin/quality_trend">Quality Trend</a>
|
|
| <a href="/admin/host_health">Host Health</a>
|
|
</small></p>
|
|
</div>
|
|
{% endblock %}
|