/views/any: none of the filters work (regression from today's filter-bar churn) #115

Open
opened 2026-05-25 15:09:52 +00:00 by mAi · 3 comments
Collaborator

m's report (2026-05-25 17:09)

https://paliad.de/views/any

None of the filters on this custom view seem to work.

Likely regression source

Today's merges touched the filter-bar / view_service heavily:

  • t-paliad-249 (#80 inbox overhaul) — InboxSystemView extension + new axes (unread_only, inbox_focus) + RowActionInbox dispatch in shape-list.ts
  • t-paliad-248 (#79 date-range picker) — replaced time axis stub with mountDateRangePicker
  • t-paliad-278 (#110 picker 3-column layout) — rewrote date-range-picker.ts internals
  • t-paliad-265 (#96 event-card choices) — added project_event_choices URL state to verfahrensablauf (not /views directly, but used the URL plumbing)
  • t-paliad-279 (#111 verfahrensablauf reorder) — touched the verfahrensablauf form but reads from filter URL

Something in this chain broke /views/any's filter pipeline. Likely candidates:

  1. render_spec.go / system_views.go — the any system view's predicate may have been clobbered when icarus extended InboxSystemView (they share render-spec scaffolding).
  2. filter_spec.go — atlas's TimeSpec extension or icarus's unread_only axis may have changed the predicate translation rules.
  3. url-codec.ts — both icarus + atlas modified it; an off-by-one in axis serialization could blank every filter from the URL on read.
  4. shape-list.ts RowActionInbox dispatch — if the any view falls through to a missing branch, the list may render with no rows OR no filter chips active.
  5. Mismatch between BarState shape (frontend) and FilterSpec parsing (backend) — added axes (unread_only, inbox_focus) may be required on every view now, breaking the any view's bare-bones spec.

What to do

  1. Reproduce live at https://paliad.de/views/any on 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/run succeed?).
  2. Diagnose with browser DevTools — Network tab to see if the backend gets the filter params, Console to see if there's a JS error.
  3. Bisect by reverting today's filter-touching merges one by one in a local branch if the cause isn't obvious.
  4. Fix the smallest correct issue. Add a regression test against /views/any rendering with each filter active.

Files most likely touched

  • internal/services/view_service.goany view's predicate handling
  • internal/services/system_views.goany SystemView's spec
  • internal/services/filter_spec.go — predicate translation
  • frontend/src/client/views/shape-list.ts — render dispatch
  • frontend/src/client/filter-bar/url-codec.ts — URL ↔ BarState serialization
  • frontend/src/client/filter-bar/axes.ts — axis render dispatch
  • frontend/src/views.tsx / frontend/src/client/views.ts — the page that hosts the bar

Hard rules

  • Don't undo today's feature merges — fix the regression with minimal surgical change.
  • Add a regression test so the /views/any filter pipeline is covered by go test going forward.
  • go build ./... && go test ./internal/... && cd frontend && bun run build clean.
  • Branch: mai/<worker>/views-any-filters-regression.

Out of scope

  • Redesigning Custom Views.
  • Other system views (focus on /any first; audit other built-in views in the same pass if cheap).
  • Replacing the existing filter-bar component.

Reporting

mai report completed with 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.

## m's report (2026-05-25 17:09) > https://paliad.de/views/any > > None of the filters on this custom view seem to work. ## Likely regression source Today's merges touched the filter-bar / view_service heavily: - t-paliad-249 (#80 inbox overhaul) — InboxSystemView extension + new axes (`unread_only`, `inbox_focus`) + RowActionInbox dispatch in shape-list.ts - t-paliad-248 (#79 date-range picker) — replaced `time` axis stub with `mountDateRangePicker` - t-paliad-278 (#110 picker 3-column layout) — rewrote date-range-picker.ts internals - t-paliad-265 (#96 event-card choices) — added project_event_choices URL state to verfahrensablauf (not /views directly, but used the URL plumbing) - t-paliad-279 (#111 verfahrensablauf reorder) — touched the verfahrensablauf form but reads from filter URL Something in this chain broke `/views/any`'s filter pipeline. Likely candidates: 1. **`render_spec.go` / `system_views.go`** — the `any` system view's predicate may have been clobbered when icarus extended InboxSystemView (they share render-spec scaffolding). 2. **`filter_spec.go`** — atlas's TimeSpec extension or icarus's unread_only axis may have changed the predicate translation rules. 3. **`url-codec.ts`** — both icarus + atlas modified it; an off-by-one in axis serialization could blank every filter from the URL on read. 4. **`shape-list.ts` RowActionInbox dispatch** — if the `any` view falls through to a missing branch, the list may render with no rows OR no filter chips active. 5. **Mismatch between BarState shape (frontend) and FilterSpec parsing (backend)** — added axes (`unread_only`, `inbox_focus`) may be required on every view now, breaking the `any` view's bare-bones spec. ## What to do 1. **Reproduce live** at `https://paliad.de/views/any` on 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/run` succeed?). 2. **Diagnose** with browser DevTools — Network tab to see if the backend gets the filter params, Console to see if there's a JS error. 3. **Bisect** by reverting today's filter-touching merges one by one in a local branch if the cause isn't obvious. 4. **Fix the smallest correct issue**. Add a regression test against `/views/any` rendering with each filter active. ## Files most likely touched - `internal/services/view_service.go` — `any` view's predicate handling - `internal/services/system_views.go` — `any` SystemView's spec - `internal/services/filter_spec.go` — predicate translation - `frontend/src/client/views/shape-list.ts` — render dispatch - `frontend/src/client/filter-bar/url-codec.ts` — URL ↔ BarState serialization - `frontend/src/client/filter-bar/axes.ts` — axis render dispatch - `frontend/src/views.tsx` / `frontend/src/client/views.ts` — the page that hosts the bar ## Hard rules - **Don't undo today's feature merges** — fix the regression with minimal surgical change. - **Add a regression test** so the `/views/any` filter pipeline is covered by `go test` going forward. - `go build ./... && go test ./internal/... && cd frontend && bun run build` clean. - Branch: `mai/<worker>/views-any-filters-regression`. ## Out of scope - Redesigning Custom Views. - Other system views (focus on `/any` first; audit other built-in views in the same pass if cheap). - Replacing the existing filter-bar component. ## Reporting `mai report completed` with 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.
mAi self-assigned this 2026-05-25 15:09:52 +00:00
Author
Collaborator

Sweep verdict (2026-07-29): CANNOT TELL.

The view still exists — paliad.user_views slug any, 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 in system_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 (commit 4c39886).

**Sweep verdict (2026-07-29): CANNOT TELL.** The view still exists — `paliad.user_views` slug `any`, 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 in `system_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` (commit 4c39886).
Author
Collaborator

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, commit ff1147d.

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) flattened FilterSpec.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.go can hold a session now, so the run is possible. New file cmd/server/http_smoke_views_filters_test.go seeds prod's saved any view 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 axis axesForSources exposes 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

any is a user view with an owner (user_id 1afbfbe9…, created 2026-05-08), not a system view. TestHTTPSmoke_ViewsAnyIsOwned pins 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

axis result
time (horizon past_7d) narrows correctly
time (custom absolute bounds) narrows correctly
deadline_status = [completed] narrows correctly
deadline_status = [pending] narrows correctly
appointment_type single value narrows correctly
appointment_type multi value narrows correctly
project_event_kind narrows correctly
approval_entity_type narrows correctly
approval_viewer_role = self_requested narrows correctly
personal_only narrows deadline + appointment, per the documented scope
two axes composing correct
approval_status BROKEN — fixed here
project_event_kind, all-subtracted case BROKEN — fixed here

So "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_status narrowed nothing on two of three rails

allowedRequestStatuses (view_service.go) skipped the post-load check for a single-valued predicate, on the stated grounds that the value "was already pushed into InboxFilter.Status".

  • ListSubmittedByUser does read that field.
  • ListPendingForApprover does not — it hardcodes ar.status = 'pending'.
  • any_visible unions the two.

So on the default rail (approver_eligible) and on any_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.

/inbox is unaffected: its system view is any_visible + status=[pending], and the union was already pending-only, so the now-enforced check admits the same set.

Defect 2 — project_event_kind widened to every kind when every picked kind was an approval-audit one

allowedProjectEventKinds subtracts every *_approval_* kind when approval_request is also a source (the de-dup from t-paliad-249), and returns nil for "no narrowing". A request consisting only of subtracted kinds leaves an empty, non-nil slice — and the caller tested len(allowedKinds) > 0, which reads empty as nil.

Measured: event_types=[approval_decided] on /views/any returned 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. /inbox is unaffected (InboxProjectEventKinds keeps 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 no time param. On a fresh /views/any load the control therefore reads "Alles" while the server is running past_30d.
  • Picking "Alles" calls ctx.patch({time: undefined}), and patch deletes undefined keys, so the saved past_30d stays 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 any as 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 hardcoding any), and it touches four surfaces — Verlauf, /inbox, /views runtime, 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.sh green under -race over ./internal/... ./cmd/... ./pkg/... ./scripts/...go vet clean, 0 new failures, 0 tolerated known-failing.
  • scripts/check-gofmt.sh clean.
  • bun test src/ — 361 pass, 0 fail.
  • bun run build clean.
  • Harness only. No prod writes; prod was read to copy the saved view's spec and nothing else.

Not closing — that is m's call.

## 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`, commit [`ff1147d`](https://mgit.msbls.de/m/paliad/commit/ff1147d). ### 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) flattened `FilterSpec.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.go` can hold a session now, so the run is possible. New file `cmd/server/http_smoke_views_filters_test.go` seeds prod's saved `any` view 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 axis `axesForSources` exposes 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 `any` is a **user** view with an owner (`user_id 1afbfbe9…`, created 2026-05-08), not a system view. `TestHTTPSmoke_ViewsAnyIsOwned` pins 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 | axis | result | |---|---| | `time` (horizon `past_7d`) | narrows correctly | | `time` (custom absolute bounds) | narrows correctly | | `deadline_status = [completed]` | narrows correctly | | `deadline_status = [pending]` | narrows correctly | | `appointment_type` single value | narrows correctly | | `appointment_type` multi value | narrows correctly | | `project_event_kind` | narrows correctly | | `approval_entity_type` | narrows correctly | | `approval_viewer_role = self_requested` | narrows correctly | | `personal_only` | narrows deadline + appointment, per the documented scope | | two axes composing | correct | | **`approval_status`** | **BROKEN — fixed here** | | **`project_event_kind`, all-subtracted case** | **BROKEN — fixed here** | So "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_status` narrowed nothing on two of three rails `allowedRequestStatuses` (view_service.go) skipped the post-load check for a single-valued predicate, on the stated grounds that the value "was already pushed into `InboxFilter.Status`". - `ListSubmittedByUser` **does** read that field. - `ListPendingForApprover` **does not** — it hardcodes `ar.status = 'pending'`. - `any_visible` unions the two. So on the default rail (`approver_eligible`) and on `any_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. `/inbox` is unaffected: its system view is `any_visible` + `status=[pending]`, and the union was already pending-only, so the now-enforced check admits the same set. ### Defect 2 — `project_event_kind` widened to every kind when every picked kind was an approval-audit one `allowedProjectEventKinds` subtracts every `*_approval_*` kind when `approval_request` is also a source (the de-dup from t-paliad-249), and returns `nil` for "no narrowing". A request consisting only of subtracted kinds leaves an **empty, non-nil** slice — and the caller tested `len(allowedKinds) > 0`, which reads empty as nil. Measured: `event_types=[approval_decided]` on `/views/any` returned **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. `/inbox` is unaffected (`InboxProjectEventKinds` keeps 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 no `time` param. On a fresh `/views/any` load the control therefore **reads "Alles" while the server is running `past_30d`**. - Picking "Alles" calls `ctx.patch({time: undefined})`, and `patch` deletes undefined keys, so the saved `past_30d` stays 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 `any` as 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 hardcoding `any`), and it touches four surfaces — Verlauf, `/inbox`, `/views` runtime, 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.sh` green under `-race` over `./internal/... ./cmd/... ./pkg/... ./scripts/...` — `go vet` clean, **0** new failures, **0** tolerated known-failing. - `scripts/check-gofmt.sh` clean. - `bun test src/` — 361 pass, 0 fail. - `bun run build` clean. - Harness only. No prod writes; prod was read to copy the saved view's spec and nothing else. Not closing — that is m's call.
Author
Collaborator

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 (merged origin/main first — #161 added an assignee axis 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:

  1. The display seeded from BarState alone. BarState is the session overlay and is empty until the user touches something, so on prod's any view — saved with past_30d — the picker read "Alles" over a running past_30d. The base spec was never consulted.
  2. Picking "Alles" patched time: undefined. ctx.patch deletes undefined as an empty, so no overlay was ever built, computeEffective left filter.time at 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 (syncURL on every patch), not a new decision for time:

  • Reloading a URL that carries ?time=any keeps the override.
  • Navigating to the view fresh (/views/any, no params) returns it to the saved horizon.
  • user_views.filter_spec is never written. The stored view keeps past_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_status chip and it behaves identically. Pinned by the 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. The any fallback 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, any included. 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 timePresets holding a next_90d view) 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 substituting any there is the same lie in a narrower case.

Does it disturb the other axes

No, and it is measured rather than claimed: an any override with deadline_status, appointment_type, approval_status, project_event_kind and personal_only all set leaves every predicate, personal_only and sources untouched.

Acceptance — row sets

cmd/server/http_smoke_views_filters_test.gotime_any_overrides_the_saved_horizon. Both directions in one subtest:

  1. the saved past_30d must drop the fixture's future-dated deadline (current_date + 30), and
  2. time=any must return it, while keeping every row the saved horizon did return.

The first half is not decoration — asserting only that any keeps 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 HorizonAny and an absent TimeSpec both 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 giving HorizonAny real bounds: the subtest goes red with "time=any did not widen the view".

Gate

scripts/ci-test-gate.sh green under -race over ./internal/... ./cmd/... ./pkg/... ./scripts/... (vet clean, 0 new failures, 0 tolerated) — run again after merging origin/main. internal/testrules green. scripts/check-gofmt.sh rc=0. bun test 441 pass. bun run build clean. Harness only (paliad-db-test-jason, port 15472); nothing was applied to prod.

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` (merged `origin/main` first — #161 added an `assignee` axis 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: 1. **The display seeded from `BarState` alone.** `BarState` is the session *overlay* and is empty until the user touches something, so on prod's `any` view — saved with `past_30d` — the picker read "Alles" over a running `past_30d`. The base spec was never consulted. 2. **Picking "Alles" patched `time: undefined`.** `ctx.patch` deletes `undefined` as an empty, so no overlay was ever built, `computeEffective` left `filter.time` at 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 (`syncURL` on every patch), not a new decision for time: - **Reloading a URL that carries `?time=any`** keeps the override. - **Navigating to the view fresh** (`/views/any`, no params) returns it to the saved horizon. - **`user_views.filter_spec` is never written.** The stored view keeps `past_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_status` chip and it behaves identically. Pinned by `the 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`. The `any` fallback 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, `any` included. 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 `timePresets` holding a `next_90d` view) 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 substituting `any` there is the same lie in a narrower case. ## Does it disturb the other axes No, and it is measured rather than claimed: an `any` override with `deadline_status`, `appointment_type`, `approval_status`, `project_event_kind` and `personal_only` all set leaves every predicate, `personal_only` and `sources` untouched. ## Acceptance — row sets `cmd/server/http_smoke_views_filters_test.go` → `time_any_overrides_the_saved_horizon`. Both directions in **one** subtest: 1. the saved `past_30d` must **drop** the fixture's future-dated deadline (`current_date + 30`), and 2. `time=any` must **return** it, while keeping every row the saved horizon did return. The first half is not decoration — asserting only that `any` keeps 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 `HorizonAny` and an absent `TimeSpec` both 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 giving `HorizonAny` real bounds: the subtest goes red with "time=any did not widen the view". ## Gate `scripts/ci-test-gate.sh` green under `-race` over `./internal/... ./cmd/... ./pkg/... ./scripts/...` (vet clean, 0 new failures, 0 tolerated) — run again after merging `origin/main`. `internal/testrules` green. `scripts/check-gofmt.sh` rc=0. `bun test` 441 pass. `bun run build` clean. Harness only (`paliad-db-test-jason`, port 15472); nothing was applied to prod. Not closing — that is m's call.
mAi added the
done
label 2026-07-30 17:56:38 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/paliad#115
No description provided.