← back to Ticket System
board: on/off toggles for all four field groups — columns (▦), stat pills (◍), expanded-row detail fields by kind (⤢), and the DM panel (✉); all persist to localStorage + Reset clears them (TK-10429)
c65f1c1b8402ea34ff43449c780e20e586c08019 · 2026-08-10 15:52:23 -0700 · steve
Files touched
Diff
commit c65f1c1b8402ea34ff43449c780e20e586c08019
Author: steve <steve@designerwallcoverings.com>
Date: Mon Aug 10 15:52:23 2026 -0700
board: on/off toggles for all four field groups — columns (▦), stat pills (◍), expanded-row detail fields by kind (⤢), and the DM panel (✉); all persist to localStorage + Reset clears them (TK-10429)
---
board.html | 38 ++++++++++++++++++++++++++++----------
1 file changed, 28 insertions(+), 10 deletions(-)
diff --git a/board.html b/board.html
index 2a07b772..9c6e7152 100644
--- a/board.html
+++ b/board.html
@@ -54,11 +54,14 @@
<input type="range" id="dens" min="4" max="14" step="1" title="row density">
<button class="btn" id="colsBtn">▦ Columns</button>
<button class="btn" id="pillsBtn" title="Toggle stat pills">◍ Pills</button>
- <button class="btn" id="resetBtn" title="Reset columns/sort/density/pills">↺ Reset</button>
+ <button class="btn" id="detBtn" title="Toggle expanded-row detail fields">⤢ Detail</button>
+ <button class="btn" id="dmBtn" title="Toggle the DM panel">✉ DMs</button>
+ <button class="btn" id="resetBtn" title="Reset all view settings">↺ Reset</button>
<a class="btn" href="/kanban">Kanban</a><a class="btn" href="/office">Office</a>
</header>
<div id="colmenu"></div>
<div id="pillmenu"></div>
+<div id="detmenu"></div>
<div class="tblwrap"><table class="g"><thead><tr id="thead"></tr></thead><tbody id="tbody"></tbody></table></div>
<div id="dmwrap"></div>
<script>
@@ -90,6 +93,10 @@ let WID=LS('tkWid',{}), DENS=LS('tkDens',7);
// 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:'stale',l:'stale'},{k:'totals',l:'total / shown'}];
let PILL_VIS=LS('tkPills',null)||Object.fromEntries(PILLS.map(p=>[p.k,1]));
+// toggleable expanded-row detail fields (by kind) + DM-panel show/hide — both persist
+const DET=[{k:'comment',l:'comments'},{k:'note',l:'notes'},{k:'action',l:'actions'},{k:'win',l:'wins'},{k:'challenge',l:'challenges'},{k:'cody',l:'cody dissents'}];
+let DET_VIS=LS('tkDet',null)||Object.fromEntries(DET.map(d=>[d.k,1]));
+let DM_ON=LS('tkDM',1), LASTMSG=[];
document.documentElement.style.setProperty('--rowpad',DENS+'px'); $('#dens').value=DENS;
const ordered=()=>ORDER.map(k=>COLS.find(c=>c.k===k)).filter(c=>c&&VIS[c.k]);
@@ -128,9 +135,11 @@ function render(){
tr.innerHTML=cols.map(c=>`<td>${c.cell(t)}</td>`).join('');
tr.onclick=e=>{if(e.target.closest('a,.who,.proj'))return;const nx=tr.nextElementSibling;if(nx&&nx.classList.contains('det')){nx.remove();return;}
const d=document.createElement('tr');d.className='det';
- const thread=(t.comments||[]).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('')
- +(t.actions||[]).slice(-6).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('')
- +(!(t.comments||[]).length&&!(t.actions||[]).length?'<div class="c none">no comments yet</div>':'');
+ 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="${cols.length}"><div class="thread">${thread}</div></td>`;tr.after(d);};
tb.appendChild(tr);
}
@@ -158,14 +167,23 @@ function renderPillMenu(){
m.innerHTML=`<div class="gh">Stat pills</div>`+PILLS.map(p=>`<label><input type="checkbox" data-k="${p.k}" ${PILL_VIS[p.k]?'checked':''}>${esc(p.l)}</label>`).join('');
m.querySelectorAll('input').forEach(cb=>cb.onchange=()=>{PILL_VIS[cb.dataset.k]=cb.checked?1:0;save('tkPills',PILL_VIS);renderPills();});
}
+function renderDetMenu(){
+ const m=$('#detmenu');
+ m.innerHTML=`<div class="gh">Expanded-row detail fields</div>`+DET.map(d=>`<label><input type="checkbox" data-k="${d.k}" ${DET_VIS[d.k]?'checked':''}>${esc(d.l)}</label>`).join('');
+ m.querySelectorAll('input').forEach(cb=>cb.onchange=()=>{DET_VIS[cb.dataset.k]=cb.checked?1:0;save('tkDet',DET_VIS);document.querySelectorAll('tr.det').forEach(r=>r.remove());});
+}
window.setQ=v=>{$('#q').value=v;q=v;render();};
$('#q').oninput=e=>{q=e.target.value;render();};
$('#dens').oninput=e=>{DENS=+e.target.value;document.documentElement.style.setProperty('--rowpad',DENS+'px');save('tkDens',DENS);};
-$('#colsBtn').onclick=()=>{$('#pillmenu').classList.remove('show');$('#colmenu').classList.toggle('show');};
-$('#pillsBtn').onclick=()=>{$('#colmenu').classList.remove('show');$('#pillmenu').classList.toggle('show');};
-$('#resetBtn').onclick=()=>{['tkCols','tkOrder','tkWid','tkDens','tkPills'].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');});
+const closeMenus=except=>['#colmenu','#pillmenu','#detmenu'].forEach(id=>{if(id!==except)$(id).classList.remove('show');});
+$('#colsBtn').onclick=()=>{closeMenus('#colmenu');$('#colmenu').classList.toggle('show');};
+$('#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'].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;}
const map=new Map(mm.map(m=>[m.mid,m]));const rootOf=mid=>{let id=mid,c=map.get(mid),seen=new Set([id]);while(c&&c.re&&map.get(c.re)){if(seen.has(c.re))break;seen.add(c.re);id=c.re;c=map.get(id);}return id;};
const th=new Map();for(const m of mm){const r=rootOf(m.mid);(th.get(r)||th.set(r,[]).get(r)).push(m);}
const convos=[...th.values()].map(a=>a.sort((x,y)=>x.ts<y.ts?-1:1)).sort((a,b)=>a[a.length-1].ts<b[b.length-1].ts?1:-1).slice(0,30);
@@ -177,8 +195,8 @@ function dmPanel(mm){
async function load(){
try{
const [t,m]=await Promise.all([fetch('/api/tickets').then(r=>r.json()),fetch('/api/messages').then(r=>r.json())]);
- DATA=t; render(); dmPanel(m);
+ DATA=t; LASTMSG=m; render(); dmPanel(m);
}catch(e){$('#tbody').innerHTML=`<tr><td>load failed: ${esc(e.message)}</td></tr>`;}
}
-renderColMenu(); renderPillMenu(); load(); setInterval(load,30000);
+renderColMenu(); renderPillMenu(); renderDetMenu(); $('#dmBtn').classList.toggle('off',!DM_ON); load(); setInterval(load,30000);
</script></body></html>
← 79770148 board: toggle FLEET TICKETS stat pills on/off (persisted); f
·
back to Ticket System
·
board: fetch via location.origin — fixes empty table when op 6ab4daff →