← back to Re Coverage Dashboard

server.js

139 lines

#!/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');
// Known CRCP roster size — update when the source list grows.
const CRCP_TOTAL_AGENTS = 2022;
// Minimum milestone value that also fires a CNCP win POST.
const CNCP_WIN_MIN_VALUE = 50;
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 = CRCP_TOTAL_AGENTS;
  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 {}

  // HomesOnSpec (builder-direct spec homes — Mac2 local DB). Builder = the firm here,
  // so coverage is naturally high (spec homes carry a listing URL + builder site).
  const h = nums(psql1('homesonspec', `SELECT (SELECT count(*) FROM "Builder"), (SELECT count(*) FROM "Builder" WHERE "websiteUrl" IS NOT NULL AND "websiteUrl"<>''), (SELECT count(*) FROM "InventoryHome" WHERE status='PUBLISHED'), (SELECT count(*) FROM "InventoryHome" WHERE status='PUBLISHED' AND "sourceUrl" IS NOT NULL), (SELECT count(*) FROM "Community"), (SELECT count(*) FROM "Community" WHERE "salesPhone" IS NOT NULL)`));
  const [hBTot, hBSite, hHomes, hHomeUrl, hCTot, hCPhone] = h.length === 6 ? h : [0, 0, 0, 0, 0, 0];

  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' },
    { key: 'hos_buildersite', group: 'HomesOnSpec', label: 'Builders with a website', pct: pct(hBSite, hBTot), num: hBSite, den: hBTot, unit: 'builders' },
    { key: 'hos_listing', group: 'HomesOnSpec', label: 'Homes with a listing link', pct: pct(hHomeUrl, hHomes), num: hHomeUrl, den: hHomes, unit: 'homes' },
    { key: 'hos_phone', group: 'HomesOnSpec', label: 'Communities with a phone', pct: pct(hCPhone, hCTot), num: hCPhone, den: hCTot, unit: 'communities' },
  ];
  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).flatMap(l => { try { return [JSON.parse(l)]; } catch { return []; } }); }
  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 >= CNCP_WIN_MIN_VALUE) 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();
  });
}

// Whole-site Basic Auth — required before any public (agentabrams.com) exposure so the
// dashboard + discovery internals aren't world-readable. /healthz stays open for the
// CF-tunnel health probe. Creds env-overridable (COV_USER/COV_PASS); house default.
const AUTH = 'Basic ' + Buffer.from((process.env.COV_USER || 'admin') + ':' + (process.env.COV_PASS || 'DW2024!')).toString('base64');
const server = http.createServer((req, res) => {
  if (req.url === '/healthz') { res.end('ok'); return; }
  if (req.url === '/favicon.ico') {  // inline SVG favicon — no 404, no auth needed
    res.setHeader('Content-Type', 'image/svg+xml');
    res.end("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' rx='3' fill='#0e1116'/><text x='8' y='12' font-size='11' text-anchor='middle'>🌊</text></svg>");
    return;
  }
  if ((req.headers.authorization || '') !== AUTH) {
    res.statusCode = 401; res.setHeader('WWW-Authenticate', 'Basic realm="RE Coverage", charset="UTF-8"');
    res.end('Authentication required'); 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()) {
    if (file.endsWith('.html')) {
      // Inject env-driven GA4 + FB-Pixel ids (per the DW build standard). Empty ids
      // = the loaders no-op; set GA4_ID / FB_PIXEL_ID in the launchd plist env to arm.
      let html = fs.readFileSync(fp, 'utf8');
      const cfg = `<script>window.__A={ga4:${JSON.stringify(process.env.GA4_ID || '')},fb:${JSON.stringify(process.env.FB_PIXEL_ID || '')}};</script>`;
      html = html.replace('</head>', cfg + '</head>');
      res.setHeader('Content-Type', 'text/html'); res.end(html); return;
    }
    res.setHeader('Content-Type', '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}`));