Files
ewoooc/templates/ai_automation_smoke.html
OoO 869cf2da31
All checks were successful
CD Pipeline / deploy (push) Successful in 59s
統一工具頁新版殼層
2026-05-14 00:57:29 +08:00

338 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 'ewoooc_base.html' %}
{% block title %}AI 自動化 Smoke Dashboard - EwoooC{% endblock %}
{% block extra_css %}
<style>
.smoke-hero {
background: var(--momo-dot-grid), var(--momo-bg-surface);
color: var(--momo-text-primary);
border: 1px solid var(--momo-border-strong);
border-radius: var(--momo-radius-md);
padding: 28px;
box-shadow: var(--momo-shadow-soft);
overflow: hidden;
position: relative;
}
.smoke-hero::after {
content: "";
position: absolute;
width: 280px;
height: 280px;
right: -90px;
top: -90px;
border: 42px solid rgba(166, 103, 45, 0.08);
transform: rotate(-8deg);
}
.smoke-pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 13px;
border-radius: var(--momo-radius-sm);
background: var(--momo-tag-caramel-bg);
border: 1px solid var(--momo-tag-caramel-border);
color: var(--momo-tag-caramel-text);
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: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
box-shadow: none;
overflow: hidden;
transition: transform .22s ease, box-shadow .22s ease;
}
.smoke-card:hover {
transform: translateY(-4px);
box-shadow: var(--momo-shadow-soft);
}
.status-badge {
border-radius: var(--momo-radius-sm);
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: var(--momo-bg-paper);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-sm);
padding: 12px;
font-family: var(--momo-font-mono);
font-size: 12px;
color: var(--momo-text-secondary);
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: var(--momo-radius-xs);
opacity: .92;
}
.trend-dot.status-ok { background: #22c55e; }
.trend-dot.status-warning { background: #f59e0b; }
.trend-dot.status-critical { background: #ef4444; }
.smoke-summary-table {
table-layout: fixed;
width: 100% !important;
min-width: 0 !important;
}
.smoke-summary-table th,
.smoke-summary-table td {
overflow-wrap: anywhere;
padding-left: 0.35rem;
padding-right: 0.35rem;
white-space: normal;
}
@media (max-width: 520px) {
.smoke-summary-table th,
.smoke-summary-table td {
font-size: 0.78rem;
}
}
</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-muted">快速確認 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-muted">版本 {{ system_version }}</div>
<button id="refreshBtn" class="btn btn-primary mt-3">
<i class="fas fa-sync-alt me-2"></i>重新檢查
</button>
<a class="btn btn-outline-primary 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 smoke-summary-table">
<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 %}