← back to Ticket Action Viewer
chore: lint (fix act() /^TK-\\d+/ regex bug) + refactor (LA_TZ/derive/STALE_MS consts) (session close)
83726732c15a06b8a1d8b789279bc5c3e88b3a46 · 2026-08-10 10:15:54 -0700 · Steve Abrams
Files touched
M public/index.htmlM server.js
Diff
commit 83726732c15a06b8a1d8b789279bc5c3e88b3a46
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 10 10:15:54 2026 -0700
chore: lint (fix act() /^TK-\\d+/ regex bug) + refactor (LA_TZ/derive/STALE_MS consts) (session close)
---
public/index.html | 7 ++++---
server.js | 18 +++++++++---------
2 files changed, 13 insertions(+), 12 deletions(-)
diff --git a/public/index.html b/public/index.html
index 2361967..de0358b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -66,6 +66,7 @@
<div id="modal"><div class="box"><span class="x" onclick="closeM()">×</span><h3 id="mtitle"></h3><pre id="mbody"></pre></div></div>
<div id="toast"></div>
<script>
+const STALE_MS=864e5; // 1 day in ms — timer turns red above this threshold
let ALL=[];
const el=id=>document.getElementById(id);
async function load(){
@@ -85,7 +86,7 @@ function render(){
<td class="rank">${i+1}</td>
<td><div class="tid">${t.shortId}</div><div class="ttl">${esc(t.title)}</div></td>
<td class="proj">${esc(t.project)}</td>
- <td><span class="st ${t.status}">${t.status==='doing'?'doing now':t.status}</span>${t.status==='doing'&&t.startedAt?`<div class="timer${(Date.now()-Date.parse(t.startedAt))>864e5?' stale':''}" data-start="${esc(t.startedAt)}"><span class="tdot"></span><span class="tval">⏱ …</span><span class="staleflag" title="Running over 1 day — review to close">⚠ >1d</span></div><div class="startedat" title="${esc(t.startedAt)}">▶ started ${fmtStart(t.startedAt)}</div>`:''}</td>
+ <td><span class="st ${t.status}">${t.status==='doing'?'doing now':t.status}</span>${t.status==='doing'&&t.startedAt?`<div class="timer${(Date.now()-Date.parse(t.startedAt))>STALE_MS?' stale':''}" data-start="${esc(t.startedAt)}"><span class="tdot"></span><span class="tval">⏱ …</span><span class="staleflag" title="Running over 1 day — review to close">⚠ >1d</span></div><div class="startedat" title="${esc(t.startedAt)}">▶ started ${fmtStart(t.startedAt)}</div>`:''}</td>
<td class="owner">${esc(t.owner)}</td>
<td class="next">${esc(t.nextStep)}${t.note?`<div class="proj" style="margin-top:4px">↳ ${esc(t.note)}</div>`:''}</td>
<td><div class="needs ${t.gated?'gated':''}">${t.gated?'🔒 ':''}${esc(t.needs||'—')}</div></td>
@@ -106,7 +107,7 @@ function fmtElapsed(ms){if(ms<0)ms=0;const s=Math.floor(ms/1000),d=Math.floor(s/
if(h)return h+':'+p(m)+':'+p(ss); // ≥1 hour: 3:04:07
return m+':'+p(ss);} // <1 hour: 4:07
function fmtStart(iso){try{return new Date(iso).toLocaleString([],{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}catch(e){return '';}}
-function tickTimers(){const now=Date.now();document.querySelectorAll('.timer[data-start]').forEach(elm=>{const st=Date.parse(elm.dataset.start);const v=elm.querySelector('.tval');if(v)v.textContent='⏱ '+fmtElapsed(now-st);elm.classList.toggle('stale',(now-st)>864e5);});}
+function tickTimers(){const now=Date.now();document.querySelectorAll('.timer[data-start]').forEach(elm=>{const st=Date.parse(elm.dataset.start);const v=elm.querySelector('.tval');if(v)v.textContent='⏱ '+fmtElapsed(now-st);elm.classList.toggle('stale',(now-st)>STALE_MS);});}
setInterval(tickTimers,1000);
async function detail(id,sid){el('mtitle').textContent=sid;el('mbody').textContent='loading…';el('modal').style.display='flex';
const r=await fetch('/api/ticket?id='+encodeURIComponent(id));const d=await r.json();el('mbody').textContent=d.out||'(no output)';}
@@ -116,7 +117,7 @@ async function runNow(id,title,project){
toast('opening iTerm2…');
const r=await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,title,project})});
const d=await r.json();toast(d.ok?('▶ '+d.out):('✗ '+d.out.slice(0,90)));if(d.ok)load();} // reload → row flips to "doing now" + timer starts
-async function act(id,action){if((action==='done'||action==='block')&&!confirm(`${action} ${id.match(/^TK-\\d+/)[0]}?`))return;
+async function act(id,action){if((action==='done'||action==='block')&&!confirm(`${action} ${id.match(/^TK-\d+/)[0]}?`))return;
const r=await fetch('/api/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,action})});
const d=await r.json();toast(d.ok?`✓ ${action} ok`:`✗ ${d.out.slice(0,80)}`);load();}
async function logIt(id){const text=prompt('Log note for '+id.match(/^TK-\d+/)[0]+':');if(!text)return;
diff --git a/server.js b/server.js
index 5cdf472..73f5fc3 100644
--- a/server.js
+++ b/server.js
@@ -53,6 +53,7 @@ function tk(args) {
}
const LINE = /^(TK-\S+)\s+\[(\w+)\]\s+\(([^)]*)\)\s+\{([^}]*)\}\s+(.*)$/;
+const LA_TZ = { timeZone: 'America/Los_Angeles', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' };
function loadAnalysis() { try { return JSON.parse(fs.readFileSync(ANALYSIS, 'utf8')); } catch { return {}; } }
// Derive a sensible default when we have no curated analysis for a ticket.
@@ -72,12 +73,13 @@ async function getTickets() {
if (!m) continue;
const [, fullId, status, owner, project, title] = m;
const shortId = (fullId.match(/^TK-\d+/) || [fullId])[0];
- const a = overlay[shortId] || derive({ status });
+ const d = derive({ status });
+ const a = overlay[shortId] || d;
tickets.push({ fullId, shortId, status, owner, project, title,
- priority: a.priority ?? derive({ status }).priority,
- nextStep: a.nextStep ?? derive({ status }).nextStep,
- gated: a.gated ?? derive({ status }).gated,
- needs: a.needs ?? derive({ status }).needs,
+ priority: a.priority ?? d.priority,
+ nextStep: a.nextStep ?? d.nextStep,
+ gated: a.gated ?? d.gated,
+ needs: a.needs ?? d.needs,
note: a.note || '', startedAt: null });
}
// Start time per doing ticket, by priority:
@@ -133,8 +135,7 @@ const server = http.createServer(async (req, res) => {
if (project && fs.existsSync(path.join(home, 'Projects', project))) dir = path.join(home, 'Projects', project);
// Reserve + timestamp IMMEDIATELY at click (before the new window boots) so the ticket
// is assigned to me and stamped the moment RUN NOW is pressed — no race, no double-work.
- const when = new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles',
- year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
+ const when = new Date().toLocaleString('en-US', LA_TZ);
const startedAt = stampStart(shortId); // start the live timer at the click, before the window boots
await tk(['take', p.id]); // tk take also flips status → doing (the "doing now" state)
await tk(['log', p.id, `▶ STARTED ${when} PT — RESERVED for ${AGENT} + launched in a new iTerm2 window (RUN NOW)`]);
@@ -164,8 +165,7 @@ const server = http.createServer(async (req, res) => {
if (action === 'take') {
const startedAt = stampStart(short); // start the live timer at the click
const r1 = await tk(['take', id]); // tk take also flips status → doing (the "doing now" state)
- const when = new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles',
- year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
+ const when = new Date().toLocaleString('en-US', LA_TZ);
const r2 = await tk(['log', id, `▶ STARTED ${when} PT — taken by ${AGENT} via Ticket Action Viewer`]);
return json(res, 200, { ok: r1.ok && r2.ok, out: r1.out + '\n' + r2.out, started: when, startedAt });
}
← 144ca64 Viewer: flag doing tickets running >1 day (red timer + ⚠ >1d
·
back to Ticket Action Viewer
·
Viewer: make all data columns sortable (clickable headers + c1736a2 →