← back to Re Coverage Dashboard

public/index.html

195 lines

<!doctype html>
<html lang="en">
<head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
  var _fetch = window.fetch.bind(window);
  var cleanBase = function () { return location.origin + location.pathname; };
  window.fetch = function (input, init) {
    try {
      if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
        input = new URL(input, cleanBase()).href;
      } else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
        input = new Request(new URL(input.url, cleanBase()).href, input);
      }
    } catch (_) { /* fall through to native */ }
    return _fetch(input, init);
  };
})();
</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RE Contact Coverage — liquid progress</title>
<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='%230e1116'/%3E%3Ctext x='8' y='12' font-size='11' text-anchor='middle'%3E%F0%9F%8C%8A%3C/text%3E%3C/svg%3E">
<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>
  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
  <script src="/drill.js"></script>
</head>
<body>
<input type="hidden" id="q" value=""><input type="hidden" id="pattern" value="">
<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>
// href-drill primitives (TK-10903)
var FIELDS=[['group','Group']];
var FIL={group:''};
var $=function(s){return document.querySelector(s);};
function applyFilters(push){if(push)writeURL(true);built=false;refresh();}

// XSS-safe HTML escaper — used on any server-sourced string interpolated into innerHTML.
function esc(s) {
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}

// ── 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', HomesOnSpec: '#d29922' };
const gauges = {};
var built = false;  // var so applyFilters (drill) can reset it

function build(metrics) {
  const filtered = FIL.group ? metrics.filter(m=>m.group===FIL.group) : metrics;
  const groups = {};
  filtered.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>${drill('group',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_${esc(m.key)}"></canvas><div class="lab">${esc(m.label)}</div><div class="cnt" id="c_${esc(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.innerHTML = `<a href="/api/coverage" style="color:inherit;text-decoration:none">${(m.num || 0).toLocaleString()} / ${(m.den || 0).toLocaleString()} ${esc(m.unit || '')}</a>`;
  });
  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"><a href="/api/coverage" style="color:inherit;text-decoration:none"><b>${(v || 0).toLocaleString()}</b></a><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">${esc(x.title)}</span><span class="when">${new Date(x.ts).toLocaleString()}</span></div>`).join('');
}
readURL();
refresh(); setInterval(refresh, 15000);
</script>
<!-- Own GA4 + FB Pixel (DW build standard). Ids come from server-injected window.__A
     (GA4_ID / FB_PIXEL_ID env). Both no-op until their id is set — no tracking fires blank. -->
<script>
(function(){ var A=window.__A||{};
  if (A.ga4) { var g=document.createElement('script'); g.async=true;
    g.src='https://www.googletagmanager.com/gtag/js?id='+A.ga4; document.head.appendChild(g);
    window.dataLayer=window.dataLayer||[]; function gtag(){dataLayer.push(arguments);}
    window.gtag=gtag; gtag('js', new Date()); gtag('config', A.ga4); }
  if (A.fb) { !function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
    n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;
    n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
    t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}
    (window,document,'script','https://connect.facebook.net/en_US/fbevents.js');
    fbq('init', A.fb); fbq('track', 'PageView'); }
})();
</script>
<!-- DW-network ad slot (per the DW build standard): renders a "Sponsored" unit;
     every ad link is rel="nofollow sponsored noopener". Fail-open — no fill = nothing. -->
<script src="https://ads.agentabrams.com/embed.js"
        data-slot="footer-strip"
        data-site="coverage.agentabrams.com"
        data-context="real-estate,analytics,agents,brokers"></script>
  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>