[object Object]

← 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

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 →