[object Object]

← 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

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">⚠ &gt;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">⚠ &gt;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 →