[object Object]

← back to Commercialrealestate

CRCP: live Commercial Real Estate Control Panel (CNCP-styled) — /crcp.html + /api/crcp/stats, auto-polls every 3s showing listings/brokers/firms/condos + live broker-enrichment progress bars

22629f02d250a606bee8507f5db6859e98f6aa16 · 2026-06-28 07:21:14 -0700 · Steve

Files touched

Diff

commit 22629f02d250a606bee8507f5db6859e98f6aa16
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jun 28 07:21:14 2026 -0700

    CRCP: live Commercial Real Estate Control Panel (CNCP-styled) — /crcp.html + /api/crcp/stats, auto-polls every 3s showing listings/brokers/firms/condos + live broker-enrichment progress bars
---
 public/crcp.html  | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 public/index.html |   1 +
 scripts/serve.js  |  39 ++++++++++++++++++
 3 files changed, 160 insertions(+)

diff --git a/public/crcp.html b/public/crcp.html
new file mode 100644
index 0000000..3368d34
--- /dev/null
+++ b/public/crcp.html
@@ -0,0 +1,120 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>CRCP — Commercial Real Estate Control Panel</title>
+<style>
+  /* Emulates CNCP (localhost:3333): dark panels, gold accent, status colors. */
+  :root{ --bg:#0c0f15; --panel:#141925; --panel2:#1d2230; --fg:#E6E8EC; --muted:#8A93A2; --gold:#C8A24B;
+         --active:#70AD47; --staged:#E0A33E; --draft:#5B9BD5; --archived:#5A6472; --del:#E74C3C; --line:#232a38; }
+  *{box-sizing:border-box;} body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;}
+  header{display:flex;align-items:center;gap:14px;padding:14px 22px;border-bottom:1px solid var(--line);background:linear-gradient(180deg,#10141d,#0c0f15);position:sticky;top:0;z-index:10;}
+  header h1{margin:0;font-size:18px;letter-spacing:.5px;} header h1 b{color:var(--gold);}
+  .live{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--muted);}
+  .pulse{width:9px;height:9px;border-radius:50%;background:var(--active);box-shadow:0 0 0 0 rgba(112,173,71,.6);animation:p 1.6s infinite;}
+  @keyframes p{0%{box-shadow:0 0 0 0 rgba(112,173,71,.6)}70%{box-shadow:0 0 0 10px rgba(112,173,71,0)}100%{box-shadow:0 0 0 0 rgba(112,173,71,0)}}
+  header .nav{margin-left:auto;display:flex;gap:14px;} header .nav a{color:var(--draft);text-decoration:none;font-size:13px;} header .nav a:hover{color:var(--gold);}
+  .wrap{padding:18px 22px 40px;max-width:1320px;margin:0 auto;}
+  .tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px;margin-bottom:18px;}
+  .tile{background:var(--panel);border:1px solid var(--line);border-radius:13px;padding:15px 16px;position:relative;overflow:hidden;}
+  .tile .k{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);}
+  .tile .v{font-size:30px;font-weight:700;font-variant-numeric:tabular-nums;margin-top:3px;}
+  .tile .sub{font-size:11px;color:var(--muted);margin-top:2px;}
+  .tile .delta{position:absolute;top:13px;right:14px;font-size:12px;font-weight:700;color:var(--active);opacity:0;transition:opacity .3s;}
+  .tile .delta.show{opacity:1;}
+  .tile.gold .v{color:var(--gold);} .tile.active .v{color:var(--active);} .tile.draft .v{color:var(--draft);} .tile.staged .v{color:var(--staged);}
+  .cols{display:grid;grid-template-columns:1fr 1fr;gap:16px;}
+  @media(max-width:880px){.cols{grid-template-columns:1fr;}}
+  .panel{background:var(--panel);border:1px solid var(--line);border-radius:13px;padding:16px;margin-bottom:16px;}
+  .panel h3{margin:0 0 12px;font-size:13px;text-transform:uppercase;letter-spacing:.6px;color:var(--gold);display:flex;justify-content:space-between;}
+  .panel h3 .c{color:var(--muted);font-weight:400;}
+  table{width:100%;border-collapse:collapse;font-size:13px;} td,th{text-align:left;padding:6px 6px;border-top:1px solid var(--line);}
+  th{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.4px;border-top:0;}
+  td.n{text-align:right;font-variant-numeric:tabular-nums;color:var(--active);font-weight:600;}
+  .firm{color:var(--muted);font-size:11px;}
+  .has{color:var(--active);} .miss{color:var(--archived);}
+  .bar{height:8px;background:#0a0d13;border-radius:5px;overflow:hidden;margin-top:4px;}
+  .bar i{display:block;height:100%;background:linear-gradient(90deg,var(--gold),var(--active));}
+  .prog{display:flex;justify-content:space-between;font-size:12px;color:var(--muted);margin-top:10px;}
+  .meta{font-size:11px;color:var(--muted);margin-top:6px;}
+  .flash{animation:fl .8s;} @keyframes fl{0%{background:rgba(200,162,75,.18)}100%{background:transparent}}
+</style>
+</head>
+<body>
+<header>
+  <h1><b>CRCP</b> · Commercial Real Estate Control Panel</h1>
+  <span class="live"><span class="pulse"></span><span id="livetxt">live</span></span>
+  <nav class="nav">
+    <a href="/">Properties</a><a href="/brokers.html">Mind-map</a><a href="/graphics.html">Graphics</a><a href="/condos.html">Condos</a>
+  </nav>
+</header>
+<div class="wrap">
+  <div class="tiles" id="tiles"></div>
+  <div class="cols">
+    <div>
+      <div class="panel"><h3>Broker enrichment <span class="c" id="enrLbl"></span></h3>
+        <div id="enrBars"></div>
+        <div class="meta" id="enrMeta"></div>
+      </div>
+      <div class="panel"><h3>Top brokers <span class="c" id="brC"></span></h3>
+        <table id="brTbl"><thead><tr><th>Broker</th><th>Firm</th><th class="n">Lists</th><th>Contact</th></tr></thead><tbody></tbody></table>
+      </div>
+    </div>
+    <div>
+      <div class="panel"><h3>Warrantability (FHA proxy) <span class="c" id="waC"></span></h3>
+        <div id="wa"></div>
+        <div class="meta">FHA/VA-approval-based proxy — NOT lender-verified Fannie/Freddie warrantability.</div>
+      </div>
+      <div class="panel"><h3>Top firms by market share <span class="c" id="fmC"></span></h3>
+        <table id="fmTbl"><thead><tr><th>Firm</th><th class="n">Listings</th><th class="n">Brokers</th></tr></thead><tbody></tbody></table>
+      </div>
+    </div>
+  </div>
+</div>
+<script>
+const $=s=>document.querySelector(s);
+const fmt=n=>n==null?'—':Number(n).toLocaleString();
+let prev={};
+function tile(k,key,v,sub,cls){ const d=(prev[key]!=null&&v>prev[key])?('+'+(v-prev[key])):'';
+  return `<div class="tile ${cls||''}"><div class="k">${k}</div><div class="v">${fmt(v)}</div><div class="sub">${sub||''}</div><div class="delta ${d?'show':''}">${d}</div></div>`; }
+function bar(label,have,total,color){ const pct=total?Math.round(100*have/total):0;
+  return `<div style="margin-bottom:10px"><div class="prog"><span>${label}</span><span>${fmt(have)} / ${fmt(total)} · ${pct}%</span></div><div class="bar"><i style="width:${pct}%;background:${color||'linear-gradient(90deg,var(--gold),var(--active))'}"></i></div></div>`; }
+
+async function poll(){
+  let s; try{ s=await (await fetch('/api/crcp/stats')).json(); }catch(e){ $('#livetxt').textContent='reconnecting…'; return; }
+  $('#livetxt').textContent='live · '+new Date(s.ts).toLocaleTimeString();
+  // tiles
+  $('#tiles').innerHTML = [
+    tile('Listings','listings',s.listings,s.market||'LA County','gold'),
+    tile('Brokers','brokers',s.brokers,(s.colist!=null?s.colist+' co-list links':''),'active'),
+    tile('Firms','firms',s.firms,'brokerages','draft'),
+    tile('Condos scraped','condos',s.condos,(s.condosByStatus?Object.entries(s.condosByStatus).map(([k,v])=>v+' '+k.replace('_',' ')).join(' · '):'awaiting Job A'),'staged'),
+    tile('FHA-approved','fhaApproved',s.fhaApproved,'warrantable (proxy)','active'),
+    tile('Broker edges','edges',s.edges,'broker↔listing','gold')
+  ].join('');
+  // enrichment bars (the LIVE populating)
+  $('#enrLbl').textContent = (s.brokers_phone||0)+' phone · '+(s.brokers_email||0)+' email';
+  $('#enrBars').innerHTML = bar('Phone captured', s.brokers_phone||0, s.brokers, 'var(--active)')
+    + bar('Email captured', s.brokers_email||0, s.brokers, 'var(--draft)')
+    + (s.brokers_web!=null?bar('Website found', s.brokers_web, s.brokers, 'var(--gold)'):'');
+  const enr=(s.brokers_phone||0)+(s.brokers_email||0);
+  const penr=(prev.brokers_phone||0)+(prev.brokers_email||0);
+  $('#enrMeta').innerHTML = enr>penr ? `<span class="has">▲ +${enr-penr} contacts since last refresh — Job B populating…</span>` : (enr===0?'Job B (broker enrichment) queued — contacts will populate here live.':'enrichment idle / complete');
+  // top brokers
+  $('#brC').textContent=(s.topBrokers||[]).length+' shown';
+  $('#brTbl tbody').innerHTML=(s.topBrokers||[]).map(b=>`<tr><td>${b.name}</td><td class="firm">${b.firm||'—'}</td><td class="n">${b.listings}</td><td>${b.phone?'<span class="has">☎</span>':'<span class="miss">☎</span>'} ${b.email?'<span class="has">✉</span>':'<span class="miss">✉</span>'}</td></tr>`).join('');
+  // warrantability
+  $('#wa').innerHTML = bar('FHA-approved', s.fhaApproved||0, (s.fhaApproved||0)+(s.fhaExpired||0), 'var(--active)')
+    + bar('FHA-expired (flag/review)', s.fhaExpired||0, (s.fhaApproved||0)+(s.fhaExpired||0), 'var(--staged)');
+  // firms
+  $('#fmTbl tbody').innerHTML=(s.topFirms||[]).map(f=>`<tr><td>${f.firm}</td><td class="n">${f.listings}</td><td class="n" style="color:var(--draft)">${f.brokers}</td></tr>`).join('');
+  prev=s;
+  // flash changed tiles
+  document.querySelectorAll('.delta.show').forEach(d=>d.closest('.tile').classList.add('flash'));
+  setTimeout(()=>document.querySelectorAll('.flash').forEach(e=>e.classList.remove('flash')),800);
+}
+poll(); setInterval(poll, 3000);
+</script>
+</body>
+</html>
diff --git a/public/index.html b/public/index.html
index 2cc478d..e20c3c0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -116,6 +116,7 @@
     <div class="sub" id="sub">loading…</div>
   </div>
   <div class="spacer"></div>
+  <a href="/crcp.html" style="color:var(--warn);text-decoration:none;font-size:13px;margin-right:12px" title="Commercial Real Estate Control Panel (live)">🛰️ CRCP</a>
   <a href="/brokers.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent relationship mind-map">🕸️ Broker map</a>
   <a href="/graphics.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / firm / warrantability charts">📊 Graphics</a>
   <a href="/condos.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LA condos — FHA warrantability">🏢 Condos</a>
diff --git a/scripts/serve.js b/scripts/serve.js
index 1127450..63bceef 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -172,6 +172,45 @@ app.get('/api/fha-condos', (req, res) => {
   } catch (e) { res.status(502).json({ error: String(e.message).split('\n')[0], condos: [] }); }
 });
 
+// CRCP — Commercial Real Estate Control Panel live stats (CNCP-style). One poll → every headline
+// number. brokersWithPhone/Email + condos tick UP live while the enrichment/scrape jobs run, so the
+// panel visibly "populates". All read-only.
+app.get('/api/crcp/stats', async (req, res) => {
+  const out = { ts: Date.now() };
+  try {
+    // listings + market from ranked.json (no DB needed)
+    try {
+      const rk = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', 'ranked.json'), 'utf8'));
+      out.listings = rk.ranked.length; out.market = rk.meta && rk.meta.market;
+      const types = {}; rk.ranked.forEach(p => { types[p.type] = (types[p.type] || 0) + 1; });
+      out.byType = types;
+    } catch (_) { out.listings = 0; }
+    // FHA warrantability reference
+    try {
+      const fc = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', 'fha-approved-condos.json'), 'utf8')).condos;
+      out.fhaApproved = fc.filter(c => c.warrant_signal === 'fha_approved').length;
+      out.fhaExpired = fc.filter(c => c.warrant_signal === 'fha_expired').length;
+    } catch (_) {}
+    if (brokerdb) {
+      const q = s => brokerdb.pool.query(s).then(r => r.rows);
+      const [c1] = await q(`SELECT
+        (SELECT count(*) FROM broker)::int brokers,
+        (SELECT count(*) FROM firm)::int firms,
+        (SELECT count(*) FROM broker WHERE phone IS NOT NULL)::int brokers_phone,
+        (SELECT count(*) FROM broker WHERE email IS NOT NULL)::int brokers_email,
+        (SELECT count(*) FROM broker_listing)::int edges,
+        (SELECT count(*) FROM broker_cobroker)::int colist`);
+      Object.assign(out, c1);
+      try { out.brokers_web = (await q(`SELECT count(*)::int n FROM broker WHERE website IS NOT NULL`))[0].n; } catch (_) { out.brokers_web = null; }
+      try { out.condos = (await q(`SELECT count(*)::int n FROM condo`))[0].n;
+            out.condosByStatus = {}; (await q(`SELECT warrantable_status s,count(*)::int n FROM condo GROUP BY 1`)).forEach(r => out.condosByStatus[r.s] = r.n); } catch (_) { out.condos = 0; }
+      out.topBrokers = await q(`SELECT name, firm, listings, total_assets, phone, email FROM broker_node ORDER BY listings DESC NULLS LAST LIMIT 12`);
+      out.topFirms = await q(`SELECT f.name firm, count(DISTINCT bl.listing_id)::int listings, count(DISTINCT b.id)::int brokers FROM firm f JOIN broker b ON b.firm_id=f.id JOIN broker_listing bl ON bl.broker_id=b.id GROUP BY 1 ORDER BY 2 DESC LIMIT 12`);
+    }
+    res.json(out);
+  } catch (e) { res.status(502).json({ ...out, error: String(e.message).split('\n')[0] }); }
+});
+
 app.use('/data', express.static(path.join(ROOT, 'data')));
 app.use('/', express.static(path.join(ROOT, 'public')));
 

← 88d26a3 CRE Job B Tier 2: firm-website team-page crawler ($0 local H  ·  back to Commercialrealestate  ·  CRE Job B Tier 3: Brave-search enrichment (~$0.004/q) for br 63ed0e2 →