[object Object]

← back to Nationalrealestate

usre render: every agent shows firm tie + phone/firm-site/agent-own-site (or $0 on-demand find); /api/brokers returns contact fields; POST /api/broker/:id/discover lazy path; firm-less agents now expand

f1b3540dbf32262b707764299409d51a48f019a1 · 2026-08-06 11:01:04 -0700 · Steve Abrams

Files touched

Diff

commit f1b3540dbf32262b707764299409d51a48f019a1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 6 11:01:04 2026 -0700

    usre render: every agent shows firm tie + phone/firm-site/agent-own-site (or $0 on-demand find); /api/brokers returns contact fields; POST /api/broker/:id/discover lazy path; firm-less agents now expand
---
 public/brokers.html                    | 63 +++++++++++++++++++++++++++-------
 src/enrich/broker_website_discovery.ts | 16 ++++++---
 src/server/index.ts                    | 22 +++++++++++-
 3 files changed, 82 insertions(+), 19 deletions(-)

diff --git a/public/brokers.html b/public/brokers.html
index 74ba0ce..b8716d8 100644
--- a/public/brokers.html
+++ b/public/brokers.html
@@ -174,6 +174,39 @@ async function loadPage() {
 const esc = s => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
 const firmCache = new Map();
 
+// Every displayed agent ties to a broker/firm and shows contact links:
+// 📞 phone · 🏢 firm site · 👤 agent's own site (or an on-demand "find" button).
+function agentContactHtml(row) {
+  const firm = row.firm_name
+    ? `<span class="fd-pill">${esc(row.firm_name)}</span>`
+    : `<span class="fd-muted">firm not linked</span>`;
+  const bits = [];
+  if (row.broker_phone) bits.push(`<a href="tel:${esc(row.broker_phone)}">📞 ${esc(row.broker_phone)}</a>`);
+  if (row.firm_website) bits.push(`<a href="${esc(row.firm_website)}" target="_blank" rel="noopener noreferrer">🏢 firm site ↗</a>`);
+  if (row.broker_website) bits.push(`<a href="${esc(row.broker_website)}" target="_blank" rel="noopener noreferrer">👤 agent site ↗</a>`);
+  else if (row.website_status === 'no_url') bits.push(`<span class="fd-muted">👤 no personal site</span>`);
+  else bits.push(`<button data-bid="${row.id}" onclick="findAgentSite(${row.id}, this)" style="background:none;border:1px solid currentColor;border-radius:4px;padding:1px 7px;cursor:pointer;font:inherit;color:inherit;opacity:.75">🔎 find agent's site</button>`);
+  const contact = bits.join(' &nbsp;·&nbsp; ') || '<span class="fd-muted">no contact on file</span>';
+  return `<div class="usre-fd">
+    <h3>${esc(row.name)} <span class="fd-pill">${esc(row.license_state)}</span>
+      ${row.license_status ? `<span class="fd-pill">${esc(row.license_status)}</span>` : ''} ${firm}</h3>
+    <div style="margin-top:6px;line-height:2">${contact}</div>
+  </div>`;
+}
+
+// $0 on-demand lazy discovery — fires only for the agent whose record is open.
+async function findAgentSite(bid, btn) {
+  btn.disabled = true; const orig = btn.textContent; btn.textContent = '🔎 searching…';
+  try {
+    const r = await fetch('/api/broker/' + bid + '/discover', { method: 'POST' }).then(r => r.json());
+    if (r.status === 'found' && r.website)
+      btn.outerHTML = `<a href="${esc(r.website)}" target="_blank" rel="noopener noreferrer">👤 agent site ↗</a>`;
+    else if (r.status === 'throttled') { btn.disabled = false; btn.textContent = '🔎 search busy — retry'; }
+    else btn.outerHTML = '<span class="fd-muted">👤 no personal site</span>';
+  } catch (e) { btn.disabled = false; btn.textContent = orig; }
+}
+window.findAgentSite = findAgentSite;
+
 function firmDetailHtml(d) {
   const f = d.firm, s = d.site;
   const idxPill = s && s.has_idx_listings != null
@@ -201,35 +234,39 @@ function firmDetailHtml(d) {
 }
 
 async function toggleFirmDetail(row, el) {
-  if (!row || !row.firm_id || !el) return;
-  // Already open under this element? Close it.
+  if (!row || !el) return;
+  // Keyed by broker id (not firm_id) so firm-less agents expand too, and two
+  // agents at the same firm each open their own agent-contact block.
+  const key = String(row.id);
   const next = el.nextElementSibling;
-  if (next && next.dataset && next.dataset.fdFor === String(row.firm_id)) { next.remove(); return; }
+  if (next && next.dataset && next.dataset.fdFor === key) { next.remove(); return; }
   document.querySelectorAll('[data-fd-for]').forEach(x => x.remove());
-  let holder;
+  const agentBlock = agentContactHtml(row);
+  const firmPlaceholder = row.firm_id ? '<div class="usre-fd fd-muted">loading firm…</div>' : '';
+  let holder, target;
   if (el.tagName === 'TR') {
     holder = document.createElement('tr');
-    holder.dataset.fdFor = row.firm_id;
+    holder.dataset.fdFor = key;
     const td = document.createElement('td');
     td.colSpan = el.children.length;
-    td.innerHTML = '<div class="usre-fd fd-muted">loading firm…</div>';
+    td.innerHTML = agentBlock + firmPlaceholder;
     holder.appendChild(td);
+    target = td;
   } else {
     holder = document.createElement('div');
-    holder.dataset.fdFor = row.firm_id;
+    holder.dataset.fdFor = key;
     holder.style.gridColumn = '1 / -1';
-    holder.innerHTML = '<div class="usre-fd fd-muted">loading firm…</div>';
+    holder.innerHTML = agentBlock + firmPlaceholder;
+    target = holder;
   }
   el.after(holder);
+  if (!row.firm_id) return; // firm-less agent → agent-contact block only
   try {
     let d = firmCache.get(row.firm_id);
     if (!d) { d = await fetch('/api/firm/' + row.firm_id).then(r => r.json()); firmCache.set(row.firm_id, d); }
-    const target = holder.tagName === 'TR' ? holder.firstElementChild : holder;
-    if (d.error) target.innerHTML = `<div class="usre-fd fd-muted">${esc(d.error)}</div>`;
-    else target.innerHTML = firmDetailHtml(d);
+    target.innerHTML = agentBlock + (d.error ? `<div class="usre-fd fd-muted">${esc(d.error)}</div>` : firmDetailHtml(d));
   } catch (e) {
-    const target = holder.tagName === 'TR' ? holder.firstElementChild : holder;
-    target.innerHTML = '<div class="usre-fd fd-muted">failed to load firm detail</div>';
+    target.innerHTML = agentBlock + '<div class="usre-fd fd-muted">failed to load firm detail</div>';
   }
 }
 
diff --git a/src/enrich/broker_website_discovery.ts b/src/enrich/broker_website_discovery.ts
index 2f2a6dd..bb05e68 100644
--- a/src/enrich/broker_website_discovery.ts
+++ b/src/enrich/broker_website_discovery.ts
@@ -291,8 +291,14 @@ async function main() {
   await pool.end();
 }
 
-main().catch(async (e) => {
-  console.error('[broker-discover]', e);
-  try { await pool.end(); } catch {}
-  process.exit(1);
-});
+// Only run the batch when executed directly (`tsx …/broker_website_discovery.ts`),
+// NOT when the server imports { discoverOne } for the on-demand lazy path.
+import { pathToFileURL } from 'node:url';
+const isEntry = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
+if (isEntry) {
+  main().catch(async (e) => {
+    console.error('[broker-discover]', e);
+    try { await pool.end(); } catch {}
+    process.exit(1);
+  });
+}
diff --git a/src/server/index.ts b/src/server/index.ts
index f10bd5e..076f9ac 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -352,8 +352,12 @@ app.get('/api/brokers', async (req, res) => {
     const r = await query(
       `SELECT b.id, b.name, b.license_no, b.license_state, b.license_type, b.license_status,
               b.city, b.state_code, b.source, f.name AS firm_name, f.id AS firm_id,
+              b.phone AS broker_phone, b.website AS broker_website, b.website_status,
+              COALESCE(fs.url, f.website) AS firm_website,
               COUNT(*) OVER()::int AS total
-         FROM broker b LEFT JOIN firm f ON f.id = b.firm_id
+         FROM broker b
+         LEFT JOIN firm f ON f.id = b.firm_id
+         LEFT JOIN firm_site fs ON fs.firm_id = f.id
         ${where}
         ORDER BY b.name, b.id
         LIMIT $${params.length - 1} OFFSET $${params.length}`,
@@ -458,6 +462,22 @@ app.get('/api/firm/:id', async (req, res) => {
   }
 });
 
+// On-demand agent-website discovery — the $0 lazy path the broker registry fires
+// when a specific agent record is opened and website_status is still NULL. One free
+// SERP query + local-LLM identity gate; a throttle returns {status:'throttled'} so
+// the UI can say "check back" without poisoning the record. Never sweeps.
+app.post('/api/broker/:id/discover', async (req, res) => {
+  try {
+    const id = Number(req.params.id);
+    if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ error: 'bad broker id' });
+    const { discoverOne } = await import('../enrich/broker_website_discovery.ts');
+    const out = await discoverOne(id);
+    res.json(out);
+  } catch (e: any) {
+    res.status(500).json({ error: String(e.message || e) });
+  }
+});
+
 // CSV export of the ranked markets table (analyst pull) — reuses the same
 // /api/markets cache-backed dataset via an internal fetch so the shape never drifts.
 app.get('/api/markets.csv', async (req, res) => {

← 52035aa usre: on-demand discoverOne returns graceful throttled statu  ·  back to Nationalrealestate  ·  TK-16: add Lane County OR (41039) free priced-deed feed — 4t d503d96 →