← 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
M board.htmlM dtd-run.jsM morning-digest.jsM server.js
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=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+const $=s=>document.querySelector(s), esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 →