Skip to content

fix(packets): keep ?obs=/?viewPath= on packet detail URLs (#147) - #167

Merged
dborup merged 4 commits into
masterfrom
codex/issue-147-packets-url-detail-params
Oct 3, 2026
Merged

dborup merged 4 commits into
masterfrom
codex/issue-147-packets-url-detail-params

Conversation

@adminopenclaw8-sketch

Copy link
Copy Markdown
Collaborator

Relates to #147

Plan (AGENTS.md rule 5: written here because the reviewer was not present)

Milestone 1, tests (commit 2d3f92de, red on master 727efca0)

  • test-issue-147-packets-url-detail-params.js: runs the real buildPacketsQuery / updateClearFiltersVisibility / updatePacketsUrl and the real Clear Filters handler body from public/packets.js in vm.createContext. It also loads the real url-state.js for sort.
  • test-packet-path-map.js: 3 new cases for ?viewPath=1 when the modal opens and closes.
  • test-issue-147-packets-url-obs-e2e.js: Playwright on desktop (1400×900) and mobile (390×844, touch).
  • The new tests are registered in test-all.sh and in the deploy.yml unit and E2E steps. test-packet-path-map.js was not run in CI before, and is now. The fork guards are unchanged: 9 × github.repository == 'Kpa-clawbot/CoreScope'.

Milestone 2, fix (commit d0a9f28e): see Changes below.

Browser validation: a local server with a fixture copy, see below.

Config and customizer: nothing new is configurable.

The viewPath decision

?viewPath=1 stays in the URL only while the View Path modal is open on the same packet. When the modal closes, the param is dropped. ?obs= always stays together with the detail subpath.

Why:

  • AGENTS.md says the URL must describe what is shown. viewPath=1 means "this modal is open", so the param should live as long as the modal does.
  • If viewPath survived every filter change, a refresh or a copied address-bar link would reopen a modal that the user had already closed.
  • If viewPath were consumed once on load, it would have to be removed by the cold-load updatePacketsUrl() call while the modal is still visible. That breaks the issue's first acceptance criterion.

How:

  • updatePacketsUrl() keeps viewPath=1 only while #packetPathModal exists and the subpath is unchanged.
  • PacketPathMap.close() drops viewPath=1, and only from #/packets/<hash> URLs. The other params stay verbatim.
  • open() calls close() first, but that only removes the param when a modal actually existed. A cold-loaded link therefore keeps it.

Tests:

  • viewPath: kept on a filter change while the View Path modal is open
  • viewPath: dropped once the modal is closed …
  • close() drops ?viewPath=1 … and open() on a cold-loaded ?viewPath=1 link keeps it …
  • E2E: desktop: ?viewPath=1 is kept while the modal is open and dropped when it closes

Changes

updatePacketsUrl(detail) is now the only history.replaceState writer in packets.js. A test enforces this.

  • It writes the filter params from the unchanged buildPacketsQuery(), followed by the detail params that function does not own.
  • Without an argument (filter handlers, Clear Filters, the cold-load call in init()), it carries over the current subpath and ?obs=.
  • With { subpath, obs }, it writes a new selection.

selectPacket(), the observation-row click and the SlideOver onClose now call updatePacketsUrl({ subpath, obs }). Before, they wrote the hash themselves and dropped the active filter params. selectPacket() also runs as the cold-load auto-select, so on master it dropped viewPath from a ?obs=…&viewPath=1 link.

Cold-load ?obs= was never applied. init() parsed it from routeParam, but the router (app.js navigate(), hash.split('?')[0]) already strips the query. A #/packets/<hash>?obs=<id> link therefore always showed the first observation. init() now reads it from getHashParams(). This was verified on master in the browser: ?obs=2 showed observation 509.

buildPacketsQuery() is unchanged. A golden test pins its output to master's while the hash carries obs/viewPath and the modal is open.

Clear Filters works as before (#121/#132). Filter params are removed. The detail subpath and ?obs= stay, like the subpath did before. viewPath follows the modal rule. The existing Clear Filters tests pass.

  • test-clear-filters.js extracts only the function body. It gets the unused parameter name detail so it can run the new signature. No assertion changed.

Upstream: the obs part also exists upstream as Kpa-clawbot/CoreScope#2091.

Perf

updatePacketsUrl runs on every filter change. Its added work is:

  • one split of the current hash query, typically fewer than 10 params;
  • one filter over those params;
  • one getElementById.

That is O(URL params) and independent of the packet count (30K+). There are no new API calls, no DOM rebuilds and no loops over packets.

Tests

Red on master, green on branch. Commit-1 test files were run against a master copy:

Test master 727efca0 branch
test-issue-147-packets-url-detail-params.js 5 passed, 24 failed 29 passed, 0 failed
test-packet-path-map.js 44 passed, 1 failed 45 passed, 0 failed
test-issue-147-packets-url-obs-e2e.js 0 passed, 7 failed 7 passed, 0 failed (5/5 runs)

The 5 tests that pass on master guard the decision and the invariants: buildPacketsQuery golden output, "list view gets no obs", "viewPath never invented", Clear on the list view, and the cold-load call site.

Unchanged, run on master and branch with identical results:

  • test-packet-filter.js 92/0
  • test-frontend-helpers.js 705/2: the 2 failures are the favStar baseline, identical on master
  • test-clear-filters.js 8/0
  • test-issue-121-clear-filters-selection.js 10/0
  • test-packet-filter-ux.js 19/0
  • test-packet-filter-time.js 20/0
  • test-url-state.js 18/0
  • test-packet-detail-channel-xss.js 8/0
  • test-issue-1415-packets-layout.js 26/0
  • test-observer-detail-view-path.js 5/0
  • test-analytics-distance-view-path.js 8/0
  • test-path-inspector.js passed
  • test-issue-1753-copy-url-slash.js 4/0
  • test-xss-escape-sinks.js 34/0
  • test-packets.js 115/13: identical failing lines on master; this file is not in CI

E2E against a local server, on a fixture prepared like CI (freshen, CI seed SQL, migrate, seed-2073). Master and branch gave identical results:

  • test-e2e-playwright.js 132/0
  • test-filter-ux-e2e.js 12/0
  • test-issue-1122-packets-filter-ux-e2e.js 6/0
  • test-issue-1122-details-row-clamp-e2e.js 18/0
  • test-issue-1128-packets-layout-e2e.js 5/0
  • test-issue-1692-packets-init-parallel-e2e.js 1/0
  • test-issue-1486-collapse-reopens-detail-e2e.js 4/0
  • test-slideover-1056-e2e.js 27/0
  • test-slideover-1168-munger-e2e.js 3/0
  • test-issue-1146-path-link-contrast-e2e.js 11/0
  • test-path-inspector-coverage-e2e.js 10/0

There is no separate Clear Filters E2E for #121/#132; those tests are unit tests. The fixture has no packet with two observations that the API returns, so the new E2E stubs the detail response (context.route) with one extra synthetic observation.

Correction to commit d0a9f28e: its message says the E2E teardown race was seen "1 in 6 runs". It was actually 2 of 3 local branch runs before the hardening. After the hardening, 5/5 runs were green.

Mutants (run in a copy)

Mutant Caught by
M1: preservation removed 21 unit tests, e.g. cold load #/packets/<hash>?obs=123 keeps ?obs=123
M2: keep obs only, drop viewPath cold load …?obs=123&viewPath=1 (modal open) keeps both, viewPath: kept on a filter change while the View Path modal is open, cold-load auto-select of the same packet keeps ?viewPath=1 …
M2b: keep viewPath regardless of the modal viewPath: dropped once the modal is closed …, Clear Filters on a detail URL drops viewPath=1 when the modal is closed
M2c: keep viewPath only, drop obs 18 unit tests, e.g. filter change keeps ?obs=: observer
M3: buildPacketsQuery adds obs itself buildPacketsQuery() output is unchanged and never contains detail params, list view (#/packets, no detail) gets no ?obs=, plus 4 detail-writer tests and all 10 tests in test-issue-121-clear-filters-selection.js
M4: no viewPath drop on close close() drops ?viewPath=1 from a #/packets/<hash> URL …
M5: cold-load obs read removed E2E only (7/7 fail), e.g. desktop: cold load ?obs=<id> selects that observation …
M6: selectPacket writes the hash itself again updatePacketsUrl() is the only history.replaceState() writer in packets.js; E2E copied URL opened in a new tab … and ?viewPath=1 is kept while the modal is open …

Browser validation

Done with a local server on a fixture copy, with one extra observation added to packet 09f282f70cc1ff60. Steps:

  1. Opened #/packets/09f282f70cc1ff60?obs=2. Observation 2 is selected ("observation 2 of 2"); master selects 509.
  2. Changed the time window to 3 h and the channel to #test. The URL became …?timeWindow=180&channel=%23test&obs=2.
  3. Opened that URL in a new tab. Observation 2 is selected, the filters are restored, and the URL is unchanged.
  4. Clicked observation 509. The URL became …?timeWindow=180&channel=%23test&obs=509.
  5. Clicked Clear. The URL became #/packets/09f282f70cc1ff60?obs=509 and the Clear button is hidden.
  6. Opened …?timeWindow=60&obs=2&viewPath=1 with a full reload. The modal is open and the URL is intact. Clicking × gave …?timeWindow=60&obs=2.

Not covered and follow-ups

  • Desktop detail close (closeDetailPanel(), × or Esc on the split pane) does not write the URL, on master or here. After closing, #/packets/<hash>?obs=… stays in the address bar. This is outside fix(packets): updatePacketsUrl drops ?obs= and ?viewPath= on load and on filter changes #147 and a candidate follow-up.
  • Standalone page #/packet/<id>: an observation-row click there rewrites the URL to #/packets/<hash>?… without navigating. That is existing behaviour; it now also carries the packets page filter params.
  • View Path button: opening the modal with the detail pane's button does not add viewPath=1. The param is only kept when it was already in the URL.
  • No deploy. Staging and prod were not touched.

🤖 Generated with Claude Code

dborup and others added 2 commits October 2, 2026 15:11
Red on master 727efca:
- test-issue-147-packets-url-detail-params.js (24 of 29 fail): runs the
  real buildPacketsQuery/updatePacketsUrl/Clear handler in vm. Cold load,
  every filter type and Clear Filters drop ?obs= (and ?viewPath= while the
  View Path modal is open); selectPacket, the observation-row click and
  the detail onClose write the hash themselves and drop the filter params.
  buildPacketsQuery() golden output (= master) and the "list view gets no
  obs" / "viewPath never invented" guards pass on master by design.
- test-packet-path-map.js: closing the modal must drop ?viewPath=1 from a
  #/packets/<hash> URL (1 new red, 2 guards green).
- test-issue-147-packets-url-obs-e2e.js (7 of 7 fail): desktop + mobile
  Playwright; also shows that cold-load ?obs= never selects the
  observation (init() parses it from routeParam, which the router has
  already stripped of its query).

test-clear-filters.js: its body-only extract of updatePacketsUrl() gets an
unused `detail` parameter name so it keeps working once the function takes
one; no assertion changed. New tests registered in test-all.sh and in the
deploy.yml unit/E2E steps (test-packet-path-map.js was not run in CI yet).

Relates to #147

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
updatePacketsUrl() is now the only writer of #/packets... and adds the
packet-detail params that buildPacketsQuery() does not own after the
filter params:

- ?obs= stays with the detail subpath on cold load (init()), every filter
  change and Clear Filters.
- ?viewPath=1 describes the open View Path modal, so it is kept only while
  #packetPathModal is open on the same packet; PacketPathMap.close() drops
  it from a #/packets/<hash> URL (other params verbatim) so a refresh or a
  copied link does not reopen a closed modal.
- selectPacket() (incl. the cold-load auto-select), the observation-row
  click and the SlideOver onClose pass the new detail state as
  updatePacketsUrl({ subpath, obs }) instead of writing the hash
  themselves, so they no longer drop the active filter params (or
  viewPath on cold load).
- init() reads ?obs= from the hash query: the router strips the query from
  routeParam, so the old routeParam parse never ran and a cold-loaded
  ?obs= link always showed the first observation.

buildPacketsQuery() is unchanged (golden test). Per call the work is a
split of the current hash query and one getElementById: O(params), not
O(packets), and no API calls.

test-issue-147-packets-url-obs-e2e.js: catch a detail refetch still in
flight when the context closes and unroute before closing it (a teardown
race seen locally, 1 in 6 runs before; 5/5 green after).

Relates to #147

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dborup and others added 2 commits October 3, 2026 08:36
Red on d0a9f28:
- Escape with the View Path modal open closed the detail pane (desktop)
  or the SlideOver (800 px) too; it must close only the modal.
- The detail's View Path button did not write ?viewPath=1, so a reload
  did not reopen the modal.
- The standalone #/packet/<id> page ignored ?obs= on load, and an
  observation click rewrote its URL to the packets list URL.
- updatePacketsUrl() wrote the list URL on any route; open() on an open
  modal dropped ?viewPath=1; the modal did not name its packet.

New guards (green on d0a9f28; each catches a named mutant): an
observation-row click and a SlideOver close keep the active filters in
the URL (E2E, real clicks), viewPath only for a modal on the subpath's
packet, and the default route is still written.

The E2E closes the SlideOver with a real backdrop click: at 800 px its
close button sits under the fixed top nav, so a real click on it hits
the nav (pre-existing, CSS untouched).

Relates to #147

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es viewPath (#147)

Review findings on #167:

- Escape: the View Path modal's handler now runs in the capture phase
  and stops the event. With the modal open, Escape closes only the
  modal and drops ?viewPath=1; the detail pane (desktop) or the
  SlideOver (641–1023 px) and ?obs= stay until the next Escape.
- View Path button: after opening the modal it calls updatePacketsUrl(),
  which now writes ?viewPath=1 while the modal is open on the packet of
  the #/packets/<hash> subpath (the overlay records it in data-hash). A
  reload reopens the modal; closing drops the param. open() on an open
  modal replaces it without the close-side URL write, so viewPath stays.
- Route guard: updatePacketsUrl() writes only while the packets list is
  the route (#/packets…, or the empty default route).
- Standalone #/packet/<id>: a new writer, updatePacketPageUrl(), writes
  #/packet/<id>?obs=<id> on an observation click (other params verbatim);
  the page reads ?obs= on load. The router strips the query before
  resolving the route, so this fits its format and the URL keeps
  describing what is shown.
- The "only writer" comment names the other hash writers
  (packet-path-map.js dropViewPathParam, touch-gestures.js row actions).

Relates to #147

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

dborup commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Review feedback addressed (commits 223a54ff tests, 4b4163b3 fix)

  1. Escape closes only the top layer (P2). The View Path modal's Escape handler (packet-path-map.js) now runs in the capture phase and stops the event. With the modal open, Escape closes only the modal and drops viewPath=1. The detail pane (desktop) or the SlideOver (641–1023 px) and obs stay, and the next Escape closes them. The E2E now asserts exactly this at 1400 px and 800 px.
  2. Behaviour coverage for the remaining writers (P3). New E2E steps, with real clicks:
    • an observation-row click with an active filter writes the new obs and keeps timeWindow;
    • a SlideOver close at 800 px with an active filter writes the list URL (#/packets?timeWindow=60&hash=<hash>; the list is still filtered to the packet's hash).
    • The SlideOver is closed with a real backdrop click. At 800 px its × sits under the fixed top nav, so a real click on it hits the nav. This is pre-existing; CSS is untouched here.
  3. Standalone #/packet/<id> (P3).
    • updatePacketsUrl() now writes only while the packets list is the route (#/packets…, or the empty default route).
    • A new writer, updatePacketPageUrl(), writes #/packet/<id>?obs=<id> on an observation click there, keeping other params. The standalone page reads ?obs= on load.
    • Why this option: the router strips the query before it resolves #/packet/<id>, so the param fits its format. Because the page reads it back, the URL describes what is shown, as the deep-link rule requires.
    • The View Path button on the standalone page opens the modal without writing the URL. That page does not read viewPath.
  4. The "only writer" comment now says it applies to packets.js, and it names the other hash writers:
    • packet-path-map.js dropViewPathParam() removes only viewPath=1, so obs and the filters stay verbatim;
    • touch-gestures.js row actions navigate to a new list query or a new selection. That re-runs init(), whose cold-load call writes the restored filters back.
    • Neither works against obs or the filters in the flows fix(packets): keep ?obs=/?viewPath= on packet detail URLs (#147) #167 covers.
  5. View Path button writes viewPath=1 (user decision).
    • updatePacketsUrl() writes viewPath=1 while the modal is open on the packet of the #/packets/<hash> subpath. The overlay records it in data-hash.
    • The button calls it after opening the modal. A reload reopens the modal, and closing drops the param.
    • open() on an already open modal replaces it without the close-side URL write, so viewPath stays.

Tests (local; CI fixture prepared as in deploy.yml: freshen, seed SQL, migrate, seed-2073):

  • test-issue-147-packets-url-detail-params.js 36/0. The new tests are red on d0a9f28e (7 failed).
  • test-packet-path-map.js 48/0. The new tests are red on d0a9f28e (3 failed).
  • test-clear-filters.js 8/0; test-issue-121-clear-filters-selection.js 10/0; test-packet-filter.js 92/0.
  • test-frontend-helpers.js 705/2. The 2 failures are the favStar baseline, with identical failing lines on master 84c637ea.
  • test-packets.js 115/13, identical failing lines on master.
  • All 105 unit files in test-all.sh, run one by one, give identical results on head and d0a9f28e. Against master, the only difference is the new fix(packets): updatePacketsUrl drops ?obs= and ?viewPath= on load and on filter changes #147 file.
  • test-issue-147-packets-url-obs-e2e.js 12/0, 5 runs in a row, covering desktop 1400, mobile 390 and tablet 800. On d0a9f28e it is 8/4.
  • Related E2E green: filter-ux, 1122 ×2, 1128, 1692, 1486, slideover-1056 27/0, slideover-1168, 1146, path-inspector, gestures-1062, map-modal-fluid.
  • test-e2e-playwright.js stops locally at its 7th test ("Version info lives on Perf dashboard", #navStats timeout). The same happens on d0a9f28e, and fail-fast skips the rest of the file, so locally it is unverified.
  • scripts/check-xss-sinks.sh --diff: clean.

Mutants (each run in a copy, with the server serving the mutated files):

Mutant Caught by
M3b selectPacket writes via window.history['replaceState'] E2E: copied URL in a new tab; Escape (desktop); View Path button + reload
M4 no cold-load obs read E2E: 11 of 12 steps
M7 SlideOver onClose writes #/packets (via window.history['replaceState']) E2E: tablet SlideOver close; tablet Escape
M8 observation row writes the hash itself (via window.history['replaceState']) E2E: observation click keeps filters; standalone page
Escape closes both layers unit: Escape closes only the modal …; E2E: desktop Escape, tablet Escape
View Path button does not write viewPath E2E: View Path button + reload; tablet Escape
Standalone page writes the list URL (route guard removed) unit: 2 route-guard tests; E2E: standalone page
viewPath for a modal on any packet unit: never written for a modal open on another packet, selecting another packet …
Route guard rejects the empty default route unit: the default route (empty hash) …
open() calls close() again unit: open() while the modal is already open keeps ?viewPath=1

M3b, M7 and M8 all evade the static writer-count test, so only behaviour tests catch them.

Browser (local server, real clicks and keys):

  • desktop: View Path button → viewPath=1; reload reopens the modal with obs=502; Escape → modal closed, detail open, obs kept; Escape → detail closed;
  • desktop: observation click → obs=503, timeWindow kept;
  • standalone page: ?obs=502 is selected after a reload; a click → #/packet/<hash>?obs=501 on the same page;
  • 800 px: View Path in the SlideOver → Escape closes only the modal; backdrop click → list URL with the filter.

Side effect: with the route guard, leaving Packets with the SlideOver open (clicking Nodes) now keeps #/nodes. That is #180 part 1. It was checked once in the browser. There is no dedicated test, and the bug was not reproduced on d0a9f28e in this round. #180 part 2 is untouched.

Fork guards: 9 before and after. No deploy.

@dborup
dborup marked this pull request as ready for review October 3, 2026 08:03
@dborup
dborup merged commit bedbe1f into master Oct 3, 2026
6 checks passed
dborup-agent pushed a commit that referenced this pull request Oct 3, 2026
Conflicts were only in test registration:
- .github/workflows/deploy.yml: master's version. The JS unit step now
  runs test-all.sh (#174), so the two explicit test-issue-165 lines are
  no longer needed there.
- test-all.sh: master's version plus the two #165 files, in its new
  `run` format.

public/packets.js (including #167) merged without conflicts.
deploy.yml keeps 9 fork guards. sh test-all.sh: 202 passed, 0 failed.

Relates to #165

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
dborup added a commit that referenced this pull request Oct 4, 2026
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