← back to Ticket System
Measure ticket runtime from latest status change
7445e60cf40f4db28f805a0919f90985859b50dc · 2026-08-31 01:55:51 -0700 · Steve Abrams
Files touched
M board.htmlM lib.jsM lib.test.js
Diff
commit 7445e60cf40f4db28f805a0919f90985859b50dc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 31 01:55:51 2026 -0700
Measure ticket runtime from latest status change
---
board.html | 2 +-
lib.js | 4 ++--
lib.test.js | 8 ++++++--
3 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/board.html b/board.html
index 5608d2e9..17c739ab 100644
--- a/board.html
+++ b/board.html
@@ -120,7 +120,7 @@ const $=s=>document.querySelector(s), esc=s=>String(s==null?'':s).replace(/[&<>"
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;
const elapsed=s=>{const ms=Math.max(0,Date.now()-new Date(s||Date.now()).getTime()),m=Math.floor(ms/60000),h=Math.floor(m/60),d=Math.floor(h/24);return d?`${d}d ${h%24}h`:h?`${h}h ${m%60}m`:`${m}m`;};
-const ticketCell=t=>{const short=esc(t.id.replace(/^(TK-\d+).*/,'$1')),age=elapsed(t.created_at),running=t.status==='doing';return `<span class="ticket-top"><a class="cell mono" href="#" onclick="setQ('${esc(t.id)}');return false" title="${esc(t.id)}">${short}</a><span class="ticket-runtime${running?' running':''}" title="since ${esc(t.created_at||'unknown')}">${running?'running':'age'} ${age}</span></span>`;};
+const ticketCell=t=>{const short=esc(t.id.replace(/^(TK-\d+).*/,'$1')),running=t.status==='doing',since=running?(t.status_since||t.created_at):t.created_at,age=elapsed(since);return `<span class="ticket-top"><a class="cell mono" href="#" onclick="setQ('${esc(t.id)}');return false" title="${esc(t.id)}">${short}</a><span class="ticket-runtime${running?' running':''}" title="since ${esc(since||'unknown')}">${running?'running':'age'} ${age}</span></span>`;};
// ── DTD run-now verdicts (fetched from /api/dtd): { TK-id: {verdict,yes,no,confidence} } ──
let DTD={running:false,verdicts:null};
const verdictOf=t=>{const v=DTD.verdicts&&DTD.verdicts.tickets&&DTD.verdicts.tickets[t.id];return v||null;};
diff --git a/lib.js b/lib.js
index 3fd110ef..8cc4b717 100644
--- a/lib.js
+++ b/lib.js
@@ -44,14 +44,14 @@ function tickets() {
const map = new Map();
for (const ev of readEvents()) {
if (ev.type === 'create') {
- map.set(ev.id, { id: ev.id, title: ev.title, project: ev.project || '', agent: ev.agent || '', assignee: ev.agent || '', status: 'open', created_at: ev.ts, updated_at: ev.ts, comments: [], actions: [] });
+ map.set(ev.id, { id: ev.id, title: ev.title, project: ev.project || '', agent: ev.agent || '', assignee: ev.agent || '', status: 'open', status_since: ev.ts, created_at: ev.ts, updated_at: ev.ts, comments: [], actions: [] });
if (ev.body) map.get(ev.id).comments.push({ ts: ev.ts, agent: ev.agent || '', kind: 'comment', text: ev.body });
} else {
const t = map.get(ev.id); if (!t) continue; t.updated_at = ev.ts;
if (ev.correlation_id) t.last_correlation_id = ev.correlation_id;
if (ev.type === 'comment') t.comments.push({ ts: ev.ts, agent: ev.agent || '', kind: ev.kind || 'comment', text: ev.text, correlation_id: ev.correlation_id || '' });
else if (ev.type === 'action') t.actions.push({ ts: ev.ts, agent: ev.agent || '', text: ev.text, correlation_id: ev.correlation_id || '' });
- else if (ev.type === 'status' && STATUSES.includes(ev.status)) t.status = ev.status;
+ else if (ev.type === 'status' && STATUSES.includes(ev.status)) { t.status = ev.status; t.status_since = ev.ts; }
else if (ev.type === 'assign') t.assignee = ev.agent || '';
}
}
diff --git a/lib.test.js b/lib.test.js
index 3e50ee38..79eafd8d 100644
--- a/lib.test.js
+++ b/lib.test.js
@@ -43,10 +43,14 @@ test('append + read round-trip and create folds to {open,title,project,agent}',
// ── 2. status transitions, reopen, unknown-status ignored, STATUSES contract ──
test('status transitions open→doing→blocked→done→stopped, then reopen', () => {
const id = 'TK-10002-transitions';
- append({ ts: iso(), type: 'create', id, title: 'Transitions', agent: 'a' });
+ const createdAt = iso(-10_000);
+ append({ ts: createdAt, type: 'create', id, title: 'Transitions', agent: 'a' });
+ assert.equal(tickets().get(id).status_since, createdAt);
for (const s of ['doing', 'blocked', 'done', 'stopped']) {
- append({ ts: iso(), type: 'status', id, status: s });
+ const statusAt = iso();
+ append({ ts: statusAt, type: 'status', id, status: s });
assert.equal(tickets().get(id).status, s);
+ assert.equal(tickets().get(id).status_since, statusAt);
}
append({ ts: iso(), type: 'status', id, status: 'open' }); // reopen from stopped
assert.equal(tickets().get(id).status, 'open');
← 7992a04f Show ticket runtime beside ticket number
·
back to Ticket System
·
keep Codex ticket loop running overnight daa6624c →