[object Object]

← back to Re Coverage Dashboard

chore: lint (XSS esc fix), refactor (named consts, resilient milestone parse), v0.1.1 (session close)

2c9331abb3fdccbab0d0530bbf498686819180f5 · 2026-08-06 15:41:43 -0700 · Steve Abrams

Files touched

Diff

commit 2c9331abb3fdccbab0d0530bbf498686819180f5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 6 15:41:43 2026 -0700

    chore: lint (XSS esc fix), refactor (named consts, resilient milestone parse), v0.1.1 (session close)
---
 package.json      |  2 +-
 public/index.html | 11 ++++++++---
 server.js         | 10 +++++++---
 3 files changed, 16 insertions(+), 7 deletions(-)

diff --git a/package.json b/package.json
index 8262413..8af6358 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "re-coverage-dashboard",
-  "version": "0.1.0",
+  "version": "0.1.1",
   "private": true,
   "description": "Liquid-fill coverage dashboard for the $0/local RE agent contact-discovery (usre + CRCP) + win tracking.",
   "scripts": {
diff --git a/public/index.html b/public/index.html
index 435cd6f..eba7a9f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -39,6 +39,11 @@
 <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>
+// 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) {
@@ -92,11 +97,11 @@ function build(metrics) {
   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>`;
+    sec.innerHTML = `<h2>${esc(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>`;
+      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);
@@ -123,7 +128,7 @@ async function refresh() {
   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('');
+    `<div class="win"><span>🏆</span><span class="t">${esc(x.title)}</span><span class="when">${new Date(x.ts).toLocaleString()}</span></div>`).join('');
 }
 refresh(); setInterval(refresh, 15000);
 </script>
diff --git a/server.js b/server.js
index 34f2b3b..e969f83 100755
--- a/server.js
+++ b/server.js
@@ -17,6 +17,10 @@ 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) {
@@ -32,7 +36,7 @@ function coverage() {
   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;
+  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);
@@ -69,7 +73,7 @@ function coverage() {
 
 // "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)); }
+  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) {
@@ -87,7 +91,7 @@ function checkMilestones(t) {
   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(() => {});
+    for (const m of out) if (m.value >= CNCP_WIN_MIN_VALUE) postWin(m).catch(() => {});
   }
 }
 function postWin(m) {

← d5ccf43 add inline SVG favicon (route + link) — silences the /favico  ·  back to Re Coverage Dashboard  ·  href-drill: gauge counts + totals now href to /api/coverage ca21edb →