Files
ewoooc/templates/ai_automation_smoke.html
OoO d5f4fd7198
All checks were successful
CD Pipeline / deploy (push) Successful in 1m15s
加入 AI Smoke 每日摘要推播
2026-04-29 23:57:36 +08:00

315 lines
12 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 'base.html' %}
{% block title %}AI 自動化 Smoke Dashboard - WOOO TECH{% endblock %}
{% block extra_css %}
<style>
.smoke-hero {
background: radial-gradient(circle at top left, rgba(42, 82, 152, 0.24), transparent 36%),
linear-gradient(135deg, #10243f 0%, #1e3c72 48%, #2a5298 100%);
color: #fff;
border-radius: 22px;
padding: 28px;
box-shadow: 0 18px 40px rgba(16, 36, 63, 0.22);
overflow: hidden;
position: relative;
}
.smoke-hero::after {
content: "";
position: absolute;
width: 280px;
height: 280px;
right: -90px;
top: -90px;
border-radius: 999px;
border: 42px solid rgba(255, 255, 255, 0.08);
}
.smoke-pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 13px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
backdrop-filter: blur(10px);
font-size: 13px;
letter-spacing: 0.04em;
}
.smoke-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 18px;
}
.smoke-card {
border: 0;
border-radius: 18px;
box-shadow: 0 10px 28px rgba(30, 60, 114, 0.12);
overflow: hidden;
transition: transform .22s ease, box-shadow .22s ease;
}
.smoke-card:hover {
transform: translateY(-4px);
box-shadow: 0 16px 36px rgba(30, 60, 114, 0.18);
}
.status-badge {
border-radius: 999px;
padding: 6px 11px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
}
.status-ok { background: #dcfce7; color: #166534; }
.status-warning { background: #fef3c7; color: #92400e; }
.status-critical { background: #fee2e2; color: #991b1b; }
.detail-box {
background: #f8fafc;
border-radius: 12px;
padding: 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
color: #334155;
max-height: 160px;
overflow: auto;
white-space: pre-wrap;
}
.trend-strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14px, 1fr));
gap: 6px;
align-items: end;
}
.trend-dot {
height: 34px;
border-radius: 8px;
opacity: .92;
}
.trend-dot.status-ok { background: #22c55e; }
.trend-dot.status-warning { background: #f59e0b; }
.trend-dot.status-critical { background: #ef4444; }
</style>
{% endblock %}
{% block content %}
<div class="smoke-hero mb-4">
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3 position-relative" style="z-index: 1;">
<div>
<span class="smoke-pill mb-3"><i class="fas fa-robot"></i> FOUR-AGENT CONTROL PLANE</span>
<h1 class="fw-bold mb-2">AI 自動化 Smoke Dashboard</h1>
<p class="mb-0 text-white-50">快速確認 EventRouter、AutoHeal、NemoTron、OpenClaw 與 ElephantAlpha 的閉環狀態。</p>
</div>
<div class="text-lg-end">
<div id="overallStatus" class="h4 fw-bold mb-2">讀取中...</div>
<div class="text-white-50">版本 {{ system_version }}</div>
<button id="refreshBtn" class="btn btn-light mt-3">
<i class="fas fa-sync-alt me-2"></i>重新檢查
</button>
<a class="btn btn-outline-light mt-3 ms-lg-2" href="/api/ai-automation/smoke/history/export">
<i class="fas fa-file-export me-2"></i>匯出 JSONL
</a>
<button id="clearHistoryBtn" class="btn btn-outline-warning mt-3 ms-lg-2">
<i class="fas fa-broom me-2"></i>清理趨勢
</button>
<button id="sendSummaryBtn" class="btn btn-outline-info mt-3 ms-lg-2">
<i class="fas fa-paper-plane me-2"></i>推播摘要
</button>
</div>
</div>
</div>
<div class="row g-3 mb-4" id="summaryCards">
<div class="col-md-3"><div class="card smoke-card"><div class="card-body"><div class="text-muted small">OK</div><div class="h3 mb-0" id="okCount">-</div></div></div></div>
<div class="col-md-3"><div class="card smoke-card"><div class="card-body"><div class="text-muted small">Warning</div><div class="h3 mb-0" id="warningCount">-</div></div></div></div>
<div class="col-md-3"><div class="card smoke-card"><div class="card-body"><div class="text-muted small">Critical</div><div class="h3 mb-0" id="criticalCount">-</div></div></div></div>
<div class="col-md-3"><div class="card smoke-card"><div class="card-body"><div class="text-muted small">Generated</div><div class="fs-6 fw-semibold" id="generatedAt">-</div></div></div></div>
</div>
<div class="card smoke-card mb-4">
<div class="card-body">
<div class="d-flex flex-column flex-md-row justify-content-between gap-2 mb-3">
<div>
<h5 class="mb-1">最近 Smoke 趨勢</h5>
<div class="text-muted small">每次 API 快檢會保存一筆精簡紀錄,保留最近 200 筆。</div>
</div>
<div class="text-muted small" id="historySummary">等待資料...</div>
</div>
<div class="trend-strip" id="trendStrip"></div>
</div>
</div>
<div class="card smoke-card mb-4">
<div class="card-body">
<div class="d-flex flex-column flex-md-row justify-content-between gap-2 mb-3">
<div>
<h5 class="mb-1">每日摘要</h5>
<div class="text-muted small">依最近保存紀錄彙整,方便快速觀察是否有連續 warning / critical。</div>
</div>
</div>
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead>
<tr>
<th>日期</th>
<th>OK</th>
<th>Warning</th>
<th>Critical</th>
<th>Total</th>
</tr>
</thead>
<tbody id="dailySummaryRows">
<tr><td colspan="5" class="text-muted">等待資料...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="smoke-grid" id="checkGrid"></div>
{% endblock %}
{% block extra_js %}
<script>
function badge(status) {
const label = {ok: 'OK', warning: 'WARNING', critical: 'CRITICAL'}[status] || status;
return `<span class="status-badge status-${status}">${label}</span>`;
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
}
function renderSmoke(data) {
document.getElementById('overallStatus').innerHTML = badge(data.status);
document.getElementById('okCount').textContent = data.summary.ok ?? 0;
document.getElementById('warningCount').textContent = data.summary.warning ?? 0;
document.getElementById('criticalCount').textContent = data.summary.critical ?? 0;
document.getElementById('generatedAt').textContent = data.generated_at || '-';
renderTrend(data.history || {});
const grid = document.getElementById('checkGrid');
grid.innerHTML = data.checks.map(item => `
<div class="card smoke-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start gap-2 mb-3">
<h5 class="mb-0">${escapeHtml(item.name)}</h5>
${badge(item.status)}
</div>
<p class="text-muted">${escapeHtml(item.summary)}</p>
<div class="detail-box">${escapeHtml(JSON.stringify(item.details || {}, null, 2))}</div>
</div>
</div>
`).join('');
}
function renderTrend(history) {
const recent = history.recent || [];
const counts = history.counts || {ok: 0, warning: 0, critical: 0};
document.getElementById('historySummary').textContent =
`OK ${counts.ok || 0} / Warning ${counts.warning || 0} / Critical ${counts.critical || 0}`;
const strip = document.getElementById('trendStrip');
if (!recent.length) {
strip.innerHTML = '<div class="text-muted small">尚無歷史紀錄。</div>';
return;
}
strip.innerHTML = recent.map(item => `
<div class="trend-dot status-${item.status || 'critical'}"
title="${escapeHtml((item.generated_at || '-') + ' · ' + (item.status || 'unknown'))}">
</div>
`).join('');
renderDailySummary(history.daily || []);
}
function renderDailySummary(rows) {
const body = document.getElementById('dailySummaryRows');
if (!rows.length) {
body.innerHTML = '<tr><td colspan="5" class="text-muted">尚無每日摘要。</td></tr>';
return;
}
body.innerHTML = rows.slice().reverse().map(row => `
<tr>
<td>${escapeHtml(row.date)}</td>
<td class="text-success fw-semibold">${row.ok || 0}</td>
<td class="text-warning fw-semibold">${row.warning || 0}</td>
<td class="text-danger fw-semibold">${row.critical || 0}</td>
<td>${row.total || 0}</td>
</tr>
`).join('');
}
async function loadSmoke() {
const btn = document.getElementById('refreshBtn');
btn.disabled = true;
btn.querySelector('i').classList.add('fa-spin');
try {
const res = await fetchWithCSRF('/api/ai-automation/smoke');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
renderSmoke(await res.json());
} catch (err) {
document.getElementById('overallStatus').innerHTML = badge('critical');
document.getElementById('checkGrid').innerHTML = `
<div class="card smoke-card">
<div class="card-body">
<h5>Smoke API</h5>
<p class="text-danger">讀取失敗:${escapeHtml(err.message)}</p>
</div>
</div>`;
} finally {
btn.disabled = false;
btn.querySelector('i').classList.remove('fa-spin');
}
}
document.getElementById('refreshBtn').addEventListener('click', loadSmoke);
document.getElementById('clearHistoryBtn').addEventListener('click', async () => {
if (!confirm('確定要清理 AI Smoke 趨勢紀錄?這只會刪除本頁 JSONL history不會影響 DB 或事件資料。')) {
return;
}
const btn = document.getElementById('clearHistoryBtn');
btn.disabled = true;
try {
const res = await fetchWithCSRF('/api/ai-automation/smoke/history/clear', {method: 'POST'});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
showToast(`已清理 ${data.cleared || 0} 筆 Smoke 趨勢紀錄`, 'success');
await loadSmoke();
} catch (err) {
showToast(`清理失敗:${err.message}`, 'error');
} finally {
btn.disabled = false;
}
});
document.getElementById('sendSummaryBtn').addEventListener('click', async () => {
const btn = document.getElementById('sendSummaryBtn');
btn.disabled = true;
try {
const res = await fetchWithCSRF('/api/ai-automation/smoke/daily-summary/send', {method: 'POST'});
const data = await res.json();
if (!res.ok) throw new Error((data.telegram?.errors || []).join(', ') || `HTTP ${res.status}`);
showToast(`AI Smoke 每日摘要已推播sent=${data.telegram?.sent || 0}`, 'success');
} catch (err) {
showToast(`推播失敗:${err.message}`, 'error');
} finally {
btn.disabled = false;
}
});
loadSmoke();
</script>
{% endblock %}