← back to Commercialrealestate
CRCP firms: new /api/firms directory endpoint (firm rosters: brokers, commercial/residential split, listings, contact coverage) + firms.html on the shared grid engine (grid/list/table + facets + mobile modal, 1121 firms)
92d0cec78fe6cd9b9c5ae3ba59176fc326ac4b4c · 2026-08-19 08:18:46 -0700 · Steve Abrams
Files touched
A public/firms.htmlM scripts/serve.js
Diff
commit 92d0cec78fe6cd9b9c5ae3ba59176fc326ac4b4c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 19 08:18:46 2026 -0700
CRCP firms: new /api/firms directory endpoint (firm rosters: brokers, commercial/residential split, listings, contact coverage) + firms.html on the shared grid engine (grid/list/table + facets + mobile modal, 1121 firms)
---
public/firms.html | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
scripts/serve.js | 25 +++++++++++++++++++
2 files changed, 99 insertions(+)
diff --git a/public/firms.html b/public/firms.html
new file mode 100644
index 0000000..6f73f47
--- /dev/null
+++ b/public/firms.html
@@ -0,0 +1,74 @@
+<!doctype html>
+<html lang="en">
+<head>
+<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='light'?'light':'dark');}catch(e){document.documentElement.setAttribute('data-theme','dark');}})();</script>
+<!-- Self-heal: nav-agent grid controls can persist a "removed"/"collapsed" state that hides the
+ whole grid with no obvious way back. Clear it before nav-agent runs so a grid can never stay hidden. -->
+<script>(function(){try{for(var i=localStorage.length-1;i>=0;i--){var k=localStorage.key(i);if(k&&k.indexOf('navagent:')===0){var v=localStorage.getItem(k);if(v&&v.indexOf('"removed":true')>=0){localStorage.removeItem(k);}}}}catch(e){}})();</script>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230c0f15'/%3E%3Ctext x='8' y='12' font-size='9' text-anchor='middle' fill='%23C8A24B'%3E%F0%9F%8F%A2%3C/text%3E%3C/svg%3E">
+<title>Firms — LA County CRE brokerages (broker rosters)</title>
+<link rel="stylesheet" href="/lending-grid.css">
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+ <link rel="stylesheet" href="/crcp-theme.css"><!-- CRCP day/night tokens: load LAST so html[data-theme] wins -->
+</head>
+<body>
+<header>
+ <h1>🏢 <b>Firms</b> — brokerages</h1>
+ <div class="nav"><a href="/broker-grid.html">← Brokers</a><a href="/brokers.html">🕸️ Mind-map</a><a href="/crcp.html">🛰️ CRCP</a><a href="/gov-agents.html">🏛️ Gov Agents</a></div>
+ <span class="spacer"></span>
+ <input class="search" id="q" type="text" placeholder="search all fields — firm, broker count, listings… (space = AND)">
+ <div class="toggle" id="view"><button data-v="table" class="on">▦ List</button><button data-v="grid">▤ Grid</button></div>
+ <select id="sortsel" title="Sort"></select>
+ <label id="denswrap" title="Card density — grid view">⤢<input type="range" id="density" min="1" max="10" step="1"></label>
+ <button class="csvbtn" id="csv" title="Export current rows to CSV">⬇ CSV</button>
+ <span class="count" id="count">…</span>
+</header>
+<div class="shell">
+<aside class="rail">
+ <div id="railFacets"></div>
+ <div class="rsec"><h4>Fields / columns — show / hide</h4>
+ <div class="fieldtoggles" id="fFields"></div>
+ <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fReset" title="Reset columns to the tight default">↺ Reset</button></div>
+ <div class="mut" style="font-size:10.5px;margin-top:7px;line-height:1.35">Drag a column header to reorder · drag its right edge to resize · click to sort.</div>
+ </div>
+</aside>
+<div class="wrap">
+ <div class="databar"><span class="pubtag">Registry</span><span id="demoNote">usre broker / firm registry — every firm with a broker roster</span></div>
+ <div id="tableView" class="tblwrap"><table class="g" data-no-sort><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
+ <div id="gridView" class="grid hide"></div>
+</div>
+</div>
+<div id="ov"><div id="modal"><button class="x" aria-label="Close" onclick="document.getElementById('ov').classList.remove('on')">✕</button><div id="mbody"></div></div></div>
+<script>
+window.GRID_CONFIG = {
+ ns: 'crcpFirms', noun: 'firms', nameKey: 'firm', idKey: 'firm',
+ title: 'Brokerage firm · LA County CRE',
+ api: '/api/firms', dataKey: 'firms',
+ sortKey: 'brokers', sortDir: -1,
+ defaultCols: ['firm', 'brokers', 'commercial', 'residential', 'listings', 'with_phone'],
+ cols: [
+ { k: 'firm', l: 'Firm', t: 's', g: 'Identity', max: 48 },
+ { k: 'brokers', l: 'Brokers', t: 'n', g: 'Roster' },
+ { k: 'commercial', l: 'Commercial', t: 'n', g: 'Roster' },
+ { k: 'residential', l: 'Residential', t: 'n', g: 'Roster' },
+ { k: 'listings', l: 'Active listings', t: 'n', g: 'Activity' },
+ { k: 'with_phone', l: 'Has phone', t: 'n', g: 'Contactable' },
+ { k: 'with_email', l: 'Has email', t: 'n', g: 'Contactable' },
+ ],
+ facets: [
+ { k: 'size', l: 'Firm size', derive: r => r.brokers >= 20 ? '20+ brokers' : r.brokers >= 5 ? '5–19 brokers' : r.brokers >= 2 ? '2–4 brokers' : 'solo', limit: 5 },
+ { k: 'activity', l: 'Listing activity', derive: r => r.listings >= 10 ? '10+ listings' : r.listings >= 1 ? '1–9 listings' : 'no active listings', limit: 4 },
+ { k: 'mix', l: 'Roster mix', derive: r => r.commercial > 0 && r.residential > 0 ? 'commercial + residential' : r.commercial > 0 ? 'commercial-only' : 'residential-only', limit: 4 },
+ ],
+};
+</script>
+<script src="/column-manager.js" defer></script>
+<script src="/lending-grid.js" defer></script>
+<script src="/col-resize.js" defer></script>
+<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
+ <script src="/crcp-theme.js" defer></script><!-- CRCP day/night toggle (auto-injects into header) -->
+</body>
+</html>
diff --git a/scripts/serve.js b/scripts/serve.js
index 881f761..0e3048e 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -756,6 +756,31 @@ app.get('/api/firms/share', async (req, res) => {
} catch (e) { res.status(502).json({ error: String(e.message).split('\n')[0] }); }
});
+// Full firms DIRECTORY — every firm with a broker roster, aggregated for the browsable
+// firms.html grid (brokers, commercial/residential split, listings, contact coverage).
+// Local-Postgres only; prod (snapshot, no brokerdb) returns [] like /api/firms/share.
+app.get('/api/firms', async (req, res) => {
+ if (!brokerdb) return res.json({ firms: [] });
+ try {
+ const r = await brokerdb.pool.query(
+ `SELECT f.name AS firm,
+ count(DISTINCT b.id)::int AS brokers,
+ count(DISTINCT b.id) FILTER (WHERE b.agent_type = 'residential')::int AS residential,
+ count(DISTINCT b.id) FILTER (WHERE b.agent_type IS DISTINCT FROM 'residential')::int AS commercial,
+ count(DISTINCT bl.listing_id)::int AS listings,
+ count(DISTINCT b.id) FILTER (WHERE b.phone IS NOT NULL AND b.phone <> '')::int AS with_phone,
+ count(DISTINCT b.id) FILTER (WHERE b.email IS NOT NULL AND b.email <> '')::int AS with_email
+ FROM firm f
+ JOIN broker b ON b.firm_id = f.id
+ LEFT JOIN broker_listing bl ON bl.broker_id = b.id
+ GROUP BY f.name
+ HAVING count(DISTINCT b.id) > 0
+ ORDER BY brokers DESC, listings DESC
+ LIMIT $1`, [Math.min(+(req.query.limit) || 2000, 5000)]);
+ res.json({ firms: r.rows, meta: { source: 'usre broker/firm registry', retrieved_at: new Date().toISOString(), count: r.rows.length } });
+ } catch (e) { res.status(502).json({ error: String(e.message).split('\n')[0] }); }
+});
+
// An FHA cert's stored warrant_signal is frozen at fetch-time; re-derive at request time so a
// project whose expiration_date has already passed reads as expired, not approved (else the
// warrantable proxy over-reports). Dates are zeroed to local midnight to match /api/fha-expiring
← 4a85e2d chore: bump v0.19.0 (session close) — CRCP UI standardizatio
·
back to Commercialrealestate
·
CRCP: migrate last 4 pages (index/mls/fha-loans/graphics) of 50e13d6 →