← 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
A public/crcp.htmlM public/index.htmlM scripts/serve.js
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 →