338 lines
12 KiB
HTML
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 => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
}[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 %}
|