dataforth/testdatadb UI: resizable inspector + fit-to-width cert + quick-search presets; drop redundant PASS/FAIL selector

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 07:05:34 -07:00
parent 55407e8601
commit d162dc7726

View File

@@ -33,8 +33,17 @@
border-radius:6px;height:30px;padding:0 10px;cursor:pointer}
.hbtn:hover{background:var(--hover)}
/* ---------- layout ---------- */
main{display:grid;grid-template-columns:232px 1fr 420px;min-height:0;height:100%}
main{display:grid;grid-template-columns:232px 1fr var(--insp,460px);min-height:0;height:100%}
.pane{min-height:0;overflow:auto}
/* ---------- presets ---------- */
.presets{display:flex;flex-direction:column;gap:5px}
.preset{display:flex;align-items:center;gap:7px;width:100%;text-align:left;font:inherit;font-size:12.5px;
height:30px;padding:0 9px;border:1px solid var(--border);border-radius:6px;background:#fff;color:var(--ink);cursor:pointer}
.preset:hover{background:var(--accent-soft);border-color:#bfdbfe}
.preset .pi{width:15px;text-align:center;color:var(--ink-3)}
.preset.fam{display:inline-flex;width:auto;height:26px;font-family:var(--mono);font-size:11.5px;padding:0 8px}
.preset.fam:disabled,.preset:disabled{opacity:.5;cursor:not-allowed;background:#fff}
.famrow{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}
/* ---------- filter rail ---------- */
.rail{border-right:1px solid var(--border);background:var(--surface);padding:14px}
.rail h3{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-3);margin:18px 0 8px;font-weight:600}
@@ -76,7 +85,10 @@
border-radius:6px;background:#fff;cursor:pointer;color:var(--ink)}
.pager button:disabled{opacity:.4;cursor:default}
/* ---------- inspector ---------- */
.insp{border-left:1px solid var(--border);background:var(--surface);display:grid;grid-template-rows:auto auto 1fr;min-height:0}
.insp{position:relative;border-left:1px solid var(--border);background:var(--surface);display:grid;grid-template-rows:auto auto 1fr;min-height:0}
.resizer{position:absolute;left:-3px;top:0;bottom:0;width:7px;cursor:col-resize;z-index:5}
.resizer:hover,.resizer.drag{background:linear-gradient(90deg,transparent,var(--accent) 45%,var(--accent) 55%,transparent)}
body.resizing{cursor:col-resize;user-select:none}
.insp .meta{padding:12px 14px;border-bottom:1px solid var(--border)}
.insp .meta .sn{font-family:var(--mono);font-size:16px;font-weight:700}
.insp .meta dl{display:grid;grid-template-columns:auto 1fr;gap:3px 12px;margin:10px 0 0;font-size:12.5px}
@@ -109,12 +121,9 @@
<main>
<!-- filter rail -->
<aside class="rail pane">
<h3>Result</h3>
<div class="seg" id="segResult">
<button data-v="" class="on">All</button>
<button data-v="PASS">PASS</button>
<button data-v="FAIL">FAIL</button>
</div>
<h3>Quick searches</h3>
<div class="presets" id="presets"></div>
<div class="famrow" id="families"></div>
<h3>Test date</h3>
<label>From</label><input type="date" id="fFrom">
<label>To</label><input type="date" id="fTo">
@@ -157,6 +166,7 @@
<!-- inspector -->
<aside class="insp">
<div class="resizer" id="resizer" title="Drag to resize"></div>
<div class="meta" id="meta"><div class="empty" style="height:auto;padding:8px 0">No record selected</div></div>
<div class="acts" id="acts" style="display:none"></div>
<div id="viewer"><div class="empty">Search a serial number, then select a row.<br>The calibration certificate renders here.</div></div>
@@ -240,15 +250,14 @@ function select(id){
<button onclick="document.getElementById('viewer').querySelector('iframe').contentWindow.print()">Print</button>
<a href="${ds}?format=txt" download="${r.serial_number}.txt">TXT</a>
<a href="${ds}?format=html" download="${r.serial_number}.html">HTML</a>`;
$('viewer').innerHTML = `<iframe src="${ds}?format=html" title="datasheet"></iframe>`;
$('viewer').innerHTML = '<iframe id="dsframe" title="datasheet"></iframe>';
const _f=document.getElementById('dsframe'); _f.onload=fitCert; _f.src=ds+'?format=html';
syncUrl();
}
// ---- filters wiring ----
function debouncedSearch(){ clearTimeout(timer); timer=setTimeout(()=>{state.page=0;search();},280); }
$('omni').addEventListener('input', e=>{ routeOmni(e.target.value); debouncedSearch(); });
$('segResult').addEventListener('click', e=>{ const b=e.target.closest('button'); if(!b)return;
[...e.currentTarget.children].forEach(x=>x.classList.toggle('on',x===b)); state.result=b.dataset.v; state.page=0; search(); });
$('fFrom').onchange=e=>{state.from=e.target.value;state.page=0;search();};
$('fTo').onchange=e=>{state.to=e.target.value;state.page=0;search();};
$('fModel').onchange=e=>{state.model=e.target.value;state.page=0;search();};
@@ -257,8 +266,58 @@ $('fLog').onchange=e=>{state.logtype=e.target.value;state.page=0;search();};
$('pageSize').onchange=e=>{state.size=+e.target.value;state.page=0;search();};
$('prev').onclick=()=>{if(state.page>0){state.page--;search();$('twrap').scrollTop=0;}};
$('next').onclick=()=>{state.page++;search();$('twrap').scrollTop=0;};
$('reset').onclick=()=>{ ['result','station','logtype','from','to'].forEach(k=>state[k]='');
$('segResult').children[0].click(); $('fFrom').value=$('fTo').value=$('fStation').value=$('fLog').value=''; };
$('reset').onclick=()=>{ ['serial','model','q','result','station','logtype','from','to'].forEach(k=>state[k]='');
$('omni').value='';$('route').textContent='';$('fFrom').value=$('fTo').value=$('fStation').value=$('fLog').value=$('fModel').value='';
state.page=0; search(); };
// ---- cert fit-to-width (same-origin: scale the rendered cert so it never side-scrolls) ----
function fitCert(){
const f=document.getElementById('dsframe'); if(!f) return;
try{ const doc=f.contentDocument; if(!doc) return;
const root=doc.documentElement; root.style.zoom='';
const natural=Math.max(doc.body?doc.body.scrollWidth:0, root.scrollWidth);
const avail=f.clientWidth-12;
root.style.zoom = (natural>avail) ? Math.max(0.45, avail/natural) : 1;
}catch(e){}
}
window.addEventListener('resize', fitCert);
// ---- resizable inspector ----
(function(){ const rz=$('resizer'); let on=false;
rz.addEventListener('mousedown', e=>{ on=true; rz.classList.add('drag'); document.body.classList.add('resizing'); e.preventDefault(); });
window.addEventListener('mousemove', e=>{ if(!on) return;
let w=Math.max(340, Math.min(window.innerWidth-560, window.innerWidth-e.clientX));
document.documentElement.style.setProperty('--insp', w+'px'); });
window.addEventListener('mouseup', ()=>{ if(on){ on=false; rz.classList.remove('drag'); document.body.classList.remove('resizing'); fitCert(); } });
})();
// ---- quick-search presets ----
function clearAll(){ ['serial','model','q','result','station','logtype','from','to'].forEach(k=>state[k]='');
$('omni').value='';$('route').textContent='';$('fFrom').value=$('fTo').value=$('fStation').value=$('fLog').value=$('fModel').value=''; }
function applyPreset(fn){ clearAll(); fn(); state.page=0; search(); }
const _iso=d=>d.toISOString().slice(0,10);
const PRESETS=[
{ic:'◷',label:'Recent',fn:()=>{}},
{ic:'✕',label:'Failures',fn:()=>{state.result='FAIL';}},
{ic:'•',label:'Today',fn:()=>{const t=_iso(new Date());state.from=t;state.to=t;}},
{ic:'7',label:'Last 7 days',fn:()=>{const d=new Date();d.setDate(d.getDate()-7);state.from=_iso(d);}},
{ic:'∷',label:'This year',fn:()=>{state.from=new Date().getFullYear()+'-01-01';}},
];
const SOON=[
{label:'Latest upload batch',why:'needs an upload-time sort param in /api/search'},
{label:'Retested units',why:'needs a retest flag in the pipeline'},
{label:'Not yet published',why:'needs a published filter in /api/search'},
];
function renderPresets(){
const host=$('presets'); host.innerHTML='';
PRESETS.forEach(p=>{ const b=document.createElement('button'); b.className='preset';
b.innerHTML='<span class="pi">'+p.ic+'</span>'+p.label; b.onclick=()=>applyPreset(p.fn); host.appendChild(b); });
SOON.forEach(s=>{ const b=document.createElement('button'); b.className='preset'; b.disabled=true; b.title=s.why;
b.innerHTML='<span class="pi">…</span>'+s.label; host.appendChild(b); });
const fam=$('families'); fam.innerHTML='';
['DSCA','8B','5B','7B','SCM5B'].forEach(m=>{ const b=document.createElement('button'); b.className='preset fam';
b.textContent=m; b.onclick=()=>applyPreset(()=>{state.model=m;$('fModel').value=m;}); fam.appendChild(b); });
}
// ---- keyboard ----
document.addEventListener('keydown', e=>{
@@ -292,6 +351,7 @@ async function boot(){
alert('Total: '+s.total_records.toLocaleString()+'\nResult: '+(s.by_result||[]).map(r=>r.overall_result+' '+r.count.toLocaleString()).join(' · ')
+'\nDates: '+fmtDate(s.date_range.oldest)+' → '+fmtDate(s.date_range.newest)
+'\nLog types: '+(s.by_log_type||[]).map(r=>r.log_type+' '+r.count.toLocaleString()).join(' · ')); };
renderPresets();
$('omni').focus();
search();
}