← back to Ticket System
board.html: persist expanded detail rows across 30s refresh + complete Reset (DTD verdict A)
58802d482929d6bf58ff4717e933fc338cde80e9 · 2026-09-10 14:22:03 -0700 · Steve Abrams
#2: expanded ticket-detail rows were DOM-injected on click and wiped every
30s by the auto-refresh render(); track ids in an EXPANDED set and re-open
after render (same pattern as SEL), drop stale ids in load().
#3: 'Reset all view settings' omitted tkSection/tkLayout/tkSectionOrder/
tkCatalogSort/tkCatalogDir/tkRunProfile(+V2) — added so reset is truly all.
Verified: inline JS parses clean, board serves 200. a11y pass (#1) queued
separately per the DTD dissent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 58802d482929d6bf58ff4717e933fc338cde80e9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 10 14:22:03 2026 -0700
board.html: persist expanded detail rows across 30s refresh + complete Reset (DTD verdict A)
#2: expanded ticket-detail rows were DOM-injected on click and wiped every
30s by the auto-refresh render(); track ids in an EXPANDED set and re-open
after render (same pattern as SEL), drop stale ids in load().
#3: 'Reset all view settings' omitted tkSection/tkLayout/tkSectionOrder/
tkCatalogSort/tkCatalogDir/tkRunProfile(+V2) — added so reset is truly all.
Verified: inline JS parses clean, board serves 200. a11y pass (#1) queued
separately per the DTD dissent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
board.html | 19 ++++++++++++-------
1 file changed, 12 insertions(+), 7 deletions(-)
diff --git a/board.html b/board.html
index 21d9e82a..7a4fdc82 100644
--- a/board.html
+++ b/board.html
@@ -221,6 +221,7 @@ let WID=LS('tkWid',{}), DENS=LS('tkDens',7);
let GROUP=LS('tkGroup','none');
let SEL=new Set(); // selected ticket ids — survives re-render/auto-refresh
let COLLAPSED=new Set(); // collapsed group keys
+let EXPANDED=new Set(); // expanded detail-row ids — survives the 30s auto-refresh re-render
// toggleable stat pills — mirrors the column-visibility pattern (persists to localStorage)
const PILLS=[{k:'open',l:'open'},{k:'doing',l:'doing'},{k:'blocked',l:'blocked'},{k:'done',l:'done'},{k:'stopped',l:'stopped'},{k:'stale',l:'stale'},{k:'total',l:'total'},{k:'shown',l:'shown'}];
let PILL_VIS=Object.assign(Object.fromEntries(PILLS.map(p=>[p.k,1])),LS('tkPills',{})); // merge → new pills default ON even with an old stored set
@@ -276,19 +277,22 @@ function render(){
const sc=COLS.find(c=>c.k===sortKey);
rows.sort((a,b)=>{let x=sc.raw(a),y=sc.raw(b);if(typeof x==='number'&&typeof y==='number')return (x-y)*sortDir;return String(x).localeCompare(String(y))*sortDir;});
const tb=$('#tbody');tb.innerHTML='';
- const rowTr=t=>{
- const tr=document.createElement('tr');tr.className='row'+(SEL.has(t.id)?' sel':'')+(t.status==='stopped'?' stopped':'');tr.dataset.id=t.id;
- tr.innerHTML=`<td class="cb"><input type="checkbox" class="rowcb" ${SEL.has(t.id)?'checked':''}></td>`+cols.map(c=>`<td>${c.cell(t)}</td>`).join('');
- tr.querySelector('.rowcb').onclick=e=>{e.stopPropagation();if(e.target.checked)SEL.add(t.id);else SEL.delete(t.id);tr.classList.toggle('sel',e.target.checked);setRunMsg('');syncBar();};
- tr.onclick=e=>{if(e.target.closest('a,.who,.proj,input'))return;const nx=tr.nextElementSibling;if(nx&&nx.classList.contains('det')){nx.remove();return;}
+ const buildDet=t=>{
const d=document.createElement('tr');d.className='det';
const cShown=(t.comments||[]).filter(c=>DET_VIS[c.kind]!==0);
const aShown=DET_VIS.action!==0?(t.actions||[]).slice(-6):[];
const thread=cShown.map(c=>`<div class="c k-${c.kind}"><b>${esc(c.agent)}</b> <i>${esc(c.kind)}</i> ${esc(c.text)}<span class="cts">${dt(c.ts)}</span></div>`).join('')
+aShown.map(a=>`<div class="c k-action"><b>${esc(a.agent)}</b> <i>action</i> ${esc(a.text)}<span class="cts">${dt(a.ts)}</span></div>`).join('')
+(!cShown.length&&!aShown.length?'<div class="c none">no visible detail (toggle via ⤢ Detail)</div>':'');
- d.innerHTML=`<td colspan="${span}"><div class="thread">${thread}</div></td>`;tr.after(d);};
+ d.innerHTML=`<td colspan="${span}"><div class="thread">${thread}</div></td>`;return d;};
+ const rowTr=t=>{
+ const tr=document.createElement('tr');tr.className='row'+(SEL.has(t.id)?' sel':'')+(t.status==='stopped'?' stopped':'');tr.dataset.id=t.id;
+ tr.innerHTML=`<td class="cb"><input type="checkbox" class="rowcb" ${SEL.has(t.id)?'checked':''}></td>`+cols.map(c=>`<td>${c.cell(t)}</td>`).join('');
+ tr.querySelector('.rowcb').onclick=e=>{e.stopPropagation();if(e.target.checked)SEL.add(t.id);else SEL.delete(t.id);tr.classList.toggle('sel',e.target.checked);setRunMsg('');syncBar();};
+ tr.onclick=e=>{if(e.target.closest('a,.who,.proj,input'))return;const nx=tr.nextElementSibling;if(nx&&nx.classList.contains('det')){nx.remove();EXPANDED.delete(t.id);return;}
+ tr.after(buildDet(t));EXPANDED.add(t.id);};
tb.appendChild(tr);
+ if(EXPANDED.has(t.id))tr.after(buildDet(t)); // re-open across the 30s auto-refresh
};
if(GROUP==='none'){
for(const t of rows)rowTr(t);
@@ -407,7 +411,7 @@ $('#colsBtn').onclick=()=>{closeMenus('#colmenu');$('#colmenu').classList.toggle
$('#pillsBtn').onclick=()=>{closeMenus('#pillmenu');$('#pillmenu').classList.toggle('show');};
$('#detBtn').onclick=()=>{closeMenus('#detmenu');$('#detmenu').classList.toggle('show');};
$('#dmBtn').onclick=()=>{DM_ON=DM_ON?0:1;save('tkDM',DM_ON);$('#dmBtn').classList.toggle('off',!DM_ON);dmPanel(LASTMSG);};
-$('#resetBtn').onclick=()=>{['tkCols','tkOrder','tkWid','tkDens','tkPills','tkDet','tkDM','tkGroup','tkView','tkSortKey','tkSortDir','tkTheme'].forEach(k=>localStorage.removeItem(k));location.reload();};
+$('#resetBtn').onclick=()=>{['tkCols','tkOrder','tkWid','tkDens','tkPills','tkDet','tkDM','tkGroup','tkView','tkSortKey','tkSortDir','tkTheme','tkSection','tkLayout','tkSectionOrder','tkCatalogSort','tkCatalogDir','tkRunProfile','tkRunProfileDefaultV2'].forEach(k=>localStorage.removeItem(k));location.reload();};
document.addEventListener('click',e=>{if(!e.target.closest('#colmenu,#colsBtn'))$('#colmenu').classList.remove('show');if(!e.target.closest('#pillmenu,#pillsBtn'))$('#pillmenu').classList.remove('show');if(!e.target.closest('#detmenu,#detBtn'))$('#detmenu').classList.remove('show');});
function dmPanel(mm){
if(!DM_ON){$('#dmwrap').innerHTML='';return;}
@@ -426,6 +430,7 @@ async function load(){
DATA=t; LASTMSG=m; AGENTS=a; SKILLS=s;
// drop any selected ids that no longer exist
const live=new Set(DATA.map(x=>x.id));for(const id of [...SEL])if(!live.has(id))SEL.delete(id);
+ for(const id of [...EXPANDED])if(!live.has(id))EXPANDED.delete(id);
// Deep-link support: a URL like /#q=TK-10454 (from the morning digest email)
// pre-filters the board to that ticket so you land right on it.
const hq=decodeURIComponent((location.hash.match(/[#&]q=([^&]+)/)||[])[1]||'');
← 8f2e8985 auto-data-snapshot: 2026-09-10T14:13:31 (1 data files) — dat
·
back to Ticket System
·
Record ordered ticket cycle and rejected terminal scan exper efc30bc9 →