Files
ewoooc/templates/admin/ai_calls_dashboard.html
OoO 87d460e243
All checks were successful
CD Pipeline / deploy (push) Successful in 2m40s
feat(p50): chart.js 折線圖視覺化 + Playbook 一鍵啟用/停用
統帥要求「視覺方格 UI/UX」:raw 表格不夠,加 chart.js 雙圖 + L2 管理。

N-1: ai_calls hourly trend chart.js(雙軸混合)
- 取代原 progress bar 表格
- 折線:呼叫數(藍)+ 錯誤次數(紅)→ 共用左軸
- 柱狀:成本 USD(黃)→ 右軸
- interaction mode index:滑鼠 hover 同時顯示三個指標
- chart.js 4.4.1 CDN 加在 {% block extra_js %}

N-2: budget 30d cost trend stacked bar chart
- 取代原 30d cost trend 表格(max-height 滾動 → 一目瞭然圖)
- 8 個 provider 各自分色
  本地 Ollama(綠系)vs 付費(橘/紫/青系)
- stacked bar:每日總成本一柱,依 provider 堆疊
- tooltip 顯示每個 provider $X.XXXX

N-3: Playbook 一鍵啟用/停用(L2 補強第 7 個)
- 新 POST /observability/playbooks/toggle/<id>
  翻轉 is_active + UPDATE updated_at
- host_health.html playbook 排行表加「切換」欄
- 動態按鈕:啟用顯示「停用」、停用顯示「啟用」
- 對應觀測台直接管理 AutoHeal 庫,不需 SSH 改 DB

L2 一鍵自動化從 6 個 → 7 個入口:
- AutoHeal / AiderHeal / Code Review / Force Throttle(既有)
- Telegram Heal / Throttle(既有)
- Playbook Toggle(Phase 50 新增)

Phase 38→50 累計 15 commits。
觀測台從 raw stats → AI 自動化專業舞台 → 視覺方格 UI 終局。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 20:04:13 +08:00

315 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "ewoooc_base.html" %}
{% block title %}AI 呼叫總覽{% endblock %}
{% block ewooo_content %}
<div class="container-fluid mt-3">
<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><i class="fas fa-exclamation-triangle me-1"></i></strong> {{ error }}</div>
{% endif %}
<!-- Phase 40 D-7: 一鍵觸發 Code Review (L2 自動化) -->
<div class="mb-3">
<button class="btn btn-warning btn-sm" onclick="triggerCodeReview()">
<i class="fas fa-microscope me-1"></i>觸發 Code Review Pipeline (5 step)
</button>
<small class="text-muted ms-2">
看到錯誤率飆高?一鍵觸發 Hermes→OpenClaw→EA→NemoTron 5 步審查最新 commit
在背景執行,完成後 Telegram 通知。
</small>
</div>
<!-- 篩選 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="">全部呼叫端</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="">全部供應商</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-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>
<!-- Phase 39 D-3: caller × RAG × MCP 編排矩陣 -->
{% if caller_richness %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-network-wired me-2"></i>呼叫端 × RAG × MCP 編排矩陣</strong>
<small class="text-muted">資料來源ai_calls × mcp_calls × rag_query_log{{ hours }}h 內呼叫 ≥ 5 次的 caller</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr>
<th>呼叫端</th>
<th class="text-end">總呼叫</th>
<th class="text-end">RAG 命中率</th>
<th class="text-end">MCP 編排率</th>
<th class="text-end">RAG 反饋分數</th>
<th class="text-end">反饋筆數</th>
</tr>
</thead>
<tbody>
{% for c in caller_richness %}
<tr>
<td><code>{{ c.caller }}</code></td>
<td class="text-end">{{ "{:,}".format(c.total_calls) }}</td>
<td class="text-end">
<strong class="{% if c.rag_hit_rate >= 50 %}text-success{% elif c.rag_hit_rate >= 20 %}text-warning{% else %}text-muted{% endif %}">
{{ "%.1f"|format(c.rag_hit_rate) }}%
</strong>
<small class="text-muted">({{ c.rag_hits }})</small>
</td>
<td class="text-end">
<strong class="{% if c.mcp_rate >= 30 %}text-info{% elif c.mcp_rate >= 10 %}text-warning{% endif %}">
{{ "%.1f"|format(c.mcp_rate) }}%
</strong>
<small class="text-muted">({{ c.mcp_orchestrated }})</small>
</td>
<td class="text-end">
{% if c.feedback_count > 0 %}
<strong class="{% if c.avg_rag_feedback >= 4 %}text-success{% elif c.avg_rag_feedback >= 3 %}text-warning{% else %}text-danger{% endif %}">
{{ "%.2f"|format(c.avg_rag_feedback) }}/5
</strong>
{% else %}
<small class="text-muted"></small>
{% endif %}
</td>
<td class="text-end">{{ c.feedback_count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card-footer small text-muted">
<i class="fas fa-info-circle me-1"></i>
<strong>RAG 命中率</strong>caller 在此期間有多少呼叫吃到 RAG 召回;
<strong>MCP 編排率</strong>:有多少呼叫透過 request_id 串接到 MCP tool
<strong>反饋分數</strong>RAG 召回後人工反饋1-5
</div>
</div>
{% endif %}
<!-- Phase 47 K-2 + Phase 50 N-1: 24h 每小時呼叫趨勢chart.js-->
{% if hourly_trend %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-chart-area me-2"></i>過去 24h 每小時呼叫趨勢</strong>
<small class="text-muted">折線:呼叫數 + 錯誤;柱狀:成本 USD</small>
</div>
<div class="card-body">
<canvas id="hourlyTrendChart" height="80"></canvas>
</div>
</div>
{% endif %}
<!-- Phase 47 K-2: by model 細分 -->
{% if by_model %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-microchip me-2"></i>依模型細分</strong>
<small class="text-muted">資料來源ai_calls.model細到模型版本— Top 15</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr>
<th>模型</th><th>供應商</th>
<th class="text-end">呼叫</th><th class="text-end">Token</th>
<th class="text-end">成本 USD</th><th class="text-end">平均耗時</th>
<th class="text-end">錯誤</th>
</tr>
</thead>
<tbody>
{% for m in by_model %}
<tr>
<td><code>{{ m.model[:35] }}</code></td>
<td><span class="badge bg-secondary">{{ m.provider }}</span></td>
<td class="text-end">{{ "{:,}".format(m.calls) }}</td>
<td class="text-end">{{ "{:,}".format(m.tokens) }}</td>
<td class="text-end">${{ "%.4f"|format(m.cost) }}</td>
<td class="text-end">{{ m.avg_ms }} ms</td>
<td class="text-end">
{% if m.errors > 0 %}<span class="text-danger">{{ m.errors }}</span>
{% else %}<small class="text-muted">0</small>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Phase 47 K-2: agent_context 最近 10 筆 -->
{% if recent_contexts %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-comments-dollar me-2"></i>Agent 對話上下文(最近 10 筆)</strong>
<small class="text-muted">資料來源agent_contextHermes/OpenClaw/NemoTron 工作 session</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.85em;">
<thead class="table-light">
<tr>
<th>時間</th><th>Agent</th><th>Key</th>
<th class="text-end">TTL min</th><th>預覽</th>
</tr>
</thead>
<tbody>
{% for c in recent_contexts %}
<tr>
<td><small>{{ c.created_at }}</small></td>
<td><span class="badge bg-info text-dark">{{ c.agent_name }}</span></td>
<td><code>{{ c.context_key }}</code></td>
<td class="text-end">{{ c.ttl_minutes }}</td>
<td><small class="text-muted">{{ c.preview }}{% if c.preview|length >= 120 %}…{% endif %}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- by provider -->
<div class="card mb-3">
<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>供應商</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 %}
<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>最近呼叫(最新 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>編號</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>
{% 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">快取</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>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 40 — AI 呼叫總覽(含 RAG/MCP 編排矩陣)
</small></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
// Phase 50 N-1: hourly trend chart
(function() {
const labels = {{ hourly_trend | map(attribute='hour') | list | tojson }};
const calls = {{ hourly_trend | map(attribute='calls') | list | tojson }};
const costs = {{ hourly_trend | map(attribute='cost') | list | tojson }};
const errors = {{ hourly_trend | map(attribute='errors') | list | tojson }};
const el = document.getElementById('hourlyTrendChart');
if (!el || !labels.length) return;
new Chart(el, {
data: {
labels: labels,
datasets: [
{ type: 'line', label: '呼叫數', data: calls, borderColor: '#0d6efd', backgroundColor: 'rgba(13,110,253,0.1)', tension: 0.3, fill: true, yAxisID: 'y' },
{ type: 'line', label: '錯誤次數', data: errors, borderColor: '#dc3545', backgroundColor: 'rgba(220,53,69,0.1)', tension: 0.3, yAxisID: 'y' },
{ type: 'bar', label: '成本 USD', data: costs, backgroundColor: 'rgba(255,193,7,0.5)', borderColor: '#ffc107', yAxisID: 'y1' },
]
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
scales: {
y: { type: 'linear', position: 'left', beginAtZero: true, title: { display: true, text: '次數' } },
y1: { type: 'linear', position: 'right', beginAtZero: true, grid: { drawOnChartArea: false }, title: { display: true, text: 'USD' } },
}
}
});
})();
async function triggerCodeReview() {
if (!confirm('觸發 Code Review Pipeline\n\n會對最新 commit 跑 5 step 審查Hermes 掃描 → OpenClaw 摘要 → EA 決策 → NemoTron 行動),背景執行。')) return;
try {
const r = await fetch('/observability/ai_calls/trigger_code_review', {method: 'POST'});
const d = await r.json();
if (d.ok) {
alert(`${d.message}\n\nPipeline ID: ${d.pipeline_id}\nCommit: ${d.commit_sha}\n變更檔案: ${d.changed_files_count}`);
} else {
alert('❌ ' + (d.error || '觸發失敗'));
}
} catch (e) {
alert('Error: ' + e);
}
}
</script>
{% endblock %}