← 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
M public/brokers.htmlM src/enrich/broker_website_discovery.tsM src/server/index.ts
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, '&').replace(/</g, '<').replace(/"/g, '"');
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(' · ') || '<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 →