← back to Re Coverage Dashboard
re-coverage-dashboard: liquid-fill gauges for $0/local RE contact-discovery coverage (usre+CRCP) + milestone/win tracking + fill.sh filler loop
7751daae72109cb3fae33b00cc95866cfb43c6fc · 2026-08-06 12:04:26 -0700 · Steve Abrams
Files touched
A .gitignoreA fill.shA package.jsonA public/index.htmlA server.js
Diff
commit 7751daae72109cb3fae33b00cc95866cfb43c6fc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 6 12:04:26 2026 -0700
re-coverage-dashboard: liquid-fill gauges for $0/local RE contact-discovery coverage (usre+CRCP) + milestone/win tracking + fill.sh filler loop
---
.gitignore | 5 +++
fill.sh | 21 +++++++++
package.json | 10 +++++
public/index.html | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
server.js | 105 +++++++++++++++++++++++++++++++++++++++++++
5 files changed, 271 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..29070bc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+.env*
+*.log
+data/
+.DS_Store
diff --git a/fill.sh b/fill.sh
new file mode 100755
index 0000000..a4d6036
--- /dev/null
+++ b/fill.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+# $0/local contact-coverage filler. Alternates CRCP + usre agent-site discovery in
+# SMALL batches with long cooldowns so the free SERP doesn't stay bot-walled (it
+# throttle-walls after ~9 rapid queries). Runs forever; fully resumable — each tool
+# skips already-attempted rows, so a kill/restart just continues. No metered spend.
+set -uo pipefail
+export OLLAMA_MODEL=${OLLAMA_MODEL:-qwen3:14b}
+CRCP="$HOME/Projects/commercialrealestate"
+USRE="$HOME/Projects/nationalrealestate"
+COOLDOWN=${COOLDOWN:-420} # 7 min between batches — lets DDG/Brave cool down
+BATCH=${BATCH:-8} # small: stops just under the ~9-query throttle wall
+
+echo "[filler] starting — batch=$BATCH cooldown=${COOLDOWN}s model=$OLLAMA_MODEL ($0/local)"
+while true; do
+ echo "[$(date '+%F %H:%M:%S')] CRCP batch"
+ ( cd "$CRCP" && node scripts/discover-agent-sites.js --limit="$BATCH" ) 2>&1 | grep -E '✓|done|throttl' | tail -4 || true
+ sleep "$COOLDOWN"
+ echo "[$(date '+%F %H:%M:%S')] usre batch"
+ ( cd "$USRE" && npx tsx src/enrich/broker_website_discovery.ts --limit="$BATCH" ) 2>&1 | grep -E '✓|done|throttl' | tail -4 || true
+ sleep "$COOLDOWN"
+done
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8262413
--- /dev/null
+++ b/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "re-coverage-dashboard",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Liquid-fill coverage dashboard for the $0/local RE agent contact-discovery (usre + CRCP) + win tracking.",
+ "scripts": {
+ "start": "node server.js",
+ "fill": "bash fill.sh"
+ }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..3a24062
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,130 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>RE Contact Coverage — liquid progress</title>
+<style>
+ :root { --bg:#0e1116; --card:#161b22; --line:#232a33; --ink:#e6edf3; --mut:#8b98a6; --acc:#3fb950; --blue:#58a6ff; }
+ * { box-sizing:border-box; }
+ body { margin:0; background:var(--bg); color:var(--ink); font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; }
+ header { padding:20px 24px 8px; }
+ h1 { font-size:20px; margin:0; font-weight:650; letter-spacing:.2px; }
+ .sub { color:var(--mut); font-size:12px; margin-top:3px; }
+ .totals { display:flex; gap:22px; margin-top:12px; flex-wrap:wrap; }
+ .tot { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:8px 14px; }
+ .tot b { font-size:19px; color:var(--acc); } .tot .l { font-size:11px; color:var(--mut); display:block; }
+ .grp { padding:6px 24px 0; }
+ .grp h2 { font-size:12px; text-transform:uppercase; letter-spacing:1px; color:var(--mut); margin:16px 0 4px; }
+ .grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:14px; }
+ .card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px; text-align:center; }
+ .card canvas { width:130px; height:130px; }
+ .card .lab { font-size:12.5px; font-weight:600; margin-top:6px; }
+ .card .cnt { font-size:11px; color:var(--mut); margin-top:2px; }
+ .wins { padding:8px 24px 30px; }
+ .wins h2 { font-size:12px; text-transform:uppercase; letter-spacing:1px; color:var(--mut); margin:18px 0 8px; }
+ .win { display:flex; gap:10px; align-items:center; background:var(--card); border:1px solid var(--line); border-left:3px solid var(--acc); border-radius:8px; padding:8px 12px; margin-bottom:6px; }
+ .win .t { flex:1; } .win .when { font-size:11px; color:var(--mut); }
+ .empty { color:var(--mut); font-size:13px; padding:8px 0; }
+</style>
+</head>
+<body>
+<header>
+ <h1>🌊 RE Contact Coverage</h1>
+ <div class="sub">Filling in phone · firm · site · agent-site across usre + CRCP — $0/local discovery. <span id="upd"></span></div>
+ <div class="totals" id="totals"></div>
+</header>
+<div id="groups"></div>
+<div class="wins"><h2>🏆 Wins / milestones</h2><div id="winlist"><div class="empty">no milestones yet — they appear as coverage crosses thresholds</div></div></div>
+
+<script>
+// ── Liquid-fill canvas gauge: a clipped circle with an animated sine-wave surface. ──
+class LiquidGauge {
+ constructor(canvas, color) {
+ this.c = canvas; this.ctx = canvas.getContext('2d');
+ const dpr = window.devicePixelRatio || 1; this.s = 130;
+ canvas.width = this.s * dpr; canvas.height = this.s * dpr; this.ctx.scale(dpr, dpr);
+ this.color = color; this.level = 0; this.target = 0; this.phase = 0;
+ requestAnimationFrame(() => this.draw());
+ }
+ setTarget(pct) { this.target = Math.max(0, Math.min(100, pct)); }
+ draw() {
+ const { ctx, s } = this, r = s / 2 - 6, cx = s / 2, cy = s / 2;
+ this.level += (this.target - this.level) * 0.06; // ease toward target
+ this.phase += 0.045;
+ ctx.clearRect(0, 0, s, s);
+ // ring
+ ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2);
+ ctx.strokeStyle = 'rgba(255,255,255,.10)'; ctx.lineWidth = 3; ctx.stroke();
+ // water clipped to circle
+ ctx.save();
+ ctx.beginPath(); ctx.arc(cx, cy, r - 1, 0, Math.PI * 2); ctx.clip();
+ const waterY = cy + r - (this.level / 100) * (2 * r);
+ const amp = 4 + (this.level > 1 && this.level < 99 ? 2 : 0);
+ for (let k = 0; k < 2; k++) {
+ ctx.beginPath();
+ ctx.moveTo(cx - r, s);
+ for (let x = -r; x <= r; x += 4) {
+ const y = waterY + Math.sin((x / r) * Math.PI * 2 + this.phase + k * Math.PI) * amp;
+ ctx.lineTo(cx + x, y);
+ }
+ ctx.lineTo(cx + r, s); ctx.closePath();
+ ctx.fillStyle = k === 0 ? this.color : this.color + '99';
+ ctx.fill();
+ }
+ ctx.restore();
+ // pct text
+ ctx.fillStyle = '#e6edf3'; ctx.font = '700 26px -apple-system,sans-serif';
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
+ ctx.fillText(this.level.toFixed(this.target < 10 ? 1 : 0) + '%', cx, cy);
+ requestAnimationFrame(() => this.draw());
+ }
+}
+
+const COLORS = { usre: '#3fb950', CRCP: '#58a6ff' };
+const gauges = {};
+let built = false;
+
+function build(metrics) {
+ const groups = {};
+ metrics.forEach(m => { (groups[m.group] = groups[m.group] || []).push(m); });
+ const host = document.getElementById('groups'); host.innerHTML = '';
+ for (const [g, ms] of Object.entries(groups)) {
+ const sec = document.createElement('div'); sec.className = 'grp';
+ sec.innerHTML = `<h2>${g}</h2><div class="grid"></div>`;
+ const grid = sec.querySelector('.grid');
+ ms.forEach(m => {
+ const card = document.createElement('div'); card.className = 'card';
+ card.innerHTML = `<canvas id="g_${m.key}"></canvas><div class="lab">${m.label}</div><div class="cnt" id="c_${m.key}"></div>`;
+ grid.appendChild(card);
+ });
+ host.appendChild(sec);
+ ms.forEach(m => { gauges[m.key] = new LiquidGauge(document.getElementById('g_' + m.key), COLORS[g] || '#3fb950'); });
+ }
+ built = true;
+}
+
+async function refresh() {
+ const d = await fetch('/api/coverage').then(r => r.json()).catch(() => null);
+ if (!d) return;
+ if (!built) build(d.metrics);
+ d.metrics.forEach(m => {
+ gauges[m.key] && gauges[m.key].setTarget(m.pct);
+ const c = document.getElementById('c_' + m.key);
+ if (c) c.textContent = `${(m.num || 0).toLocaleString()} / ${(m.den || 0).toLocaleString()} ${m.unit}`;
+ });
+ const t = d.totals;
+ document.getElementById('totals').innerHTML = [
+ ['Sites found', t.sites_found], ['Phones found', t.phones_found],
+ ['Agents searched', t.searched], ['Firm-linked', t.firm_linked],
+ ].map(([l, v]) => `<div class="tot"><b>${(v || 0).toLocaleString()}</b><span class="l">${l}</span></div>`).join('');
+ document.getElementById('upd').textContent = 'updated ' + new Date(d.updated).toLocaleTimeString();
+ const w = await fetch('/api/wins').then(r => r.json()).catch(() => ({ wins: [] }));
+ const wl = document.getElementById('winlist');
+ if (w.wins && w.wins.length) wl.innerHTML = w.wins.map(x =>
+ `<div class="win"><span>🏆</span><span class="t">${x.title}</span><span class="when">${new Date(x.ts).toLocaleString()}</span></div>`).join('');
+}
+refresh(); setInterval(refresh, 15000);
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
new file mode 100755
index 0000000..6e722e2
--- /dev/null
+++ b/server.js
@@ -0,0 +1,105 @@
+#!/usr/bin/env node
+/**
+ * RE Contact-Coverage dashboard — liquid-fill gauges showing how the $0/local
+ * agent contact-discovery is filling in phone / firm / site / agent-site coverage
+ * across the RE builds (usre + CRCP), plus a wins/milestone feed.
+ *
+ * Zero deps: shells to psql for usre counts, reads CRCP data/agent-sites.json.
+ * Read-only. Milestones appended to data/milestones.jsonl as coverage crosses
+ * thresholds (the "track wins" ask); big ones also POST to CNCP.
+ */
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const PORT = process.env.PORT || 9792;
+const ROOT = __dirname;
+const CRCP_SITES = process.env.HOME + '/Projects/commercialrealestate/data/agent-sites.json';
+const MILESTONES = path.join(ROOT, 'data', 'milestones.jsonl');
+fs.mkdirSync(path.join(ROOT, 'data'), { recursive: true });
+
+function psql1(db, sql) {
+ try { return execSync(`psql -h /tmp ${db} -tAc ${JSON.stringify(sql)}`, { encoding: 'utf8', timeout: 8000 }).trim(); }
+ catch { return ''; }
+}
+const nums = s => s.split('|').map(x => parseInt(x, 10) || 0);
+
+function coverage() {
+ // usre broker + firm
+ const b = nums(psql1('usre', `SELECT count(*), count(*) FILTER (WHERE firm_id IS NOT NULL), count(website_status), count(website) FROM broker`));
+ const f = nums(psql1('usre', `SELECT count(*), (SELECT count(url) FROM firm_site) FROM firm`));
+ const [bTotal, bFirm, bAttempted, bSite] = b.length === 4 ? b : [0, 0, 0, 0];
+ const [fTotal, fSite] = f.length === 2 ? f : [0, 0];
+ // CRCP agent-sites
+ let cAttempted = 0, cFound = 0, cPhone = 0, cTotal = 2022;
+ try {
+ const s = JSON.parse(fs.readFileSync(CRCP_SITES, 'utf8'));
+ const v = Object.values(s);
+ cAttempted = v.length; cFound = v.filter(x => x.status === 'found').length; cPhone = v.filter(x => x.phone).length;
+ } catch {}
+
+ const pct = (n, d) => d > 0 ? Math.round((n / d) * 1000) / 10 : 0;
+ const metrics = [
+ { key: 'usre_firm', group: 'usre', label: 'Agents tied to a firm', pct: pct(bFirm, bTotal), num: bFirm, den: bTotal, unit: 'agents' },
+ { key: 'usre_firmsite', group: 'usre', label: 'Firms with a website', pct: pct(fSite, fTotal), num: fSite, den: fTotal, unit: 'firms' },
+ { key: 'usre_swept', group: 'usre', label: 'Agent sweep progress', pct: pct(bAttempted, bTotal), num: bAttempted, den: bTotal, unit: 'agents searched' },
+ { key: 'usre_agentsite', group: 'usre', label: 'Agent own-sites found', pct: pct(bSite, Math.max(bAttempted, 1)), num: bSite, den: bAttempted, unit: 'of searched' },
+ { key: 'crcp_swept', group: 'CRCP', label: 'CRCP agents searched', pct: pct(cAttempted, cTotal), num: cAttempted, den: cTotal, unit: 'agents' },
+ { key: 'crcp_site', group: 'CRCP', label: 'CRCP agent sites found', pct: pct(cFound, Math.max(cAttempted, 1)), num: cFound, den: cAttempted, unit: 'of searched' },
+ { key: 'crcp_phone', group: 'CRCP', label: 'CRCP phones found', pct: pct(cPhone, Math.max(cAttempted, 1)), num: cPhone, den: cAttempted, unit: 'of searched' },
+ ];
+ const totals = {
+ sites_found: bSite + cFound,
+ phones_found: cPhone,
+ searched: bAttempted + cAttempted,
+ firm_linked: bFirm,
+ };
+ checkMilestones(totals);
+ return { updated: new Date().toISOString(), metrics, totals };
+}
+
+// "track wins": append a milestone when a running total crosses a threshold.
+function readMilestones() {
+ try { return fs.readFileSync(MILESTONES, 'utf8').trim().split('\n').filter(Boolean).map(l => JSON.parse(l)); }
+ catch { return []; }
+}
+function checkMilestones(t) {
+ const done = new Set(readMilestones().map(m => m.id));
+ const bands = { sites_found: 10, phones_found: 5, searched: 100 };
+ const labels = { sites_found: 'agent/firm sites found', phones_found: 'phone numbers found', searched: 'agents searched' };
+ const out = [];
+ for (const [metric, step] of Object.entries(bands)) {
+ const crossed = Math.floor((t[metric] || 0) / step) * step;
+ if (crossed >= step) {
+ const id = `${metric}:${crossed}`;
+ if (!done.has(id)) out.push({ id, ts: new Date().toISOString(), metric, value: crossed, title: `${crossed.toLocaleString()} ${labels[metric]}` });
+ }
+ }
+ if (out.length) {
+ fs.appendFileSync(MILESTONES, out.map(m => JSON.stringify(m)).join('\n') + '\n');
+ // big round milestones also land as a CNCP win (best-effort, non-blocking)
+ for (const m of out) if (m.value >= 50) postWin(m).catch(() => {});
+ }
+}
+function postWin(m) {
+ const body = JSON.stringify({ project: 're-coverage', title: `RE contact coverage: ${m.title}`, summary: `$0/local discovery crossed ${m.title} across usre + CRCP.` });
+ return new Promise((res, rej) => {
+ const req = http.request('http://127.0.0.1:3333/api/wins', { method: 'POST', headers: { 'Content-Type': 'application/json' } }, r => { r.resume(); r.on('end', res); });
+ req.on('error', rej); req.write(body); req.end();
+ });
+}
+
+const server = http.createServer((req, res) => {
+ if (req.url === '/healthz') { res.end('ok'); return; }
+ if (req.url === '/api/coverage') { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(coverage())); return; }
+ if (req.url === '/api/wins') { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ wins: readMilestones().reverse() })); return; }
+ const file = req.url === '/' ? '/index.html' : req.url.split('?')[0];
+ const fp = path.join(ROOT, 'public', file);
+ if (fp.startsWith(path.join(ROOT, 'public')) && fs.existsSync(fp) && fs.statSync(fp).isFile()) {
+ res.setHeader('Content-Type', file.endsWith('.html') ? 'text/html' : 'text/plain');
+ res.end(fs.readFileSync(fp)); return;
+ }
+ res.statusCode = 404; res.end('not found');
+});
+server.listen(PORT, () => console.log(`RE coverage dashboard → http://127.0.0.1:${PORT}`));
(oldest)
·
back to Re Coverage Dashboard
·
add launchd plists (dashboard + filler) for reboot-durabilit c3a3a70 →