← back to Homesonspec

scripts/engine-dashboard.mjs

145 lines

// HomesOnSpec "engine room" — a live V8 where EACH cylinder fires only when ITS
// data stream is actually moving (records landing right now). Idle streams go dark
// and still. Zero-dep (Node http + psql + pgrep). Run: node scripts/engine-dashboard.mjs
import { createServer } from "node:http";
import { execFile } from "node:child_process";

const DBURL = "postgresql://macstudio3@localhost/homesonspec?host=/tmp";
const PORT = Number(process.env.PORT || 9798);
const sh = (cmd, args) => new Promise((r) => execFile(cmd, args, { maxBuffer: 8 << 20 }, (e, o) => r(e ? "" : o.trim())));
const q = (sql) => sh("psql", [DBURL, "-tAc", sql]);

// 8 cylinders = 8 live data streams, each with the SQL that counts its records.
const STREAMS = [
  ["Lennar",     `select count(*) from "InventoryHome" h join "Builder" b on b.id=h."builderId" where b.slug='lennar' and h.status='PUBLISHED'`],
  ["DR Horton",  `select count(*) from "InventoryHome" h join "Builder" b on b.id=h."builderId" where b.slug='dr-horton' and h.status='PUBLISHED'`],
  ["Pulte",      `select count(*) from "InventoryHome" h join "Builder" b on b.id=h."builderId" where b.slug='pultegroup' and h.status='PUBLISHED'`],
  ["Tri Pointe", `select count(*) from "InventoryHome" h join "Builder" b on b.id=h."builderId" where b.slug='tri-pointe' and h.status='PUBLISHED'`],
  ["Discovery",  `select count(*) from "InventoryHome" h join "Builder" b on b.id=h."builderId" where b.slug='discovery-homes' and h.status='PUBLISHED'`],
  ["Permits",    `select count(*) from building_permits`],
  ["Geocode",    `select count(*) from building_permits where lat is not null`],
  ["Enrich",     `select count(*) from "Community" where amenities is not null`],
];

let prev = null, prevT = 0;
const sm = new Array(STREAMS.length).fill(0); // per-stream smoothed rate

async function stats() {
  const bigsql = "select json_build_array(" + STREAMS.map(([, s]) => `(${s})`).join(",") + ")";
  let counts; try { counts = JSON.parse(await q(bigsql)); } catch { counts = STREAMS.map(() => 0); }
  const now = Date.now();
  const dt = prev ? Math.max(1, (now - prevT) / 1000) : 1;
  const streams = STREAMS.map(([name], i) => {
    const rate = prev ? Math.max(0, (counts[i] - prev[i]) / dt) : 0;
    sm[i] = Math.max(rate, sm[i] * 0.55);           // decay so a burst sustains a few polls
    const live = sm[i] > 0.05;
    return { name, count: counts[i], rate: sm[i], live };
  });
  prev = counts; prevT = now;
  const procs = await sh("bash", ["-lc", "pgrep -fl 'build-loop.sh|ingest-permits|geocode-permits|enrich-nearby' 2>/dev/null | wc -l"]);
  const totalRate = streams.reduce((a, s) => a + s.rate, 0);
  const liveCount = streams.filter((s) => s.live).length;
  const rpm = liveCount ? Math.min(7200, Math.round(900 + totalRate * 210)) : 0; // no live data → engine OFF
  return {
    streams, rpm, totalRate, liveCount, workers: Number(procs) || 0,
    homes: counts.slice(0, 5).reduce((a, b) => a + b, 0),
    permits: counts[5], permits_geo: counts[6], enriched: counts[7],
  };
}

const PAGE = `<!doctype html><html><head><meta charset=utf8><title>HomesOnSpec · V8</title>
<meta name=viewport content="width=device-width,initial-scale=1">
<style>
:root{--red:#e01b1b;--amber:#ffb020;--grn:#2dd4bf;--ink:#e9eef2}
*{box-sizing:border-box}html,body{margin:0;height:100%;background:radial-gradient(1200px 700px at 50% -10%,#141a20,#080a0c);color:var(--ink);font:13px/1.4 ui-monospace,Menlo,monospace;overflow:hidden}
.hd{display:flex;justify-content:space-between;align-items:center;padding:12px 20px;border-bottom:1px solid #1c262e}
.hd h1{margin:0;font:700 17px system-ui;letter-spacing:.5px}.hd .sub{color:#7f97a3;font-size:11px}.badge{color:var(--red);font-weight:700}
.stage{display:grid;grid-template-columns:1fr 300px;gap:18px;padding:16px 20px;height:calc(100% - 52px)}
.enginewrap{display:flex;align-items:center;justify-content:center}svg{width:100%;height:100%;max-height:80vh}
@keyframes stroke{0%{transform:translateY(0)}50%{transform:translateY(52px)}100%{transform:translateY(0)}}
@keyframes spin{to{transform:rotate(360deg)}}
.crank{transform-origin:310px 452px}
.panel{display:flex;flex-direction:column;gap:11px}
.gauge{background:#0d141a;border:1px solid #1e2a32;border-radius:12px;padding:11px 14px}
.gauge .k{color:#7f97a3;font-size:10px;text-transform:uppercase;letter-spacing:1px}.gauge .v{font:800 25px system-ui;margin-top:2px}.gauge .d{color:var(--grn);font-size:11px}
.rpmv{font:900 40px system-ui;text-shadow:0 0 18px rgba(224,27,27,.4)}
.rpml{color:#7f97a3;font-size:10px;letter-spacing:2px}
.live{color:var(--grn)}.off{color:#4a5a64}
.foot{color:#5f7684;font-size:10px;text-align:center}
</style></head><body>
<div class=hd><div><h1>HOMESONSPEC <span class=badge>V8</span> ENGINE ROOM</h1><div class=sub>each cylinder fires only when its data stream is moving live</div></div>
<div class=sub id=work>workers: –</div></div>
<div class=stage>
 <div class=enginewrap><svg viewBox="0 0 620 540" id=eng>
   <defs><linearGradient id=pist x1=0 y1=0 x2=0 y2=1><stop offset=0 stop-color=#cfd8de /><stop offset=1 stop-color=#8996a0 /></linearGradient>
     <radialGradient id=pulley><stop offset=0 stop-color=#3a4650 /><stop offset=1 stop-color=#141a1f /></radialGradient></defs>
   <rect x=60 y=64 width=500 height=320 rx=16 fill=#0c1216 stroke=#222d35 />
   <text x=310 y=52 fill=#5f7684 font-size=11 text-anchor=middle letter-spacing=3>V8 · 8 LIVE DATA STREAMS</text>
   <g id=cyls></g>
   <g class=crank id=crank><circle cx=310 cy=452 r=46 fill=url(#pulley) stroke=#2a353d stroke-width=4 /><circle cx=310 cy=452 r=7 fill=#e01b1b /><rect x=306 y=410 width=8 height=42 fill=#3a4650 /></g>
   <circle cx=470 cy=452 r=22 fill=url(#pulley) stroke=#2a353d stroke-width=3 /><circle cx=170 cy=452 r=22 fill=url(#pulley) stroke=#2a353d stroke-width=3 />
   <path d="M170 452 L470 452" stroke=#3a4650 stroke-width=3 opacity=.5 />
 </svg></div>
 <div class=panel>
  <div class=gauge><div class=rpml>ENGINE RPM · LIVE THROUGHPUT</div><div class=rpmv id=rpm>—</div><div class=d id=rate></div></div>
  <div class=gauge><div class=k>Streams firing now</div><div class=v id=liven>—</div><div class=d id=livelist></div></div>
  <div class=gauge><div class=k>Listed homes</div><div class=v id=homes>—</div></div>
  <div class=gauge><div class=k>Building permits · geocoded</div><div class=v id=permits>—</div></div>
  <div class=gauge><div class=k>Communities enriched</div><div class=v id=enriched>—</div></div>
  <div class=foot id=stamp></div>
 </div>
</div>
<script>
const NAMES=[];const cyls=document.getElementById('cyls');let g='';
for(let i=0;i<8;i++){const bank=i<4?0:1;const idx=i%4;const x=130+idx*100;const yTop=bank?230:88;
 g+='<g transform="translate('+x+','+yTop+')">'
  +'<rect x=-26 y=0 width=52 height=86 rx=6 fill=#0e151a stroke=#28343d />'
  +'<rect id="p'+i+'" x=-22 y=6 width=44 height=28 rx=4 fill=url(#pist) style="transform-origin:center"/>'
  +'<circle id="s'+i+'" cx=0 cy=2 r=6 fill=#ffcf5a opacity=.1 />'
  +'<text id="t'+i+'" x=0 y=-8 fill=#546570 font-size=9 text-anchor=middle></text>'
  +'<text id="r'+i+'" x=0 y=104 fill=#3f4d57 font-size=8 text-anchor=middle></text></g>';
}
cyls.innerHTML=g;
const fmt=n=>Number(n).toLocaleString();
async function tick(){
 let s;try{s=await fetch('/api/stats').then(r=>r.json())}catch(e){return}
 // engine RPM
 const rpmEl=document.getElementById('rpm');rpmEl.textContent=s.rpm?fmt(s.rpm):'OFF';
 rpmEl.style.color=s.rpm?'#e01b1b':'#4a5a64';
 document.getElementById('rate').textContent=s.totalRate>0?('▲ '+s.totalRate.toFixed(1)+' records/sec'):'no live data';
 // crank spins only if anything is live
 const crank=document.getElementById('crank');
 crank.style.animation=s.rpm?('spin '+Math.max(.1,60/s.rpm).toFixed(3)+'s linear infinite'):'none';
 // per-cylinder: fire ONLY the moving streams
 const firing=[];
 s.streams.forEach((st,i)=>{
   const p=document.getElementById('p'+i),sp=document.getElementById('s'+i),t=document.getElementById('t'+i),r=document.getElementById('r'+i);
   t.textContent=st.name;
   if(st.live){
     const dur=Math.max(.12, 1/Math.min(8,st.rate)).toFixed(3); // faster stream = faster piston
     p.style.animation='stroke '+dur+'s linear infinite';
     p.style.fill='url(#pist)';sp.style.opacity=.95;t.style.fill='#2dd4bf';
     r.textContent='+'+st.rate.toFixed(1)+'/s';r.style.fill='#2dd4bf';
     firing.push(st.name);
   }else{
     p.style.animation='none';p.style.fill='#20282e';sp.style.opacity=.06;t.style.fill='#465561';
     r.textContent='idle';r.style.fill='#33424c';
   }
 });
 document.getElementById('liven').textContent=s.liveCount+' / 8';
 document.getElementById('livelist').textContent=firing.join(' · ')||'engine idle';
 document.getElementById('homes').textContent=fmt(s.homes);
 document.getElementById('permits').textContent=fmt(s.permits)+' · '+fmt(s.permits_geo);
 document.getElementById('enriched').textContent=fmt(s.enriched);
 document.getElementById('work').textContent='workers: '+s.workers;
 document.getElementById('stamp').textContent=new Date().toLocaleTimeString()+' · '+fmt(s.homes+s.permits)+' records under the hood';
}
tick();setInterval(tick,2500);
</script></body></html>`;

const server = createServer(async (req, res) => {
  if (req.url === "/api/stats") { res.setHeader("content-type", "application/json"); return res.end(JSON.stringify(await stats())); }
  res.setHeader("content-type", "text/html"); res.end(PAGE);
});
server.listen(PORT, () => console.log(`engine-dashboard on http://127.0.0.1:${PORT}`));