From dd8ada479ebc196e9313826152bbb7d786452d7b Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Sat, 27 Jun 2026 20:36:24 -0400 Subject: [PATCH] feat(ui): hide/exclude models from the dashboard (persisted) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each scoreboard row gets a × to hide that model — for retired ones (m1 etc.) you no longer want in the view. Hidden models drop out of the table, totals, and the findings drill-down; the set persists in localStorage (grt-hidden) across reloads, with a "hidden (N): …" bar of click-to-restore chips + a "show all". Solo-ness is still computed against ALL models (hiding is a view filter, not a rescoring), so hiding one model never fakes another's solo finds. README Dashboard section updated. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 6 ++++++ ui.html | 45 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1b0c8b3..0c6a1d7 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,12 @@ Its mirror, **solo-error penalty ×** (default `1.5`), multiplies the FP penalty was made by **only that model** — a unique wrong claim is noisier than a shared mistake. So a Blocking-claimed solo FP costs `high(8) × -0.5 × 1.5 = -6` vs `-4` for a shared one. Set to `1` to disable. +**Hiding models.** Each scoreboard row has a small **×** to hide that model — handy for retired ones +(e.g. `m1`) you no longer want cluttering the view. Hidden models drop out of the table, the totals, +and the findings drill-down (but **not** from solo-ness, which stays computed against all models — hiding +is a view filter, not a rescoring). The hidden set persists in `localStorage` across reloads; a +**hidden (N): …** bar lists them as click-to-restore chips, with a **show all** to clear. + Auth: the `/ui` shell is public (it holds no data); paste the store token into its **connect** box, or open `/ui?token=` once (remembered in `localStorage`). Prefer your own dashboard? Point Grafana/Metabase/etc. at the SQLite file or the same `/export` + `/scoreboard` + `/runs` JSON. diff --git a/ui.html b/ui.html index b5b2157..8f5b3a2 100644 --- a/ui.html +++ b/ui.html @@ -88,6 +88,7 @@
+ @@ -115,6 +116,17 @@ const SEVCOLOR = { trivial:"#3b4252", small:"#2e4d3a", medium:"#4d4a2e", high:"# let RUNS = [], ROWS = []; let sortKey = "ptsPerMin", sortAsc = false, selModel = null; +// Persistently-excluded models (e.g. retired ones like m1). Hidden from the +// scoreboard, totals, and drill-down; persisted in localStorage across reloads. +// Solo-ness is still computed against ALL models (hiding is a view filter, not a +// rescoring), so hiding one model never fakes another's solo finds. +function loadHidden(){ try { return new Set(JSON.parse(localStorage.getItem("grt-hidden") || "[]")); } catch { return new Set(); } } +let HIDDEN = loadHidden(); +function saveHidden(){ localStorage.setItem("grt-hidden", JSON.stringify([...HIDDEN].sort())); } +function hideModel(m){ HIDDEN.add(m); if (selModel===m) selModel=null; saveHidden(); render(); } +function showModel(m){ HIDDEN.delete(m); saveHidden(); render(); } +function showAllModels(){ HIDDEN.clear(); saveHidden(); render(); } + function token(){ const q = new URL(location.href).searchParams.get("token"); if (q) { localStorage.setItem("grt", q); return q; } @@ -229,7 +241,7 @@ function aggregate(f){ for (const r of RUNS){ if(!runMatch(r,f)) continue; const m=get(r.model); m.runs++; m.minutes += (r.duration_secs||0)/60; m.inTok += r.input_tokens||0; m.outTok += r.output_tokens||0; if(r.provider) m.provider=r.provider; } - const rows = ROWS.filter(r => rowMatch(r, f)); + const rows = ROWS.filter(r => rowMatch(r, f) && !HIDDEN.has(r.model)); for (const r of rows){ const m=get(r.model); if(r.provider) m.provider=m.provider||r.provider; m.findings.add(r.finding_id); if (r.graded && r.is_real === true){ m.confirmed.set(r.finding_id, r.severity || ""); } @@ -256,7 +268,7 @@ function aggregate(f){ ptsPerMin: m.minutes>0 ? points/m.minutes : null, ptsPerRun: m.runs>0 ? points/m.runs : null, confirmedPct: findings>0 ? confirmed/findings*100 : null }; - }).filter(m => m.runs>0 || m.findings>0); + }).filter(m => (m.runs>0 || m.findings>0) && !HIDDEN.has(m.model)); return { models: out, rows }; } @@ -297,7 +309,16 @@ function render(){ for (const col of COLS){ const td = document.createElement("td"); if (col.l) td.className="l"; const v = m[col.k]; - td.innerHTML = col.fmt ? col.fmt(v) : (v==null?"—":v); + if (col.k==="model"){ + // model name + a hide control (× pill) — injection-safe via JS handler. + td.textContent = (v==null?"—":v) + " "; + const x = document.createElement("span"); + x.className = "pill"; x.textContent = "×"; x.title = "hide this model (persists)"; + x.onclick = (e)=>{ e.stopPropagation(); hideModel(m.model); }; + td.appendChild(x); + } else { + td.innerHTML = col.fmt ? col.fmt(v) : (v==null?"—":v); + } if ((col.k==="ptsPerMin" || col.k==="ptsPerRun" || col.k==="points") && v!=null) td.classList.add(v<0 ? "bad" : "good"); if (col.k==="fpPen" && v<0) td.classList.add("bad"); if (col.k==="solo" && v>0) td.classList.add("good"); @@ -306,6 +327,24 @@ function render(){ } mb.appendChild(tr); } + // hidden-models panel: click a model to restore it + const hid = document.getElementById("hidden"); + if (HIDDEN.size){ + hid.innerHTML = ""; + const lab = document.createElement("span"); lab.textContent = "hidden ("+HIDDEN.size+"): "; hid.appendChild(lab); + for (const m of [...HIDDEN].sort()){ + const p = document.createElement("span"); p.className="pill"; p.textContent = "+ "+m; + p.title = "show this model again"; p.style.marginRight="6px"; + p.onclick = ()=> showModel(m); + hid.appendChild(p); + } + const all = document.createElement("button"); all.className="link"; all.textContent="show all"; + all.onclick = showAllModels; hid.appendChild(all); + hid.style.display = ""; + } else { + hid.style.display = "none"; hid.innerHTML = ""; + } + const tot = models.reduce((a,m)=>({runs:a.runs+m.runs, min:a.min+m.minutes, find:a.find+m.findings, conf:a.conf+m.confirmed, pts:a.pts+m.points}), {runs:0,min:0,find:0,conf:0,pts:0}); document.getElementById("summary").innerHTML = `${models.length} models · ${tot.runs} runs · ${tot.min.toFixed(0)} min · ${tot.find} findings · ${tot.conf} confirmed · ${tot.pts.toFixed(0)} pts` +