Files
ewoooc/templates/ai_automation_smoke.html
ogt 2144ef2102
All checks were successful
CD Pipeline / deploy (push) Successful in 1m11s
fix: lock pchome growth ui copy guardrails
2026-06-26 11:38:04 +08:00

338 lines
12 KiB
HTML

{% extends 'ewoooc_base.html' %}
{% block title %}AI 自動化健康檢查 - 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>AI 閉環守門</span>
<h1 class="fw-bold mb-2">AI 自動化健康檢查</h1>
<p class="mb-0 text-muted">確認 AI 建議、修復與通知是否能支援業績流程。</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>下載檢查紀錄
</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">正常</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">注意</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">嚴重</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">產生時間</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">最近健康檢查趨勢</h5>
<div class="text-muted small">保留最近檢查結果,判斷 AI 閉環是否連續穩定。</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">用每日彙整快速找出連續風險。</div>
</div>
</div>
<div class="table-responsive">
<table class="table align-middle mb-0 smoke-summary-table">
<thead>
<tr>
<th>日期</th>
<th>正常</th>
<th>注意</th>
<th>嚴重</th>
<th>總計</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: '正常', warning: '注意', 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">已保留診斷細節,請由維護者查看。</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 =
`正常 ${counts.ok || 0} / 注意 ${counts.warning || 0} / 嚴重 ${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('smoke-unavailable');
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>健康檢查服務</h5>
<p class="text-danger">讀取失敗,請稍後重試。</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 健康檢查趨勢紀錄?這只會刪除本頁趨勢,不影響正式事件資料。')) {
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} 筆健康檢查趨勢紀錄`, '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 健康檢查每日摘要已推播:已送出 ${data.telegram?.sent || 0}`, 'success');
} catch (err) {
showToast(`推播失敗:${err.message}`, 'error');
} finally {
btn.disabled = false;
}
});
loadSmoke();
</script>
{% endblock %}