/views/any: none of the filters work (regression from today's filter-bar churn) #115
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
m's report (2026-05-25 17:09)
Likely regression source
Today's merges touched the filter-bar / view_service heavily:
unread_only,inbox_focus) + RowActionInbox dispatch in shape-list.tstimeaxis stub withmountDateRangePickerSomething in this chain broke
/views/any's filter pipeline. Likely candidates:render_spec.go/system_views.go— theanysystem view's predicate may have been clobbered when icarus extended InboxSystemView (they share render-spec scaffolding).filter_spec.go— atlas's TimeSpec extension or icarus's unread_only axis may have changed the predicate translation rules.url-codec.ts— both icarus + atlas modified it; an off-by-one in axis serialization could blank every filter from the URL on read.shape-list.tsRowActionInbox dispatch — if theanyview falls through to a missing branch, the list may render with no rows OR no filter chips active.unread_only,inbox_focus) may be required on every view now, breaking theanyview's bare-bones spec.What to do
https://paliad.de/views/anyon a real test project — click each filter chip, observe what happens (does the URL update? Does the rendered list change? Does the network call to/api/views/any/runsucceed?)./views/anyrendering with each filter active.Files most likely touched
internal/services/view_service.go—anyview's predicate handlinginternal/services/system_views.go—anySystemView's specinternal/services/filter_spec.go— predicate translationfrontend/src/client/views/shape-list.ts— render dispatchfrontend/src/client/filter-bar/url-codec.ts— URL ↔ BarState serializationfrontend/src/client/filter-bar/axes.ts— axis render dispatchfrontend/src/views.tsx/frontend/src/client/views.ts— the page that hosts the barHard rules
/views/anyfilter pipeline is covered bygo testgoing forward.go build ./... && go test ./internal/... && cd frontend && bun run buildclean.mai/<worker>/views-any-filters-regression.Out of scope
/anyfirst; audit other built-in views in the same pass if cheap).Reporting
mai report completedwith branch + SHAs + root cause + UX verification: open /views/any → each filter chip changes the rendered list as expected → URL updates → backend call returns filtered rows.Sweep verdict (2026-07-29): CANNOT TELL.
The view still exists —
paliad.user_viewsslugany, name "Any", created 2026-05-08. Note it is a user view, not a system view (system slugs are dashboard / agenda / events / inbox / inbox-mine), which is why it does not appear insystem_views.go.The filter machinery is present and unit-tested (
filter_spec_predicates_test.go,filter-bar/url-codec.test.ts,compute-effective.test.ts). But "none of the filters work" is a runtime claim about a logged-in session against a user-owned view, and no test asserts that end to end.What would settle it: a browser session on paliad.de as the view's owner, clicking each filter and watching the network call. Recording CANNOT TELL rather than guessing — #154 sat two months for exactly this reason and a guess would have closed it wrongly.
Full sweep:
docs/findings-issue-sweep-2026-07-29.md(commit4c39886).Driven through the HTTP smoke harness as the view's owner — 11 of 13 axes were already correct, 2 were not
Branch
mai/brunel/m-paliad-115-the-sweep-s, commitff1147d.What the sweep could not tell, and why it can now
The named cause of this issue was found and fixed the same day it was reported:
c70914c(t-paliad-283, 2026-05-25 17:46, 37 minutes after m's 17:09 report) flattenedFilterSpec.Predicates, which Go had declared as a doubled map. Every per-source chip the bar POSTed unmarshalled into an empty struct and narrowed nothing — literally "none of the filters work".That fix shipped with unit coverage on both sides (
filter_spec_predicates_test.go,compute-effective.test.ts) and neither of them runs a query. Nothing ever established that the server returns a different set of rows when it receives the narrowed spec, which is the only thing the report was about. Hence CANNOT TELL in the 2026-07-29 sweep.cmd/server/http_smoke_enforcing_test.gocan hold a session now, so the run is possible. New filecmd/server/http_smoke_views_filters_test.goseeds prod's savedanyview field-for-field (four sources,past_30d, read from prod 2026-07-30), plus one keep/drop row pair per axis, boots the real server binary against a prod-shape harness, and drives every axisaxesForSourcesexposes for this view as the view's owner.Assertions are on row sets, never status codes — a filter that narrows nothing answers 200 over a full list. Each subtest names the row it must keep and the row it must drop: keep-only passes on a no-op filter, drop-only passes on a filter that returns nothing.
The trap, handled explicitly
anyis a user view with an owner (user_id 1afbfbe9…, created 2026-05-08), not a system view.TestHTTPSmoke_ViewsAnyIsOwnedpins the distinction from the other side: the owner gets rows, the member and the outsider get 404 — not an empty 200. So nothing below can be misread as a visibility result.Measured: 11 of 13 axis assertions were already green
time(horizonpast_7d)time(custom absolute bounds)deadline_status = [completed]deadline_status = [pending]appointment_typesingle valueappointment_typemulti valueproject_event_kindapproval_entity_typeapproval_viewer_role = self_requestedpersonal_onlyapproval_statusproject_event_kind, all-subtracted caseSo "none of the filters work" is history. Two are still true, and both are the same shape of mistake: a single-value shortcut that assumed a pushdown which is not there.
Defect 1 —
approval_statusnarrowed nothing on two of three railsallowedRequestStatuses(view_service.go) skipped the post-load check for a single-valued predicate, on the stated grounds that the value "was already pushed intoInboxFilter.Status".ListSubmittedByUserdoes read that field.ListPendingForApproverdoes not — it hardcodesar.status = 'pending'.any_visibleunions the two.So on the default rail (
approver_eligible) and onany_visible, picking a status had nothing enforcing it anywhere. Measured on the harness:status=[approved]came back carrying the pending request, over a 200.Fixed in the post-load check. Not by pushing an arbitrary status into
ListPendingForApprover— that rail means "requests you can approve", i.e. pending, and widening it would change what it is./inboxis unaffected: its system view isany_visible+status=[pending], and the union was already pending-only, so the now-enforced check admits the same set.Defect 2 —
project_event_kindwidened to every kind when every picked kind was an approval-audit oneallowedProjectEventKindssubtracts every*_approval_*kind whenapproval_requestis also a source (the de-dup from t-paliad-249), and returnsnilfor "no narrowing". A request consisting only of subtracted kinds leaves an empty, non-nil slice — and the caller testedlen(allowedKinds) > 0, which reads empty as nil.Measured:
event_types=[approval_decided]on/views/anyreturned both seeded project_events. The user asked for one kind and got every kind.The caller now tests for
nil, so an empty allow-list excludes everything — which is what an empty allow-list means./inboxis unaffected (InboxProjectEventKindskeeps non-approval kinds after the subtraction).One finding NOT fixed — needs your call
The time picker on a saved view whose horizon is not
any:renderTimeAxis(filter-bar/axes.ts) seeds the picker with{horizon: "any"}whenever the URL carries notimeparam. On a fresh/views/anyload the control therefore reads "Alles" while the server is runningpast_30d.ctx.patch({time: undefined}), andpatchdeletes undefined keys, so the savedpast_30dstays in force. The chip does nothing.The display half is plainly wrong. The behaviour half is a product question, which is why I did not change it: should "Alles" override a saved view's own horizon, or is the saved horizon the view? The current comment in the code says the suppression is deliberate ("the bar treats
anyas no time overlay"), and it keeps the URL clean — it just makes "everything" inexpressible on any view that is not already unbounded.The fix is small (thread the base horizon into
RenderAxisOpts; treat "picked == base" as the no-overlay case instead of hardcodingany), and it touches four surfaces — Verlauf,/inbox,/viewsruntime, the views editor. There is no DOM test harness in this repo, so it is not a change I can verify at runtime the way the two above were. Say the word and I file it as its own issue with that plan.Gate
scripts/ci-test-gate.shgreen under-raceover./internal/... ./cmd/... ./pkg/... ./scripts/...—go vetclean, 0 new failures, 0 tolerated known-failing.scripts/check-gofmt.shclean.bun test src/— 361 pass, 0 fail.bun run buildclean.Not closing — that is m's call.
Follow-up to the sweep: "Alles" now overrides a saved view's horizon. m's ruling 2026-07-30.
Commit: https://mgit.msbls.de/m/paliad/commit/bfb6e8f
Branch:
mai/jason/alles-overrides-horizon(mergedorigin/mainfirst — #161 added anassigneeaxis to the same substrate; full gate re-run after the merge)Which half was broken
Both defects were in the bar. Neither was in the query — the server side of the time axis was already green in the sweep, and it still is.
renderTimeAxis(frontend/src/client/filter-bar/axes.ts) held two decisions and both were wrong on a saved view:BarStatealone.BarStateis the session overlay and is empty until the user touches something, so on prod'sanyview — saved withpast_30d— the picker read "Alles" over a runningpast_30d. The base spec was never consulted.time: undefined.ctx.patchdeletesundefinedas an empty, so no overlay was ever built,computeEffectiveleftfilter.timeat the base, and the saved horizon kept running. That is the inert chip.What happens on reload — stated, not assumed
You asked me to say this explicitly rather than let it be inferred.
The override lives in the URL, exactly like every other axis in this bar. That is the bar's existing state model (
syncURLon every patch), not a new decision for time:?time=anykeeps the override./views/any, no params) returns it to the saved horizon.user_views.filter_specis never written. The stored view keepspast_30d.That matches your safe reading — a saved view returns to what it saved — and it gets there without a special case: pick a
deadline_statuschip and it behaves identically. Pinned bythe saved horizon still runs when the session overlays nothing.The fix
Both decisions moved into
frontend/src/client/filter-bar/time-axis-pure.ts:displayedTimeSpec(overlay, base)— shows what is running: overlay ?? saved ??any. Theanyfallback now applies only when neither exists, i.e. when nothing is filtering and "Alles" is the truth.timeOverlayFor(next)— returns an overlay for every horizon,anyincluded. The bar's "no overlay" state still exists; it is just no longer something a click can reach.They are extracted because they are what was broken, and a DOM-mounted picker is the one place they could not be tested. Both mutation-checked: reverting either half to its original form fails three tests each.
One case worth naming: a saved horizon the surface has no chip for (a past-only
timePresetsholding anext_90dview) is now displayed rather than swapped for "Alles". The picker's button label derives from the horizon, not from the chip set, so it reads correctly — and substitutinganythere is the same lie in a narrower case.Does it disturb the other axes
No, and it is measured rather than claimed: an
anyoverride withdeadline_status,appointment_type,approval_status,project_event_kindandpersonal_onlyall set leaves every predicate,personal_onlyandsourcesuntouched.Acceptance — row sets
cmd/server/http_smoke_views_filters_test.go→time_any_overrides_the_saved_horizon. Both directions in one subtest:past_30dmust drop the fixture's future-dated deadline (current_date + 30), andtime=anymust return it, while keeping every row the saved horizon did return.The first half is not decoration — asserting only that
anykeeps the row would pass even if the saved horizon had never been binding.What this test measures, precisely: it pins the server contract the fix rests on — that
HorizonAnyand an absentTimeSpecboth compute to empty bounds — not the defect, which was entirely client-side. If that equivalence ever stops holding, the bar goes silently inert again and no frontend test could see it. Mutation-checked by givingHorizonAnyreal bounds: the subtest goes red with "time=any did not widen the view".Gate
scripts/ci-test-gate.shgreen under-raceover./internal/... ./cmd/... ./pkg/... ./scripts/...(vet clean, 0 new failures, 0 tolerated) — run again after mergingorigin/main.internal/testrulesgreen.scripts/check-gofmt.shrc=0.bun test441 pass.bun run buildclean. Harness only (paliad-db-test-jason, port 15472); nothing was applied to prod.Not closing — that is m's call.