← 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 &amp; 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>