Log rows: CSS grid layout, generic cell model, column header #39

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

Part of the dynamic-columns work. Depends on the log-list scroller change (the list becoming the scroll container in both axes, plus the nowrap row-width fix), which is planned separately and must land first — the column header cannot be sticky until then.

Why

LogRow is a fixed five-field struct (src/views/mod.rs) and templates/partials/log_row.html is five hardcoded spans in a flex row with four fixed-width classes. Hiding, reordering or resizing a column is not expressible against that shape: the geometry lives in four separate SCSS rules rather than in one list, and the cells are addressed by name rather than iterated.

This issue establishes the structure. It adds no user-visible feature beyond the header row; the same five columns render at the same widths.

Scope

Layout. Each row becomes display: grid with grid-template-columns: var(--log-cols), the variable set once on the list. The column header consumes the same variable, so header/body alignment is structural rather than two sets of widths kept in sync.

The message column is not part of --log-cols. It is implicit and always last, so the two wrap modes are two static rules:

.log-row            { grid-template-columns: var(--log-cols) minmax(<floor>, 1fr); }
.is-nowrap .log-row { grid-template-columns: var(--log-cols) max-content; }

A floor on the message track is required, not optional: a minmax(0, 1fr) track that resolves to zero width does not clip pre-wrap text, it wraps one character per line and makes every row hundreds of lines tall.

Units. Widths are expressed in ch as text capacity, with padding added in CSS. The log is monospace, so a character count computed in Rust is directly commensurable with a CSS track width — this is what later lets the server compute fit exactly. Note this inverts the current border-box convention documented in assets/scss/main.scss.

Model. LogRow becomes an ordered sequence of cells. A column is declared per view with its source (entry field / stream label / flattened field), its formatter, its fallback, its default width and a declared maximum. The closed set stays as it is today; the representation should be chosen so an arbitrary-field passthrough column can be added later without reshaping it.

A cell is an enum rather than a string, distinguishing:

  • verbatim — raw and rendered are identical (app, node, level when present)
  • formatted — they differ (time)
  • absent — no underlying value; and absent because missing (the em-dash) is distinct from absent because defaulted (level falling back to info, which src/views/mod.rs documents as a substantive claim rather than a placeholder)

Cells carry a stable name so the partial can compose log-{name} class names. Template-side logic is fine — the constraint recorded in src/views/mod.rs is really no client-side render logic, so that the buffered and SSE paths produce identical markup; the comment should be corrected to say so.

Header. A column header row with per-column cells, inside the scroll container so it stays aligned under horizontal scroll. Needs explicit ARIA roles (row, columnheader) — a <table> would have given column association for free and divs do not.

Remove title. log_row.html emits title unconditionally on node and app, so it fires on every cell including ones that fit and including the em-dash placeholder. Column resize (separate issue) supersedes it, and row detail expansion will later supersede it again. title is weak for accessibility regardless.

Acceptance

  • The all view renders identically to today apart from the new header row.
  • tests/views.rs passes unchanged — it selects .log-time, .log-node, .log-app, .log-level, .log-msg and asserts the em-dash fallback, the level default and the time format. Those assertions surviving is the signal that the generic model preserved behaviour.
  • Row markup still comes from the single partial (architecture invariant 3).

Out of scope

URL-driven column state, resize, fit, zebra and severity colouring, the narrow-viewport reflow, row detail expansion, passthrough columns.

Note on verification

The model half is covered by the existing harness. The layout half is not observable to axum_test + scraper at all, and there is no browser in CI. Prefer designing hazards out (the message floor, explicit parity) over relying on review to catch them.

Part of the dynamic-columns work. **Depends on** the log-list scroller change (the list becoming the scroll container in both axes, plus the nowrap row-width fix), which is planned separately and must land first — the column header cannot be sticky until then. ## Why `LogRow` is a fixed five-field struct (`src/views/mod.rs`) and `templates/partials/log_row.html` is five hardcoded spans in a flex row with four fixed-width classes. Hiding, reordering or resizing a column is not expressible against that shape: the geometry lives in four separate SCSS rules rather than in one list, and the cells are addressed by name rather than iterated. This issue establishes the structure. It adds no user-visible feature beyond the header row; the same five columns render at the same widths. ## Scope **Layout.** Each row becomes `display: grid` with `grid-template-columns: var(--log-cols)`, the variable set once on the list. The column header consumes the same variable, so header/body alignment is structural rather than two sets of widths kept in sync. The message column is *not* part of `--log-cols`. It is implicit and always last, so the two wrap modes are two static rules: ```scss .log-row { grid-template-columns: var(--log-cols) minmax(<floor>, 1fr); } .is-nowrap .log-row { grid-template-columns: var(--log-cols) max-content; } ``` A floor on the message track is required, not optional: a `minmax(0, 1fr)` track that resolves to zero width does not clip `pre-wrap` text, it wraps one character per line and makes every row hundreds of lines tall. **Units.** Widths are expressed in `ch` as *text capacity*, with padding added in CSS. The log is monospace, so a character count computed in Rust is directly commensurable with a CSS track width — this is what later lets the server compute `fit` exactly. Note this inverts the current border-box convention documented in `assets/scss/main.scss`. **Model.** `LogRow` becomes an ordered sequence of cells. A column is declared per view with its source (entry field / stream label / flattened field), its formatter, its fallback, its default width and a declared maximum. The closed set stays as it is today; the representation should be chosen so an arbitrary-field passthrough column can be added later without reshaping it. A cell is an enum rather than a string, distinguishing: - **verbatim** — raw and rendered are identical (`app`, `node`, `level` when present) - **formatted** — they differ (`time`) - **absent** — no underlying value; and *absent because missing* (the em-dash) is distinct from *absent because defaulted* (`level` falling back to `info`, which `src/views/mod.rs` documents as a substantive claim rather than a placeholder) Cells carry a stable name so the partial can compose `log-{name}` class names. Template-side logic is fine — the constraint recorded in `src/views/mod.rs` is really *no client-side render logic*, so that the buffered and SSE paths produce identical markup; the comment should be corrected to say so. **Header.** A column header row with per-column cells, inside the scroll container so it stays aligned under horizontal scroll. Needs explicit ARIA roles (`row`, `columnheader`) — a `<table>` would have given column association for free and divs do not. **Remove `title`.** `log_row.html` emits `title` unconditionally on `node` and `app`, so it fires on every cell including ones that fit and including the em-dash placeholder. Column resize (separate issue) supersedes it, and row detail expansion will later supersede it again. `title` is weak for accessibility regardless. ## Acceptance - The `all` view renders identically to today apart from the new header row. - `tests/views.rs` passes unchanged — it selects `.log-time`, `.log-node`, `.log-app`, `.log-level`, `.log-msg` and asserts the em-dash fallback, the `level` default and the time format. Those assertions surviving is the signal that the generic model preserved behaviour. - Row markup still comes from the single partial (architecture invariant 3). ## Out of scope URL-driven column state, resize, `fit`, zebra and severity colouring, the narrow-viewport reflow, row detail expansion, passthrough columns. ## Note on verification The model half is covered by the existing harness. The layout half is not observable to `axum_test` + `scraper` at all, and there is no browser in CI. Prefer designing hazards out (the message floor, explicit parity) over relying on review to catch them.
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#39
No description provided.