117 lines
5.0 KiB
HTML
117 lines
5.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Watchdog — CCSite Fleet</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{background:#0a0a0f;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding:1.5rem}
|
|
h1{font-size:1.5rem;margin-bottom:0.3rem}
|
|
h1 span{color:#22c55e}
|
|
.sub{color:#666;font-size:0.8rem;margin-bottom:1.5rem}
|
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}
|
|
.card{background:#111118;border-radius:10px;padding:1.2rem;border:1px solid #1a1a24}
|
|
.card.pass{border-color:#22c55e33}
|
|
.card.warn{border-color:#f59e0b33}
|
|
.card.fail{border-color:#ef444433}
|
|
.card .header{display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem}
|
|
.card .name{font-weight:700;font-size:0.9rem}
|
|
.card .status{font-size:0.7rem;padding:0.2rem 0.6rem;border-radius:4px;font-weight:700;text-transform:uppercase}
|
|
.status.pass{background:#22c55e22;color:#22c55e}
|
|
.status.warn{background:#f59e0b22;color:#f59e0b}
|
|
.status.fail{background:#ef444422;color:#ef4444}
|
|
.card .detail{font-size:0.75rem;color:#666;font-family:monospace}
|
|
.summary{display:flex;gap:1.5rem;margin-bottom:1.5rem;flex-wrap:wrap}
|
|
.summary .stat{text-align:center}
|
|
.summary .stat .n{font-size:2rem;font-weight:900}
|
|
.summary .stat .l{font-size:0.65rem;color:#666;text-transform:uppercase;letter-spacing:1px}
|
|
.summary .stat .n.green{color:#22c55e}
|
|
.summary .stat .n.yellow{color:#f59e0b}
|
|
.summary .stat .n.red{color:#ef4444}
|
|
.issues{background:#ef444411;border:1px solid #ef444433;border-radius:10px;padding:1rem;margin-bottom:1.5rem}
|
|
.issues h3{font-size:0.85rem;color:#ef4444;margin-bottom:0.5rem}
|
|
.issues li{font-size:0.8rem;color:#fca5a5;margin-left:1.2rem;line-height:1.6}
|
|
.healing{background:#f59e0b11;border:1px solid #f59e0b33;border-radius:10px;padding:1rem;margin-bottom:1.5rem}
|
|
.healing h3{font-size:0.85rem;color:#f59e0b;margin-bottom:0.5rem}
|
|
.healing li{font-size:0.8rem;color:#fcd34d;margin-left:1.2rem;line-height:1.6}
|
|
.actions{margin-top:1rem;display:flex;gap:0.5rem}
|
|
button{background:#1a1a2e;border:1px solid #333;color:#ccc;padding:0.5rem 1rem;border-radius:6px;cursor:pointer;font-size:0.8rem}
|
|
button:hover{background:#222;border-color:#555}
|
|
.refresh{color:#666;font-size:0.7rem;margin-top:0.5rem}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>🛡️ <span>CCSite Watchdog</span></h1>
|
|
<p class="sub">Self-healing health monitor — checks every 60s</p>
|
|
|
|
<div id="summary" class="summary"></div>
|
|
<div id="issues"></div>
|
|
<div id="healing"></div>
|
|
|
|
<div class="actions">
|
|
<button onclick="runScan()">🔍 Run Scan Now</button>
|
|
<button onclick="location.reload()">🔄 Refresh</button>
|
|
</div>
|
|
|
|
<div class="grid" id="checks"></div>
|
|
<p class="refresh" id="last"></p>
|
|
|
|
<script>
|
|
function icon(s) { return s==='pass'?'✅':s==='warn'?'⚠️':'❌' }
|
|
|
|
async function load() {
|
|
try {
|
|
const r = await fetch('/api/watchdog');
|
|
const d = await r.json();
|
|
|
|
// Summary
|
|
const s = d.summary || {};
|
|
const statusEmoji = s.status === 'healthy' ? '🟢' : s.status === 'degraded' ? '🟡' : '🔴';
|
|
document.getElementById('summary').innerHTML =
|
|
`<div class="stat"><div class="n green">${s.pass||0}</div><div class="l">Passing</div></div>
|
|
<div class="stat"><div class="n yellow">${s.warn||0}</div><div class="l">Warnings</div></div>
|
|
<div class="stat"><div class="n red">${s.fail||0}</div><div class="l">Failing</div></div>
|
|
<div class="stat"><div class="n">${statusEmoji}</div><div class="l">${(s.status||'unknown').toUpperCase()}</div></div>`;
|
|
|
|
// Issues
|
|
const issues = d.issues || [];
|
|
document.getElementById('issues').innerHTML = issues.length ?
|
|
`<div class="issues"><h3>🚨 ${issues.length} Issue(s)</h3><ul>${issues.map(i=>`<li>${i}</li>`).join('')}</ul></div>` : '';
|
|
|
|
// Healing
|
|
const healing = d.healing_actions || [];
|
|
document.getElementById('healing').innerHTML = healing.length ?
|
|
`<div class="healing"><h3>🔧 Self-Healing Actions</h3><ul>${healing.map(h=>`<li>${h.success?'✅':'❌'} ${h.action}: ${h.detail}</li>`).join('')}</ul></div>` : '';
|
|
|
|
// Checks
|
|
const checks = d.checks || [];
|
|
document.getElementById('checks').innerHTML = checks.map(c =>
|
|
`<div class="card ${c.status}">
|
|
<div class="header">
|
|
<span class="name">${c.check}</span>
|
|
<span class="status ${c.status}">${c.status}</span>
|
|
</div>
|
|
<div class="detail">${c.detail}</div>
|
|
</div>`
|
|
).join('');
|
|
|
|
document.getElementById('last').textContent = `Last check: ${d.last_check || 'never'}`;
|
|
} catch(e) {
|
|
document.getElementById('summary').innerHTML = '<div class="stat"><div class="n red">⚠️</div><div class="l">Watchdog unavailable</div></div>';
|
|
}
|
|
}
|
|
|
|
async function runScan() {
|
|
document.querySelector('button').textContent = 'Scanning...';
|
|
await fetch('/api/watchdog/run', {method:'POST'});
|
|
await load();
|
|
document.querySelector('button').textContent = '🔍 Run Scan Now';
|
|
}
|
|
|
|
load();
|
|
setInterval(load, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|