[object Object]

← back to Ticket System

chore: session-close quality gate — refactor (hoist resolveList/helpers to module scope, dead-code cleanup), harden board.html esc (single-quote), guard morning-digest against missing events file (no version field to bump)

f8c7ca9ff7484dcc3b4558f9ce79407d3cc55b1c · 2026-08-13 10:52:37 -0700 · Steve Abrams

Files touched

Diff

commit f8c7ca9ff7484dcc3b4558f9ce79407d3cc55b1c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 13 10:52:37 2026 -0700

    chore: session-close quality gate — refactor (hoist resolveList/helpers to module scope, dead-code cleanup), harden board.html esc (single-quote), guard morning-digest against missing events file (no version field to bump)
---
 board.html        |  2 +-
 dtd-run.js        |  5 ++---
 morning-digest.js |  2 ++
 server.js         | 18 +++++++++---------
 4 files changed, 14 insertions(+), 13 deletions(-)

diff --git a/board.html b/board.html
index 8b2f7efe..3bdb0130 100644
--- a/board.html
+++ b/board.html
@@ -101,7 +101,7 @@
 <div class="tblwrap"><table class="g"><thead><tr id="thead"></tr></thead><tbody id="tbody"></tbody></table></div>
 <div id="dmwrap"></div>
 <script>
-const $=s=>document.querySelector(s), esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+const $=s=>document.querySelector(s), esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
 const dt=s=>{if(!s)return '—';const d=new Date(s);if(isNaN(d))return '—';return '<span title="'+esc(new Date(s).toISOString())+'">'+esc(d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}))+'</span>';};
 const agoH=s=>s?((Date.now()-new Date(s).getTime())/3600000):null;
 // ── DTD run-now verdicts (fetched from /api/dtd): { TK-id: {verdict,yes,no,confidence} } ──
diff --git a/dtd-run.js b/dtd-run.js
index 88fc3984..98b02e88 100755
--- a/dtd-run.js
+++ b/dtd-run.js
@@ -109,9 +109,8 @@ Return ONLY a JSON array, nothing else, one object per ticket, exactly this shap
 
 TICKETS:
 ${digest}`;
-  let dir = null;
   const out = execFileSync('bash', [PANEL, QUESTION], { encoding: 'utf8', timeout: 360000, maxBuffer: 32 * 1024 * 1024 });
-  const m = out.match(/^DIR=(.+)$/m); dir = m ? m[1].trim() : null;
+  const m = out.match(/^DIR=(.+)$/m); const dir = m ? m[1].trim() : null;
   const perPanel = {};
   if (dir) for (const p of panelists) {
     try { perPanel[p] = parseVotes(fs.readFileSync(path.join(dir, `${p}.txt`), 'utf8')); } catch { perPanel[p] = new Map(); }
@@ -177,7 +176,7 @@ if (MERGE) {
     const prior = JSON.parse(fs.readFileSync(OUT, 'utf8'));
     result.tickets = Object.assign({}, prior.tickets || {}, result.tickets);
     result.count = Object.keys(result.tickets).length;
-    result.merged = (chosen || []).length;
+    result.merged = chosen.length;
     if (Array.isArray(prior.dirs)) result.dirs = [...prior.dirs, ...result.dirs].slice(-12);
   } catch (e) { /* no prior file — just write fresh */ }
 }
diff --git a/morning-digest.js b/morning-digest.js
index 2cf08c02..f249297c 100755
--- a/morning-digest.js
+++ b/morning-digest.js
@@ -27,6 +27,8 @@ if (overnightStart > now) overnightStart.setDate(overnightStart.getDate() - 1);
 const day24Start = new Date(now.getTime() - 24 * 3600 * 1000);
 
 const STATUSES = ['open', 'doing', 'blocked', 'done', 'stopped']; // 'stopped' = TicketStopped (kept in sync with lib.js)
+// Guard: a fresh/cleaned system may not have the events file yet — don't ENOENT-crash the cron digest.
+if (!fs.existsSync(EVENTS)) { process.stdout.write(JSON.stringify({ subject: '🎟️ Tickets: no data', html: '<p>No ticket data yet.</p>', counts: { overnight: 0, last24: 0, open: 0 } })); process.exit(0); }
 const events = fs.readFileSync(EVENTS, 'utf8').split('\n').filter(Boolean)
   .map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
 
diff --git a/server.js b/server.js
index e9e3d1f1..f72b1e92 100644
--- a/server.js
+++ b/server.js
@@ -12,7 +12,15 @@ const VERDICTS = path.join(DATA_DIR, 'dtd-verdicts.json');       // last batched
 const DTD_RUNNING = path.join(DATA_DIR, 'dtd-verdicts.running'); // present while a sweep is in flight
 const RUN_SH = path.join(__dirname, 'run-ticket.sh');           // opens an iTerm2 Claude session
 const DTD_RUN = path.join(__dirname, 'dtd-run.js');             // batched panel.sh sweep
-const IDRE = /^TK-[0-9]+(-[a-z0-9-]+)?$/;                        // hard id gate before any shell use
+const IDRE  = /^TK-[0-9]+(-[a-z0-9-]+)?$/;                        // hard id gate before any shell use
+// Resolve a list of client refs to canonical, shell-safe ticket ids. A RAW ref
+// must itself look like a clean ticket ref FIRST — otherwise "TK-1; rm -rf /"
+// would numeric-resolve to the real TK-1 (resolveId stops parseInt at the first
+// non-digit). Pre-gating the raw ref blocks that whole class before resolveId.
+const REFRE = /^(TK-?)?\d+(-[a-z0-9-]+)?$/i;
+const resolveList = (refs, map) => { const out = []; if (!Array.isArray(refs)) return out;
+  for (const r of refs) { if (!REFRE.test(String(r).trim())) continue; const id = resolveId(r, map); if (id && IDRE.test(id)) out.push(id); }
+  return [...new Set(out)]; };
 const json = (res, code, obj) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };
 function readJson(req, cb) { let b = ''; req.on('data', d => { b += d; if (b.length > 1e6) req.destroy(); }); req.on('end', () => { try { cb(JSON.parse(b || '{}')); } catch { cb(null); } }); }
 
@@ -116,14 +124,6 @@ http.createServer((req, res) => {
   if (req.headers.authorization !== AUTH) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="tickets"' }); return res.end('auth'); }
 
   // ── writes / actions (all auth-gated; ids resolved server-side from the real store) ──
-  // Resolve a list of client refs to canonical, shell-safe ticket ids. A RAW ref
-  // must itself look like a clean ticket ref FIRST — otherwise "TK-1; rm -rf /"
-  // would numeric-resolve to the real TK-1 (resolveId stops parseInt at the first
-  // non-digit). Pre-gating the raw ref blocks that whole class before resolveId.
-  const REFRE = /^(TK-?)?\d+(-[a-z0-9-]+)?$/i;
-  const resolveList = (refs, map) => { const out = []; if (!Array.isArray(refs)) return out;
-    for (const r of refs) { if (!REFRE.test(String(r).trim())) continue; const id = resolveId(r, map); if (id && IDRE.test(id)) out.push(id); }
-    return [...new Set(out)]; };
 
   // Run Now — open one iTerm2 Claude session per selected ticket (staggered so iTerm doesn't drop windows).
   if (req.method === 'POST' && req.url === '/api/run') {

← 1f899783 dtd-run: concurrency guard (refuse to clobber a live sweep)  ·  back to Ticket System  ·  auto-data-snapshot: 2026-08-17T23:34:22 (1 data files) — TK- 0eec50fe →