← back to Ticket Action Viewer
Viewer: flag doing tickets running >1 day (red timer + ⚠ >1d) so stale long-runners self-surface for cleanup
144ca64252c43447169ecbdc06d87c9d18fdc08d · 2026-08-10 08:23:19 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 144ca64252c43447169ecbdc06d87c9d18fdc08d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 10 08:23:19 2026 -0700
Viewer: flag doing tickets running >1 day (red timer + ⚠ >1d) so stale long-runners self-surface for cleanup
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/index.html | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/public/index.html b/public/index.html
index 052f36c..2361967 100644
--- a/public/index.html
+++ b/public/index.html
@@ -27,6 +27,11 @@
.timer .tdot{width:7px;height:7px;border-radius:50%;background:var(--warn);box-shadow:0 0 0 0 var(--warn);animation:tpulse 1.6s ease-out infinite}
@keyframes tpulse{0%{box-shadow:0 0 0 0 #f2b23a88}70%{box-shadow:0 0 0 6px #f2b23a00}100%{box-shadow:0 0 0 0 #f2b23a00}}
.startedat{margin-top:3px;color:var(--mut);font-size:10px;white-space:nowrap}
+ .timer .staleflag{display:none}
+ .timer.stale{color:var(--gate)}
+ .timer.stale .tdot{background:var(--gate);box-shadow:0 0 0 0 var(--gate);animation:spulse 1.6s ease-out infinite}
+ .timer.stale .staleflag{display:inline;color:var(--gate);background:#2a1512;border:1px solid #4a2620;border-radius:5px;padding:1px 5px;font-size:10px;font-weight:700}
+ @keyframes spulse{0%{box-shadow:0 0 0 0 #ff6b5a88}70%{box-shadow:0 0 0 6px #ff6b5a00}100%{box-shadow:0 0 0 0 #ff6b5a00}}
.owner{color:var(--mut);font-size:12px}
.next{color:#cfd6e2;max-width:340px}
.needs{max-width:320px;color:var(--mut);font-size:12px}
@@ -80,7 +85,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" data-start="${esc(t.startedAt)}"><span class="tdot"></span><span class="tval">⏱ …</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))>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 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>
@@ -101,7 +106,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);});}
+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);});}
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)';}
← 06752db Viewer: start stamp shows date + time (was time-only); full
·
back to Ticket Action Viewer
·
chore: lint (fix act() /^TK-\\d+/ regex bug) + refactor (LA_ 8372673 →