Skip to content

fix(ui): column widths ignore colspan rows and empty first renders (#258) - #268

Merged
dborup merged 7 commits into
masterfrom
codex/issue-258-column-widths
Oct 6, 2026
Merged

dborup merged 7 commits into
masterfrom
codex/issue-258-column-widths

Conversation

@adminopenclaw8-sketch

Copy link
Copy Markdown
Collaborator

Relates to #258

Follow-up to #252 (#244). It addresses finding 1 and the "UX remainder" of the #252 review. Plan: comment on #258.

Problem

makeColumnsResizable() (public/app.js) sizes a table's columns from the header and the first body rows. Two things went wrong:

  1. Colspan rows counted. It measured cells by index, so a full-width colspan row (the packets vscroll spacers, "No packets found") was credited to column 0.
  2. One-shot measurement. It measured only once, from whatever the first render had.

On packets with an aged fixture, the default 15-min window is empty. At 1200 px the expand column got 43 % of the table and Details 65 px. Long advert names then sat entirely on the line the #67 clamp hides, and only the icon showed. Even with a fresh fixture, expand took 176 px for a chevron.

Fix

  • Skip colspan rows. columnMeasureRows() keeps only rows with one cell per header cell and no colspan.
  • Re-measure once, later. With fewer than 5 usable rows the widths are provisional.
    • One MutationObserver (childList on the tbody) measures again once real rows arrive, then disconnects.
    • It does not re-measure if the user has saved widths by then.
    • A table measured from a full body gets no observer.
  • The re-measure sees the table as the first measure did:
    • Hidden columns: TableResponsive.unhidden(table, fn) (packets.js) runs the measurement with its own column hiding lifted. Its tbody observer runs after ours, so the new rows' cells are not yet marked; without this, at 900 px HB came out at 12.2 % instead of 3.8 %.
    • Handles: the resize handles are hidden while measuring. They stick out 4 px past their th.
    • Page-set widths: widths that the page's markup sets on a th are restored, for example observers' style="width:32px".
  • Saved widths (meshcore-*-col-widths) are applied exactly as before, and the handles are unchanged.
  • Fit logic unchanged. The measure and fit steps are extracted as they were (measureColumnWidths, fitColumnWidths, applyMeasuredColumnWidths, readSavedColumnWidths). The first measure is the same as master's except that colspan rows are skipped.

Perf

  • First call: as before, at most 30 rows × columns, once per table.
  • Provisional tables only: each tbody childList mutation runs a guard:
    • isConnected;
    • a localStorage read for the storage key;
    • tbody.rows.length;
    • when that length is ≥ 5, a scan for 5 usable rows.
  • One re-measure, then nothing. After it, the observer is disconnected, so no per-render or per-row work is left.
  • unhidden: two querySelectorAll calls and a class toggle on the hidden cells, only during a measurement.
  • Callers: tables measured from a full body (nodes, observers, analytics, new-nodes on the fixture) never create an observer. No new API calls.

Tests

Test-first: each fix commit follows a red test commit.

  • test-issue-258-column-widths.js (new, in test-all.sh) runs the real app.js in a vm sandbox with a fake table DOM. Its cells report content width, assigned width and handle overhang. It covers:

    • colspan, short and same-length colspan rows;
    • an empty first body and a near-empty one (2 rows; 4 rows are not enough, 6 rows are);
    • one re-measure, then a disconnected observer;
    • no feedback from the provisional widths;
    • a th width set by the page;
    • hidden columns measured inside unhidden;
    • saved widths before and during the provisional phase;
    • idempotent re-calls;
    • a detached table.

    15 cases, 3/15 on master.

  • test-packets.js: the real TableResponsive.unhidden lifts and restores, also when its callback throws. Both new cases are red on master.

  • test-issue-258-column-widths-e2e.js (new, in deploy.yml after the bug(packets): filter UX disaster — help panel overlaps table, toolbar chaotic, path chips spill rows Kpa-clawbot/CoreScope#1122 Details clamp E2E). It ages the fixture by moving the browser clock to an effective 20 and 120 min, with the offset taken from the newest packet. At 1200 and 900 px it checks:

    • the default window renders empty;
    • after widening, Details is ≥ 15 % and expand ≤ 10 % of the table;
    • every column's share matches a page opened with that window right away;
    • every on-screen advert link shows ≥ 12 px, and the long pinned name shows at least half;
    • an adverts-only filter, the empty window and the wide window again do not move the widths.

    It also checks that saved widths apply unchanged and that a handle drag resizes and saves. 10/22 on master (12 red).

  • test-issue-1122-details-row-clamp-e2e.js:

    • 375 px: the pinned long name must still be cut by its clip there.
    • 900 / 1200 px: it must show at least half of itself, since Details now fits it.

Gates

  • scripts/check-xss-sinks.sh --diff origin/master is clean.
  • No hardcoded colours.
  • Fork guards are unchanged: 9 in deploy.yml and 1 in release-fast-path.yml.
  • One new E2E line in deploy.yml.

🤖 Generated with Claude Code

dborup and others added 7 commits October 5, 2026 16:12
…rst renders (#258)

makeColumnsResizable() credits full-width colspan rows (vscroll spacers,
"No packets found") to column 0 and locks the widths from an empty or
near-empty first body. The new vm-sandbox unit test runs the real app.js
against a minimal fake table: colspan and short rows must not change any
width, an (almost) empty body must be measured again once when rows
arrive (and only then), saved widths must be applied as before.

Red on master: 3 passed, 9 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New test-issue-258-column-widths-e2e.js, registered after the Kpa-clawbot#1122
Details clamp E2E. It ages the fixture by moving the browser clock (20
and 120 min effective, offset from the newest packet), checks that the
default window renders empty, then widens the window at 1200 and 900 px:
Details >= 15% and expand <= 10% of the table, advert names show text
(the long pinned name at least half), widths stay through later renders,
saved widths apply unchanged and a resize handle still resizes and saves.
Red on master: 9 passed, 9 failed.

test-issue-1122-details-row-clamp-e2e.js: the pinned long advert name
must be cut by its clip only on mobile (375 px). At 900/1200 px Details
is sized from the real rows after the fix, so the name must show at
least half of itself instead (finding 1 of the #252 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
)

makeColumnsResizable() measured each column by row index, so a
full-width colspan row (vscroll spacer, "No packets found") was
credited to column 0, and it measured only once, from whatever the
first render had. On packets with an aged fixture the default 15-min
window is empty: expand got ~43% of the table and Details 65 px at
1200 px, hiding long advert names behind the #67 clamp.

- columnMeasureRows() keeps only rows with one cell per header cell and
  no colspan.
- With fewer than 5 usable rows the widths are provisional; one
  MutationObserver (childList on the tbody) measures again once real
  rows arrive, unless widths have been saved by then, and disconnects.
  A table measured from a full body gets no observer.
- Saved widths are read by readSavedColumnWidths() and applied as before.
- The measure and fit steps are extracted unchanged
  (measureColumnWidths, fitColumnWidths, applyMeasuredColumnWidths);
  the re-measure clears the old th widths first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…258)

- A row with as many cells as the header, one of them a colspan, is
  not measured either.
- A width the page's markup puts on a th (observers' compare column has
  style="width:32px") counts in the first measure, as on master, and
  again in the re-measure. Red on the previous commit, which cleared it
  before measuring and changed the observers table's widths slightly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first measure no longer clears the header cells' inline widths, so
tables measured from a full body (nodes, observers, analytics,
new-nodes) get exactly master's widths. The re-measure restores the
widths the page's markup set before measuring, instead of the
provisional percentages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- E2E: after the deferred re-measure, every column's share must equal
  (within 1 point) that of a page opened with the wide window right
  away. Red on the previous commit: at 900 px TableResponsive had hidden
  five header cells but not yet the new rows' cells, so the re-measure
  saw a skewed table (HB 12.2% instead of 3.8%); at 1200 px the resize
  handles, which stick out 4px past their th, widened the narrow
  columns.
- Unit: columns TableResponsive hid are measured inside
  TableResponsive.unhidden (stubbed here), and a shown handle's 4px
  overhang does not count.
- test-packets.js: the real TableResponsive.unhidden lifts col-hidden
  and the pills only while its callback runs, also when it throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- TableResponsive.unhidden(table, fn) (packets.js) runs fn with its own
  column hiding lifted (no col-hidden, no pill) and restores it.
  measureColumnWidths() measures inside it, so the re-measure, which
  runs before TableResponsive has marked the new rows' cells, sees the
  same columns as the first measure (before register()). Other CSS that
  hides a column still applies.
- The resize handles are hidden while measuring; they did not exist at
  the first measure and their overhang inflated th.scrollWidth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dborup
dborup marked this pull request as ready for review October 6, 2026 01:13
@dborup
dborup merged commit 406f848 into master Oct 6, 2026
6 checks passed
dborup added a commit that referenced this pull request Oct 6, 2026
#268 (issue #258, column widths) and this branch both appended a line to
test-all.sh's runner list. Kept both, in issue order next to #254:

  run test-issue-258-column-widths.js
  run test-issue-259-nodes-esc-listener.js

Everything else auto-merged and was verified to be the exact union of both
sides: .github/workflows/deploy.yml keeps the #258 and #259 E2E lines,
public/packets.js keeps #258's TableResponsive.unhidden() and #259's
groupIsExpandedInView(), and test-packets.js keeps both sets of cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dborup added a commit that referenced this pull request Oct 6, 2026
…wups

fix(packets): one pktEsc listener per page and #268/#260/#212 follow-ups (#282)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants