Skip to content

makeColumnsResizable measures once and credits colspan rows to column 0 (packets Details column too narrow) #258

Description

@dborup

Found while fixing #244 (PR #252); the reviewer confirmed it there. The default column widths of resizable tables come from a one-shot measurement that can go wrong in two ways. On the packets table, the Details column then becomes so narrow that some advert names are entirely on the hidden, clamped second line, so only the icon shows.

Cause

makeColumnsResizable() in public/app.js (master 310c501a, around line 2254):

  1. It measures only once. It sets table.dataset.resizable = '1' and sizes the columns from the first ≤ 30 body rows present at that moment. If the first render is empty or nearly empty, the widths come from the header alone and stay that way. On packets, that happens when the default 15-minute window has few rows.
  2. Colspan rows are credited to column 0. The measurement reads row.children[i] per column index. A row with one full-width colspan cell, such as a spacer, empty-state or group-detail row, therefore puts its whole content width into the first column. The extra width then stays with that column (expand on packets).

Observed in #252 at 1200 px (reported by the author and the reviewer, [T] there):

expand Details
fresh fixture (first render has 29 rows) 15 % 15 % (178 px)
aged fixture (empty first window) 43.5 % 5.4 % (64 px)

When colspan rows are skipped, the review measured Details at 301 px with the fresh fixture. CSS can't fix it: width: 32px !important on th.col-expand had no effect, because Chrome's auto layout still gives the leftover width to that column.

Saved widths in localStorage (meshcore-pkt-col-widths etc.) bypass the measurement. Users who resized a column once keep their own widths.

Expected

  • Skip rows with a colspan cell, or any row where row.children.length !== ths.length, when measuring.
  • Do not lock widths from an empty or near-empty body. Measure again once real rows exist, but at most once and only while the user has no saved widths, so there is no per-render cost.
  • Keep the behaviour for the other callers: nodes.js, observers.js, analytics.js, gps-sanity.js, new-nodes.js, node-changes.js and position-gaps.js.
  • Update the pinned.truncated guard in test-issue-1122-details-row-clamp-e2e.js in the same change. The test(packets): make the Details advert-link E2E independent of fixture age (#244) #252 review notes that it assumes today's narrow Details column and will fail once the widths are fixed.

Tests

  • A unit test (vm context) with a colspan row and with an empty first body.
  • An E2E on packets at 1200 and 900 px with an aged fixture, which checks the Details width and that long advert names show text, not only the icon.
  • A mutant check: put the colspan rows back into the measurement, and the test must fail.

Perf: the measurement runs once per table per page load (at most twice with the re-measure). There is no per-row or per-render work.

Activity

  1. added
    bugSomething isn't working
    type:bugSomething broken
    on Oct 5, 2026
  2. adminopenclaw8-sketch commented on Oct 5, 2026

    @adminopenclaw8-sketch
    Collaborator

    Plan — CS-Macmini #258

    Branch codex/issue-258-column-widths from origin/master 3bb2cb89. Tests first (red), then the fix. One draft PR, "Relates to #258".

    1. Skip colspan rows (public/app.js)

    • A new helper, columnMeasureRows(tbody, colCount, limit), returns at most 30 body rows that have exactly one cell per header cell and no cell with colSpan > 1.
    • It skips the vscroll spacers, "No packets found", the group-detail rows and any row with a different cell count.
    • makeColumnsResizable() measures only those rows plus the header.

    2. Do not lock widths from an (almost) empty body

    • Threshold: COL_MEASURE_MIN_ROWS = 5 usable rows.
    • Below the threshold, the table gets provisional widths as today: the header plus the usable rows, with handles added. One MutationObserver on that tbody (childList only) then waits for real rows.
    • The callback is a cheap guard:
      • if the table is gone or the storage key now holds saved widths, it disconnects;
      • if tbody.rows.length is below the threshold, it returns.
    • Once enough usable rows exist, it disconnects first and then re-measures once. Before measuring it clears the th widths, so the old percentages do not feed back.
    • At or above the threshold on the first call, no observer is created. Today's path is unchanged, apart from the colspan skip.
    • Cost: at most two measurements per table per page load. The guard runs only while the table is still provisional; after that there is no per-render or per-row work.

    3. Saved widths

    • A valid meshcore-*-col-widths entry is applied exactly as today, with no measurement and no observer.
    • If the user drags a handle while the table is provisional, onUp saves the widths as today, and the observer then disconnects without re-measuring.
    • The handles themselves are unchanged.

    4. The 8 callers

    • packets: the first render with the default 15-min window and an aged fixture has only colspan rows, so it gets the deferred measure.
    • nodes, observers and gps-sanity, new-nodes, node-changes, position-gaps: they call after rendering rows. They change only when they render colspan rows or fewer than 5 rows first; then they also re-measure once later.
    • analytics: small static tables never mutate, so the observer never fires and costs nothing. They keep today's widths.
    • The measurement keeps today's table-layout and fitting logic. I'll extract the fit step into a pure helper without changing what it does.
    • Browser check: column widths of nodes, observers and one analytics table, master vs branch, with a fresh fixture.

    5. test-issue-1122-details-row-clamp-e2e.js

    • With the fix, Details at 1200 px is about 300 px, so the pinned 21-char name fits.
    • Truncation is required only where it is real, at 375 px.
    • At 900 and 1200 px the pinned long name must instead show its whole width. This is a stronger check, and it cannot pass vacuously, because the pinned row must still be on screen.
    • I will base the exact rule on measured widths.

    6. Tests and mutants

    Gates

    • No hardcoded colours.
    • scripts/check-xss-sinks.sh --diff origin/master clean.
    • Fork guards stay 9 + 1.
    • One new E2E line in deploy.yml.

    Customizer

    COL_MEASURE_MIN_ROWS (5) is an internal layout heuristic, not a user setting, so it gets no customizer entry.

  3. added a commit that references this issue on Oct 6, 2026
  4. dborup commented on Oct 6, 2026

    @dborup
    OwnerAuthor

    Fixed by #268 (merged as 406f8481).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtype:bugSomething broken

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions