diff --git a/app/web/main.py b/app/web/main.py index 705a9ff..e514939 100644 --- a/app/web/main.py +++ b/app/web/main.py @@ -742,14 +742,16 @@ async def index(request: Request) -> HTMLResponse: reducers = _reducer_choices() default_reducer = reducers[0]["key"] if reducers else None default_spec = REDUCERS.get(default_reducer) if default_reducer else None + q = request.query_params + required = _chip_filter_tags(q) + initial_limit = 50 if required else 10 async with httpx.AsyncClient(timeout=5.0) as client: - runs = await PREFECT.recent_runs(client, limit=10) + runs = await PREFECT.recent_runs(client, limit=initial_limit, required_tags=required) dep_id = await PREFECT.deployment_id(client) views = [_run_view(r) for r in runs] _mark_stale_views(views) # Pre-resolve the two
sections' open state from the URL so # first paint matches (no flash). Intro defaults closed, picker open. - q = request.query_params intro_open = q.get("intro") == "1" picker_open = q.get("picker") != "0" # Also pre-resolve the radio-group selections so n/f/j render with the diff --git a/app/web/static/dataset-picker.js b/app/web/static/dataset-picker.js index 6c6b00f..6e43ef5 100644 --- a/app/web/static/dataset-picker.js +++ b/app/web/static/dataset-picker.js @@ -298,7 +298,10 @@ async function main() { // the query string so a refresh restores the page. Intro defaults to // closed (no param); picker defaults to open (no param). function updateUrlState() { - const p = new URLSearchParams(); + // Merge into the current query — other scripts (e.g. runs-filter.js) + // also own some keys, so wipe only ours before repopulating. + const p = new URLSearchParams(window.location.search); + for (const k of ['intro', 'picker', 'ds', 'n', 'f', 'j']) p.delete(k); const introEl = document.getElementById('intro'); const pickerEl = document.getElementById('picker'); if (introEl && introEl.open) p.set('intro', '1'); diff --git a/app/web/static/runs-filter.js b/app/web/static/runs-filter.js index ef11e7c..471eed1 100644 --- a/app/web/static/runs-filter.js +++ b/app/web/static/runs-filter.js @@ -37,6 +37,26 @@ slot.setAttribute('hx-vals', JSON.stringify(stateAsQuery())); } + function syncUrl() { + // Mirror chip state to URL query — merge so we don't stomp on other + // scripts' keys (dataset-picker writes intro/picker/ds/n/f/j). + const p = new URLSearchParams(window.location.search); + for (const ax of AXES) p.delete(ax.key); + for (const ax of AXES) { + if (ax.selected != null) p.set(ax.key, ax.selected); + } + const qs = p.toString(); + history.replaceState(null, '', qs ? `?${qs}` : window.location.pathname); + } + + function applyUrlState() { + const u = new URLSearchParams(window.location.search); + for (const ax of AXES) { + const v = u.get(ax.key); + ax.selected = v || null; + } + } + function paint(ax) { if (!ax.el) return; ax.el.innerHTML = ''; @@ -101,6 +121,7 @@ ax.selected = (ax.selected === v) ? null : v; paint(ax); syncHtmxVals(); + syncUrl(); updateCounter(); triggerRunsRefresh(); }); @@ -115,9 +136,15 @@ } }); + applyUrlState(); syncHtmxVals(); refreshUniverse(); updateCounter(); + // If the URL had filter params, kick a refresh so #runs-slot's initial + // HTML (rendered unfiltered server-side) gets replaced with the matching + // slice. Without this the user would see the full list until the 3s + // poll tick. + if (AXES.some((ax) => ax.selected != null)) triggerRunsRefresh(); // Periodically refresh the universe so newly-introduced values appear. setInterval(refreshUniverse, 30_000); })(); diff --git a/app/web/templates/index.html b/app/web/templates/index.html index 51294ff..eaf52ec 100644 --- a/app/web/templates/index.html +++ b/app/web/templates/index.html @@ -500,7 +500,7 @@ - +