runs: live counter + keep compare selections across filter swaps

- Counter shows 'N / cap' where cap flips 10→50 based on whether any
  chip is active. Updates immediately on chip click and after every
  htmx swap.
- compare-select.js no longer prunes selections that aren't in the
  current DOM slice — server-side filtering replaces the whole list, so
  absence from DOM means 'off-current-filter', not 'run deleted'.
This commit is contained in:
Michael Pilosov 2026-04-22 18:02:35 -06:00
parent e94d28b8fc
commit 59a6bece2e
3 changed files with 23 additions and 12 deletions

View File

@ -19,14 +19,11 @@
}
function applyToDOM() {
const checkboxes = slot.querySelectorAll('.compare-cb');
// Drop any selected stems that are no longer in the DOM (run aged out of list)
const present = new Set();
checkboxes.forEach((cb) => present.add(cb.dataset.stem));
for (const s of [...selected]) if (!present.has(s)) selected.delete(s);
// Selections persist across swaps — with server-side filtering, rows
// leave the DOM when they don't match the current filter, but the user
// still has them "in the cart".
const atCap = selected.size >= MAX;
checkboxes.forEach((cb) => {
slot.querySelectorAll('.compare-cb').forEach((cb) => {
const stem = cb.dataset.stem;
cb.checked = selected.has(stem);
cb.disabled = atCap && !cb.checked;

View File

@ -72,6 +72,19 @@
}
}
function anyFilterActive() {
return AXES.some((ax) => ax.selected != null);
}
function updateCounter() {
const count = slot.querySelectorAll('li.run').length;
const cap = anyFilterActive() ? 50 : 10;
const countEl = document.getElementById('runs-count');
const capEl = document.getElementById('runs-cap');
if (countEl) countEl.textContent = String(count);
if (capEl) capEl.textContent = String(cap);
}
function triggerRunsRefresh() {
// Tell htmx to re-fetch /runs right now with the updated hx-vals.
if (window.htmx && typeof window.htmx.trigger === 'function') {
@ -88,6 +101,7 @@
ax.selected = (ax.selected === v) ? null : v;
paint(ax);
syncHtmxVals();
updateCounter();
triggerRunsRefresh();
});
}
@ -96,14 +110,14 @@
// current even between explicit refreshes.
document.body.addEventListener('htmx:afterSwap', (e) => {
if (e.target && e.target.id === 'runs-slot') {
// Nothing to re-apply in the DOM — the server already honored the
// filter. We just make sure selected chips stay marked.
repaintAll();
updateCounter();
}
});
syncHtmxVals();
refreshUniverse();
updateCounter();
// Periodically refresh the universe so newly-introduced values appear.
setInterval(refreshUniverse, 30_000);
})();

View File

@ -296,7 +296,7 @@
<div class="section-label">
<span>§ 4 &nbsp; recent runs</span>
<span class="run-count">
<span id="runs-count">{{ runs|length }}</span> / 10 · refresh 3s
<span id="runs-count">{{ runs|length }}</span> / <span id="runs-cap">10</span> &middot; refresh 3s
<span id="poll-ind" class="htmx-indicator" style="margin-left:6px">&#9679;</span>
</span>
</div>
@ -500,8 +500,8 @@
<script src="/static/theme.js?v=11"></script>
<script type="module" src="/static/dataset-picker.js?v=11"></script>
<script type="module" src="/static/metrics.js?v=11"></script>
<script src="/static/compare-select.js?v=2"></script>
<script src="/static/runs-filter.js?v=5"></script>
<script src="/static/compare-select.js?v=3"></script>
<script src="/static/runs-filter.js?v=6"></script>
<script type="module" src="/static/run-modal.js?v=3"></script>
<script>
// Anchor-links alone don't expand <details>; force it.