[object Object]

← back to Approval Viewer

Mark ticket links as record drills

9dc5949ea866ff94bf526a6b9b86b273e6b5d271 · 2026-09-02 17:52:07 -0700 · Steve Abrams

Files touched

Diff

commit 9dc5949ea866ff94bf526a6b9b86b273e6b5d271
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 2 17:52:07 2026 -0700

    Mark ticket links as record drills
---
 public/index.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/index.html b/public/index.html
index 3639e31..ded22ba 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,4 +1,4 @@
 <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Gated Approval Viewer</title><style>
 :root{color-scheme:dark;--bg:#101216;--panel:#191d24;--line:#303744;--ink:#f1f3f5;--muted:#aab3c0;--gold:#d9b166;--ok:#64d1ba;--red:#ed8e8e}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.45 system-ui,sans-serif}header{padding:24px max(18px,4vw);border-bottom:1px solid var(--line);background:#15181e}h1{margin:0 0 5px;font-size:25px}p{color:var(--muted);margin:5px 0}.notice{margin-top:14px;padding:10px 12px;border:1px solid #3e4b60;border-radius:9px;color:#c8d9f4}.tools{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:17px}input,select,button,textarea{font:inherit;border:1px solid var(--line);border-radius:8px;padding:9px 11px;background:var(--panel);color:var(--ink)}input{min-width:240px}.grid{padding:22px max(18px,4vw);display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:15px}.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:10px}.card.done{border-color:#4b5b69}.title{font-weight:700;font-size:17px}.meta{display:flex;gap:7px;flex-wrap:wrap}.badge{font-size:12px;padding:3px 8px;border-radius:999px;border:1px solid var(--line);color:var(--muted);text-decoration:none}a.badge:hover{border-color:var(--gold);color:var(--gold)}.hard{color:var(--red);border-color:#704244}.soft{color:var(--ok);border-color:#32675d}.summary{color:#d2d7df;min-height:45px}.actions{display:flex;gap:7px;flex-wrap:wrap;margin-top:auto}.actions button,.approve-all{cursor:pointer}.approve,.approve-all{border-color:#32675d;color:var(--ok)}.approve-all{background:#183a33;font-weight:750}.approve-all:disabled{opacity:.55;cursor:wait}.revise{border-color:#76602e;color:var(--gold)}.block{border-color:#704244;color:var(--red)}.status{font-size:13px;color:var(--muted)}.when{font-variant-numeric:tabular-nums}.empty{padding:30px;color:var(--muted)}textarea{width:100%;min-height:44px;resize:vertical}
 </style></head><body><header><h1>Approval command center</h1><p>Every chip is immediately approvable. Approval is full-scope and recorded without a confirmation step.</p><div class="notice">Approvals are append-only audit decisions. This viewer never executes commands, publishes, emails, spends, deploys, or deletes by itself.</div><div class="tools"><input id="search" type="search" placeholder="Search tickets…"><select id="filter"><option value="all">All gates</option><option value="hard">Hard gates</option><option value="soft">Safe local items</option></select><button id="approve-all" class="approve-all" type="button">Approve all shown</button><span id="count" class="status"></span></div></header><main id="grid" class="grid"></main><script>
-let data={items:[],decisions:[]};const $=s=>document.querySelector(s);const latest=()=>Object.fromEntries(data.decisions.map(d=>[d.id,d]));function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}function fmtDate(value){let date=new Date(value);return Number.isNaN(date.valueOf())?'Unknown':date.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}function visibleItems(){let q=$('#search').value.toLowerCase(),f=$('#filter').value;return data.items.filter(i=>(f==='all'||i.risk===f)&&(!q||JSON.stringify(i).toLowerCase().includes(q)));}function render(){let st=latest(),list=visibleItems();$('#count').textContent=`${list.length} item${list.length===1?'':'s'} shown`;$('#approve-all').textContent=`Approve all shown (${list.length})`;$('#approve-all').disabled=!list.length;$('#grid').innerHTML=list.length?list.map(i=>{let d=st[i.id];return `<article class="card ${d?'done':''}"><div class="title">${esc(i.title)}</div><div class="meta"><a class="badge" href="http://127.0.0.1:9794/#q=${encodeURIComponent(i.id)}" target="_blank" rel="noopener noreferrer" title="Open ${esc(i.id)} on the ticket board">${esc(i.id)}</a><span class="badge ${i.risk}">${esc(i.gate)}</span><span class="badge">${esc(i.owner)}</span><span class="badge when" title="${esc(i.created_at)}">🕓 ${esc(fmtDate(i.created_at))}</span></div><div class="summary">${esc(i.summary)}</div><textarea id="note-${esc(i.id)}" placeholder="Optional note" aria-label="Note for ${esc(i.title)}"></textarea>${d?`<div class="status">Recorded: <b>${esc(d.action)}</b>${d.approvalScope==='full'?' · full approval':''} · ${esc(fmtDate(d.at))}</div>`:''}<div class="actions"><button class="approve" data-id="${esc(i.id)}" data-action="approve">Approve fully</button><button class="revise" data-id="${esc(i.id)}" data-action="revise">Revise</button><button class="block" data-id="${esc(i.id)}" data-action="block">Block</button></div></article>`}).join(''):'<div class="empty">No matching items.</div>';document.querySelectorAll('[data-action]').forEach(b=>b.onclick=()=>decide(b.dataset.id,b.dataset.action));}async function decide(id,action){let note=(document.getElementById(`note-${id}`)||{}).value||'';let r=await fetch('/api/decision',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,action,note})});let j=await r.json();if(!r.ok)return alert(j.error||'Could not record decision');data.decisions.push(j.decision);render();}async function approveAll(){let button=$('#approve-all'),ids=visibleItems().map(i=>i.id);if(!ids.length)return;button.disabled=true;button.textContent='Approving…';let r=await fetch('/api/decisions/approve-all',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids})});let j=await r.json();if(!r.ok){button.disabled=false;render();return alert(j.error||'Could not approve shown items');}data.decisions.push(...j.decisions);render();}function syncUrl(){let u=new URL(location.href),qv=$('#search').value,fv=$('#filter').value;qv?u.searchParams.set('search',qv):u.searchParams.delete('search');fv&&fv!=='all'?u.searchParams.set('filter',fv):u.searchParams.delete('filter');history.replaceState(null,'',u);}function applyUrlState(){let p=new URLSearchParams(location.search);if(p.has('search'))$('#search').value=p.get('search');if(p.has('filter'))$('#filter').value=p.get('filter');}async function load(){applyUrlState();data=await (await fetch('/api/items')).json();render();}$('#search').oninput=()=>{render();syncUrl();};$('#filter').onchange=()=>{render();syncUrl();};$('#approve-all').onclick=approveAll;load();</script></body></html>
+let data={items:[],decisions:[]};const $=s=>document.querySelector(s);const latest=()=>Object.fromEntries(data.decisions.map(d=>[d.id,d]));function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}function fmtDate(value){let date=new Date(value);return Number.isNaN(date.valueOf())?'Unknown':date.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}function visibleItems(){let q=$('#search').value.toLowerCase(),f=$('#filter').value;return data.items.filter(i=>(f==='all'||i.risk===f)&&(!q||JSON.stringify(i).toLowerCase().includes(q)));}function render(){let st=latest(),list=visibleItems();$('#count').textContent=`${list.length} item${list.length===1?'':'s'} shown`;$('#approve-all').textContent=`Approve all shown (${list.length})`;$('#approve-all').disabled=!list.length;$('#grid').innerHTML=list.length?list.map(i=>{let d=st[i.id];return `<article class="card ${d?'done':''}"><div class="title">${esc(i.title)}</div><div class="meta"><a class="badge" data-drill="record" href="http://127.0.0.1:9794/#q=${encodeURIComponent(i.id)}" target="_blank" rel="noopener noreferrer" title="Open ${esc(i.id)} on the ticket board">${esc(i.id)}</a><span class="badge ${i.risk}">${esc(i.gate)}</span><span class="badge">${esc(i.owner)}</span><span class="badge when" title="${esc(i.created_at)}">🕓 ${esc(fmtDate(i.created_at))}</span></div><div class="summary">${esc(i.summary)}</div><textarea id="note-${esc(i.id)}" placeholder="Optional note" aria-label="Note for ${esc(i.title)}"></textarea>${d?`<div class="status">Recorded: <b>${esc(d.action)}</b>${d.approvalScope==='full'?' · full approval':''} · ${esc(fmtDate(d.at))}</div>`:''}<div class="actions"><button class="approve" data-id="${esc(i.id)}" data-action="approve">Approve fully</button><button class="revise" data-id="${esc(i.id)}" data-action="revise">Revise</button><button class="block" data-id="${esc(i.id)}" data-action="block">Block</button></div></article>`}).join(''):'<div class="empty">No matching items.</div>';document.querySelectorAll('[data-action]').forEach(b=>b.onclick=()=>decide(b.dataset.id,b.dataset.action));}async function decide(id,action){let note=(document.getElementById(`note-${id}`)||{}).value||'';let r=await fetch('/api/decision',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,action,note})});let j=await r.json();if(!r.ok)return alert(j.error||'Could not record decision');data.decisions.push(j.decision);render();}async function approveAll(){let button=$('#approve-all'),ids=visibleItems().map(i=>i.id);if(!ids.length)return;button.disabled=true;button.textContent='Approving…';let r=await fetch('/api/decisions/approve-all',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids})});let j=await r.json();if(!r.ok){button.disabled=false;render();return alert(j.error||'Could not approve shown items');}data.decisions.push(...j.decisions);render();}function syncUrl(){let u=new URL(location.href),qv=$('#search').value,fv=$('#filter').value;qv?u.searchParams.set('search',qv):u.searchParams.delete('search');fv&&fv!=='all'?u.searchParams.set('filter',fv):u.searchParams.delete('filter');history.replaceState(null,'',u);}function applyUrlState(){let p=new URLSearchParams(location.search);if(p.has('search'))$('#search').value=p.get('search');if(p.has('filter'))$('#filter').value=p.get('filter');}async function load(){applyUrlState();data=await (await fetch('/api/items')).json();render();}$('#search').oninput=()=>{render();syncUrl();};$('#filter').onchange=()=>{render();syncUrl();};$('#approve-all').onclick=approveAll;load();</script></body></html>

← c64e08d href-drill-canary: real fix, not exemption — link ticket-ID  ·  back to Approval Viewer  ·  (newest)