[object Object]

← back to Commercialrealestate

CRCP: residential broker/firm directory proxied from usre (asset_class=residential) as Frank's lending-lead source (Node C, TK-10535)

fb2a46f38e9c5dd7d9064917b0d0aa2eb08849ac · 2026-08-13 10:37:23 -0700 · steve

Files touched

Diff

commit fb2a46f38e9c5dd7d9064917b0d0aa2eb08849ac
Author: steve <steve@designerwallcoverings.com>
Date:   Thu Aug 13 10:37:23 2026 -0700

    CRCP: residential broker/firm directory proxied from usre (asset_class=residential) as Frank's lending-lead source (Node C, TK-10535)
---
 public/residential-brokers.html | 129 ++++++++++++++++++++++++++++++++++++++++
 scripts/serve.js                |  31 ++++++++++
 2 files changed, 160 insertions(+)

diff --git a/public/residential-brokers.html b/public/residential-brokers.html
new file mode 100644
index 0000000..be90ed9
--- /dev/null
+++ b/public/residential-brokers.html
@@ -0,0 +1,129 @@
+<!doctype html>
+<html lang="en">
+<head>
+<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='%230a0d13'/%3E%3Ctext x='8' y='12' font-size='11' text-anchor='middle' fill='%23c8a24b'%3E%24%3C/text%3E%3C/svg%3E">
+<title>Residential Brokers — Lending Leads</title>
+<style>
+  :root{--cols:4;--bg:#0a0d13;--panel:#121722;--ink:#e9edf5;--muted:#8b93a3;--line:#232a38;--gold:#c8a24b;--accent:#4a90d9}
+  *{box-sizing:border-box}
+  body{margin:0;font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;background:var(--bg);color:var(--ink)}
+  header{padding:20px 28px 14px;border-bottom:1px solid var(--line);background:linear-gradient(180deg,#0d1119,#0a0d13)}
+  h1{margin:0 0 4px;font-size:20px;font-weight:650;letter-spacing:.2px}
+  h1 span{color:var(--gold)}
+  .sub{color:var(--muted);font-size:12.5px;max-width:760px}
+  a.back{color:var(--accent);text-decoration:none;font-size:12.5px}
+  .bar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding:14px 28px;border-bottom:1px solid var(--line);background:var(--panel);position:sticky;top:0;z-index:5}
+  .bar input,.bar select{background:#0c1017;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px}
+  .bar label{color:var(--muted);font-size:11.5px;display:flex;align-items:center;gap:6px}
+  .count{margin-left:auto;color:var(--muted);font-size:12px}
+  .toggles{display:flex;flex-wrap:wrap;gap:6px;padding:10px 28px;border-bottom:1px solid var(--line);background:#0d1119}
+  .toggles button{background:#0c1017;color:var(--muted);border:1px solid var(--line);border-radius:20px;padding:4px 11px;font-size:11.5px;cursor:pointer}
+  .toggles button.on{color:#0a0d13;background:var(--gold);border-color:var(--gold);font-weight:600}
+  main{padding:20px 28px 60px}
+  .grid{display:grid;grid-template-columns:repeat(var(--cols),minmax(0,1fr));gap:12px}
+  .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:13px 15px;min-width:0}
+  .nm{font-weight:600;font-size:14px;margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+  .fm{color:var(--gold);font-size:12px;margin-bottom:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+  .row{color:var(--muted);font-size:11.5px;margin:1px 0}
+  .row b{color:var(--ink);font-weight:500}
+  .pill{display:inline-block;font-size:10px;padding:1px 7px;border-radius:10px;background:#1a2130;color:var(--muted);border:1px solid var(--line);margin-top:6px}
+  .empty{color:var(--muted);padding:40px;text-align:center}
+</style>
+</head>
+<body>
+<header>
+  <a class="back" href="/">← back to properties</a>
+  <h1>Residential <span>Brokers</span> — Lending Leads</h1>
+  <p class="sub">The residential half of the shared licensee registry, surfaced here as a purchase-loan / refi lead source. Commercial brokers live in the <a class="back" href="/brokers.html">CRE Broker registry</a>. Public DRE license-board data — always re-verify a license before doing business.</p>
+</header>
+
+<div class="bar">
+  <input id="q" type="search" placeholder="Search name, firm, license…" style="min-width:220px">
+  <select id="state">
+    <option value="CA">California</option>
+    <option value="">All states</option>
+    <option value="FL">Florida</option><option value="TX">Texas</option>
+    <option value="NY">New York</option><option value="AZ">Arizona</option>
+  </select>
+  <select id="sort">
+    <option value="name">Sort: Name A→Z</option>
+    <option value="firm_name">Firm A→Z</option>
+    <option value="city">City A→Z</option>
+    <option value="license_state">License state</option>
+    <option value="license_no">License #</option>
+  </select>
+  <label>Density <input id="dens" type="range" min="2" max="7" value="4"></label>
+  <span class="count" id="count">loading…</span>
+</div>
+<div class="toggles" id="toggles"></div>
+
+<main><div class="grid" id="grid"></div><div class="empty" id="empty" hidden></div></main>
+
+<script src="/corner-nav.js" defer></script>
+<script>
+const FIELDS = [
+  {k:'firm',    label:'Firm',    get:r=>r.firm_name},
+  {k:'city',    label:'City',    get:r=>[r.city,r.state_code].filter(Boolean).join(', ')},
+  {k:'license', label:'License', get:r=>r.license_no ? (r.license_no+(r.license_state?' · '+r.license_state:'')) : ''},
+  {k:'status',  label:'Status',  get:r=>r.license_status},
+  {k:'class',   label:'Class',   get:r=>r.asset_class},
+];
+const LS='crcp-resbrokers';
+const state = Object.assign({state:'CA',sort:'name',q:'',cols:4,off:{}}, JSON.parse(localStorage.getItem(LS)||'{}'));
+let ROWS=[];
+const $=id=>document.getElementById(id);
+function save(){ localStorage.setItem(LS, JSON.stringify(state)); }
+
+function renderToggles(){
+  $('toggles').innerHTML='';
+  for(const f of FIELDS){
+    const b=document.createElement('button');
+    b.textContent=f.label; b.className = state.off[f.k] ? '' : 'on';
+    b.onclick=()=>{ state.off[f.k]=!state.off[f.k]; save(); b.className=state.off[f.k]?'':'on'; draw(); };
+    $('toggles').appendChild(b);
+  }
+}
+function draw(){
+  const grid=$('grid');
+  document.documentElement.style.setProperty('--cols', state.cols);
+  let rows=[...ROWS];
+  const key=state.sort;
+  rows.sort((a,b)=>String(a[key]||'').localeCompare(String(b[key]||''),undefined,{numeric:true}));
+  if(!rows.length){ grid.innerHTML=''; $('empty').hidden=false; $('empty').textContent='No residential brokers match.'; return; }
+  $('empty').hidden=true;
+  grid.innerHTML = rows.map(r=>{
+    const lines = FIELDS.filter(f=>!state.off[f.k] && f.k!=='firm' && f.k!=='class').map(f=>{
+      const v=f.get(r); return v ? `<div class="row">${f.label}: <b>${esc(v)}</b></div>` : '';
+    }).join('');
+    const firm = !state.off.firm && r.firm_name ? `<div class="fm">${esc(r.firm_name)}</div>` : '';
+    const cls = !state.off.class && r.asset_class ? `<span class="pill">${esc(r.asset_class)}</span>` : '';
+    return `<div class="card"><div class="nm">${esc(r.name||'—')}</div>${firm}${lines}${cls}</div>`;
+  }).join('');
+}
+function esc(s){ return String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
+
+async function load(){
+  $('count').textContent='loading…';
+  const p=new URLSearchParams({state:state.state,limit:'500'});
+  if(state.q) p.set('q',state.q);
+  try{
+    const r=await fetch('/api/residential-brokers?'+p);
+    const j=await r.json();
+    ROWS=j.rows||[];
+    $('count').textContent = j.upstream_down ? 'registry offline' : `${j.total?.toLocaleString?.()||ROWS.length} residential brokers`;
+    draw();
+  }catch(e){ $('count').textContent='error'; $('empty').hidden=false; $('empty').textContent='Could not reach the registry.'; }
+}
+
+// wire controls
+$('q').value=state.q; $('state').value=state.state; $('sort').value=state.sort; $('dens').value=state.cols;
+let t; $('q').oninput=e=>{ state.q=e.target.value.trim(); save(); clearTimeout(t); t=setTimeout(load,300); };
+$('state').onchange=e=>{ state.state=e.target.value; save(); load(); };
+$('sort').onchange=e=>{ state.sort=e.target.value; save(); draw(); };
+$('dens').oninput=e=>{ state.cols=+e.target.value; save(); draw(); };
+renderToggles(); load();
+</script>
+</body>
+</html>
diff --git a/scripts/serve.js b/scripts/serve.js
index b73e458..c107bca 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -1699,6 +1699,37 @@ function contractorQs(q, keys) {
   for (const k of keys) { const v = q[k]; if (v != null && String(v).trim() !== '') u.set(k, String(v).slice(0, 120)); }
   return u.toString() ? '?' + u.toString() : '';
 }
+
+// ── Residential broker directory (TK-10535) ─────────────────────────────────────────────
+// CRCP is a COMMERCIAL platform, but Frank (loan officer) also writes RESIDENTIAL loans, so we
+// surface the residential half of the shared usre broker/firm registry here as a lead source.
+// CRCP's OWN commercial brokers stay in /api/brokers/all (local brokerdb); THIS is the residential
+// complement, proxied from usre with a forced asset_class=residential so the two never cross-mix.
+const USRE_BASE = (process.env.USRE_BASE || CONTRACTORS_API_BASE).replace(/\/+$/, '');
+const USRE_AUTH = 'Basic ' + Buffer.from(process.env.USRE_AUTH || 'admin:DW2024!').toString('base64');
+async function usreFetch(pathQs, timeoutMs = 10000) {
+  const ac = new AbortController(); const timer = setTimeout(() => ac.abort(), timeoutMs);
+  try {
+    const r = await fetch(USRE_BASE + pathQs, { signal: ac.signal, headers: { Accept: 'application/json', Authorization: USRE_AUTH } });
+    if (!r.ok) return { ok: false, status: r.status, rows: [], total: 0 };
+    return { ok: true, ...(await r.json()) };
+  } catch (e) {
+    return { ok: false, unreachable: true, rows: [], total: 0, error: String(e && e.message || e).split('\n')[0] };
+  } finally { clearTimeout(timer); }
+}
+app.get('/api/residential-brokers', async (req, res) => {
+  const qs = contractorQs(req.query, ['state', 'city', 'q', 'firm', 'status', 'limit', 'offset']);
+  const j = await usreFetch('/api/brokers' + qs + (qs ? '&' : '?') + 'asset_class=residential');
+  res.set('Cache-Control', 'private, max-age=60');
+  res.json({ total: j.total || 0, rows: j.rows || [], upstream_down: j.ok === false });
+});
+app.get('/api/residential-firms', async (req, res) => {
+  const qs = contractorQs(req.query, ['state', 'city', 'q', 'limit', 'offset']);
+  const j = await usreFetch('/api/firms' + qs + (qs ? '&' : '?') + 'asset_class=residential');
+  res.set('Cache-Control', 'private, max-age=60');
+  res.json({ total: j.total || 0, rows: j.rows || [], upstream_down: j.ok === false });
+});
+
 // Listing/search proxy. On upstream-down: 200 with {count:0, results:[], upstream_down:true} so the
 // page renders its empty-state (the wiring is what matters) rather than throwing.
 app.get('/api/contractors', async (req, res) => {

← 8437e2e auto-data-snapshot: 2026-08-13T09:53:42 (2 data files) — pub  ·  back to Commercialrealestate  ·  auto-data-snapshot: 2026-08-13T12:26:31 (3 data files) — dat b83f4e8 →