Column state in the query string; migrate wrap out of the cookie #40

Open
opened 2026-11-12 17:26:08 +01:00 by omp-deck · 0 comments
Collaborator

Part of the dynamic-columns work. Depends on the grid layout / cell model / header issue.

Why

Column visibility, order and width need somewhere to live. The query string is the right place rather than the view cookie:

  • Scoping is automatic — state belongs to the view's URL, so there is no global-vs-per-view decision and no shared cookie size ceiling.
  • Layouts become shareable and bookmarkable. A URL pasted into a ticket reproduces what the sender saw. A cookie cannot do this at all.
  • Hide and reorder become plain navigations, so they work with scripting disabled.
  • The escaping-free constraint documented on the view cookie disappears, since query parameters are percent-encoded.
  • AGENTS.md invariant 6 already requires internal view links to carry the current time range forward, so a mechanism for propagating view params through hrefs is mandated regardless.

Scope

Encoding. One parameter: ?cols=time,node,app:28,level

  • sequence gives order
  • presence gives visibility
  • name:width overrides the view's declared default; bare name uses it
  • widths are in ch, meaning text capacity
  • fit is a valid width value (app:fit), resolved server-side at render
  • the message column is not listed — it is implicit and always last
  • unknown names are ignored, matching the existing tolerance rule: a stale or hand-edited URL must never fail a render

Widths are optional by design, so a view can retune its defaults later without every saved link pinning stale values.

Hide and reorder are server-side re-renders, deliberately. CSS order and display: none would both work visually, but order leaves text selection and copy following DOM order — so copying a reordered row yields the fields in the wrong sequence. Omitting the cell server-side is correct and also emits less markup.

wrap moves from the cookie to the query. It is presentation of this view and belongs in a shared link. Note it is not a navigation: nothing about wrap is server-side (it already toggles client-side today), so the control flips the class and calls history.replaceState. The server renders it only for correct first paint.

tz stays a cookie. It is a property of whose browser is looking, not of the view; a link shared with someone in another zone should render in theirs.

--log-cols derives from cols and wrap together, since the message track differs between modes.

This leaves hide and reorder as the only operations needing a round trip.

Acceptance

  • ?cols= controls which cells are emitted, in what order, at what width.
  • Omitted, malformed and unknown entries fall back cleanly; no input produces a 500.
  • wrap is read from the query, rendered into first paint, and toggled client-side with the URL kept in sync.
  • ViewState no longer carries wrap.

Out of scope

Resize and fit gestures (separate issue). A scroll/entry anchor in the URL — it is only durable once a pinned time range exists, so it belongs with the time-range work.

Known gap

Hide and reorder are full navigations and therefore lose scroll position. Accepted for now; the entry anchor that would fix it is deferred. A client-side swap of the rendered content block would also fix it and would reuse the same template via askama's block rendering, but it is not worth the tail-stream teardown complexity for the two rarest operations.

Follow-up noted while writing this

The comment in assets/js/view-state.js justifies a synchronous cookie write with "callers reload immediately after writing", but its only caller does not reload. Removing wrap from the cookie forces a decision there — tz would be the sole remaining value.

Part of the dynamic-columns work. **Depends on** the grid layout / cell model / header issue. ## Why Column visibility, order and width need somewhere to live. The query string is the right place rather than the `view` cookie: - Scoping is automatic — state belongs to the view's URL, so there is no global-vs-per-view decision and no shared cookie size ceiling. - Layouts become shareable and bookmarkable. A URL pasted into a ticket reproduces what the sender saw. A cookie cannot do this at all. - Hide and reorder become plain navigations, so they work with scripting disabled. - The escaping-free constraint documented on the `view` cookie disappears, since query parameters are percent-encoded. - `AGENTS.md` invariant 6 already requires internal view links to carry the current time range forward, so a mechanism for propagating view params through `href`s is mandated regardless. ## Scope **Encoding.** One parameter: `?cols=time,node,app:28,level` - sequence gives order - presence gives visibility - `name:width` overrides the view's declared default; bare `name` uses it - widths are in `ch`, meaning text capacity - `fit` is a valid width value (`app:fit`), resolved server-side at render - the message column is not listed — it is implicit and always last - unknown names are ignored, matching the existing tolerance rule: a stale or hand-edited URL must never fail a render Widths are optional by design, so a view can retune its defaults later without every saved link pinning stale values. **Hide and reorder** are server-side re-renders, deliberately. CSS `order` and `display: none` would both work visually, but `order` leaves text selection and copy following DOM order — so copying a reordered row yields the fields in the wrong sequence. Omitting the cell server-side is correct and also emits less markup. **`wrap` moves from the cookie to the query.** It is presentation of this view and belongs in a shared link. Note it is *not* a navigation: nothing about wrap is server-side (it already toggles client-side today), so the control flips the class and calls `history.replaceState`. The server renders it only for correct first paint. **`tz` stays a cookie.** It is a property of whose browser is looking, not of the view; a link shared with someone in another zone should render in theirs. **`--log-cols` derives from `cols` and `wrap` together**, since the message track differs between modes. This leaves hide and reorder as the only operations needing a round trip. ## Acceptance - `?cols=` controls which cells are emitted, in what order, at what width. - Omitted, malformed and unknown entries fall back cleanly; no input produces a 500. - `wrap` is read from the query, rendered into first paint, and toggled client-side with the URL kept in sync. - `ViewState` no longer carries `wrap`. ## Out of scope Resize and `fit` gestures (separate issue). A scroll/entry anchor in the URL — it is only durable once a pinned time range exists, so it belongs with the time-range work. ## Known gap Hide and reorder are full navigations and therefore lose scroll position. Accepted for now; the entry anchor that would fix it is deferred. A client-side swap of the rendered `content` block would also fix it and would reuse the same template via askama's block rendering, but it is not worth the tail-stream teardown complexity for the two rarest operations. ## Follow-up noted while writing this The comment in `assets/js/view-state.js` justifies a synchronous cookie write with "callers reload immediately after writing", but its only caller does not reload. Removing `wrap` from the cookie forces a decision there — `tz` would be the sole remaining value.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/logview#40
No description provided.