← back to Nationalrealestate
public/health.html
133 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>USRealEstate — Ingest Engine Health</title>
<style>
:root{--bg:#0f1216;--card:#181c22;--line:#262c34;--muted:#8b95a3;--fg:#e7ecf2;
--healthy:#2ecc71;--degraded:#f1c40f;--retrying:#e67e22;--quarantined:#e74c3c;--unknown:#6b7684;}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
header{padding:18px 22px;border-bottom:1px solid var(--line);display:flex;align-items:baseline;gap:14px;flex-wrap:wrap}
h1{font-size:18px;margin:0;font-weight:650;letter-spacing:.2px}
.sub{color:var(--muted);font-size:12px}
.wrap{padding:22px;max-width:1100px;margin:0 auto}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px}
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px}
.card h3{margin:0 0 8px;font-size:15px;display:flex;align-items:center;gap:8px}
.dot{width:10px;height:10px;border-radius:50%;display:inline-block}
.badge{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;padding:2px 8px;border-radius:99px;margin-left:auto}
.kv{display:flex;justify-content:space-between;color:var(--muted);font-size:12.5px;padding:2px 0}
.kv b{color:var(--fg);font-weight:550}
.reason{margin-top:8px;font-size:12px;color:var(--muted);border-top:1px dashed var(--line);padding-top:8px}
section h2{font-size:13px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);margin:26px 0 10px}
.quota-bar{height:10px;background:#222831;border-radius:6px;overflow:hidden;margin-top:6px}
.quota-fill{height:100%;background:linear-gradient(90deg,#2ecc71,#f1c40f)}
table{width:100%;border-collapse:collapse;font-size:12.5px}
td,th{text-align:left;padding:6px 10px;border-bottom:1px solid var(--line)}
th{color:var(--muted);font-weight:550}
.ev{font-weight:600}
.muted{color:var(--muted)}
a{color:#5aa9ff;text-decoration:none}
</style>
</head>
<body>
<header>
<h1>⚙︎ Ingest Engine Health</h1>
<span class="sub" id="stamp">loading…</span>
<span class="sub" style="margin-left:auto"><a href="/">← portal</a> · auto-refresh 30s</span>
</header>
<div class="wrap">
<div class="grid" id="cards"></div>
<section>
<h2>Google Places — monthly quota (free-tier hard cap)</h2>
<div class="card" id="quota">…</div>
</section>
<section>
<h2>Homes & commercial coverage — West → East sweep</h2>
<div class="card" id="coverage" style="padding:4px 0">…</div>
</section>
<section>
<h2>Broker rosters by state</h2>
<div class="card" style="padding:4px 0"><table id="brokerStates"><tbody><tr><td class="muted" style="padding:12px">…</td></tr></tbody></table></div>
</section>
<section>
<h2>Recent self-correction events</h2>
<div class="card" style="padding:4px 0"><table id="events"><tbody><tr><td class="muted" style="padding:12px">…</td></tr></tbody></table></div>
</section>
</div>
<script>
const COLORS = {healthy:'var(--healthy)',degraded:'var(--degraded)',retrying:'var(--retrying)',quarantined:'var(--quarantined)',unknown:'var(--unknown)'};
const EVCOLOR = {recovered:'var(--healthy)',degraded:'var(--degraded)',retry:'var(--retrying)',probe:'#5aa9ff',quarantined:'var(--quarantined)'};
function num(v){ return v==null?'—':Number(v).toLocaleString(); }
async function load(){
let d;
try { d = await (await fetch('/api/ingest-health',{headers:{'accept':'application/json'}})).json(); }
catch(e){ document.getElementById('stamp').textContent='fetch failed'; return; }
if(!d.ok){ document.getElementById('stamp').textContent='error: '+d.error; return; }
document.getElementById('stamp').textContent =
d.sources.length+' streams · updated '+new Date(d.generated_at).toLocaleTimeString();
document.getElementById('cards').innerHTML = d.sources.map(s=>{
const c = COLORS[s.state]||COLORS.unknown;
const base = s.baseline_upserted ? ' / base '+num(s.baseline_upserted) : '';
return `<div class="card">
<h3><span class="dot" style="background:${c}"></span>${s.job}
<span class="badge" style="background:${c}22;color:${c}">${s.state}</span></h3>
<div class="kv"><span>last OK</span><b>${s.last_ok_at||'never'}${s.ok_age_h!=null?` · ${s.ok_age_h}h ago`:''}</b></div>
<div class="kv"><span>last rows</span><b>${num(s.last_upserted)}${base}</b></div>
<div class="kv"><span>consec. failures</span><b>${s.consecutive_failures||0}</b></div>
${s.quarantined_at?`<div class="kv"><span>quarantined</span><b style="color:var(--quarantined)">${s.quarantined_at}</b></div>`:''}
</div>`;
}).join('') || '<div class="card muted">No source health yet — the loop hasn\'t run a tick.</div>';
const q = d.places_quota, pct = q.cap? Math.min(100,Math.round(100*q.calls_used/q.cap)):0;
document.getElementById('quota').innerHTML =
`<div class="kv"><span>${q.year_month||'this month'}</span><b>${num(q.calls_used)} / ${num(q.cap)} calls · $0 (free tier)</b></div>
<div class="quota-bar"><div class="quota-fill" style="width:${pct}%"></div></div>
<div class="reason">Calls stop automatically at the cap — no spend is possible.</div>`;
// Geographic coverage sweep (West → East)
document.getElementById('coverage').innerHTML =
'<table><thead><tr><th>region</th><th>metros</th><th>started</th><th>covered (≥60)</th><th>progress</th></tr></thead><tbody>' +
(d.coverage||[]).map(c=>{
const pct = c.metros? Math.round(100*c.covered/c.metros):0;
return `<tr><td>${c.tier.replace(/^\d·/,'')}</td><td>${num(c.metros)}</td><td>${num(c.started)}</td><td><b>${num(c.covered)}</b></td>
<td><div class="quota-bar" style="width:120px"><div class="quota-fill" style="width:${pct}%"></div></div></td></tr>`;
}).join('') + '</tbody></table>';
// Per-state broker rosters
const stName = s => s.split('_')[0].toUpperCase();
document.getElementById('brokerStates').innerHTML =
'<thead><tr><th>state</th><th>source</th><th>brokers</th><th>last OK</th><th>status</th></tr></thead><tbody>' +
(d.broker_states||[]).map(b=>{
const ok = b.last_status==='ok', c = ok?'var(--healthy)':(b.last_status==='failed'?'var(--quarantined)':'var(--unknown)');
const age = b.ok_age_d!=null? ` · ${b.ok_age_d}d ago`:'';
return `<tr><td><b>${stName(b.source)}</b></td><td class="muted">${b.source}</td><td>${num(b.brokers)}</td>
<td class="muted">${b.last_ok||'never'}${age}</td>
<td><span class="dot" style="background:${c}"></span> ${b.last_status||'—'}</td></tr>`;
}).join('') + '</tbody>';
document.getElementById('events').innerHTML =
'<thead><tr><th>when</th><th>stream</th><th>event</th><th>detail</th></tr></thead><tbody>' +
(d.events.length ? d.events.map(e=>
`<tr><td class="muted">${e.at}</td><td>${e.job}</td>
<td class="ev" style="color:${EVCOLOR[e.event]||'var(--fg)'}">${e.event}</td>
<td class="muted">${e.detail||''}</td></tr>`).join('')
: '<tr><td class="muted" style="padding:12px" colspan="4">No events — every source healthy since the engine started.</td></tr>') +
'</tbody>';
}
load(); setInterval(load, 30000);
</script>
</body>
</html>