← 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
M package.jsonM public/index.htmlM server.js
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
+}
+
// ── 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 →