EVL-145 + EVL-161: Sessions list in React, and the backend fields it needs - #1668
Draft
rogefm wants to merge 15 commits into
Draft
EVL-145 + EVL-161: Sessions list in React, and the backend fields it needs#1668rogefm wants to merge 15 commits into
rogefm wants to merge 15 commits into
Conversation
models.ListSessions did not SELECT s.ai_analysis, so ai_analysis was always null on GET /sessions even though openapi.Session declares it — the list and detail endpoints share that schema. The embedded review object had the same divergence, omitting time_window, access_request_rule_name, min_approvals and force_approval_groups. Practical consequence: a list UI could not render a risk badge without N+1 calls to the detail endpoint. SQL only — no Go changes. Session.AIAnalysis already carries gorm:"serializer:json", SessionReview's custom Scan unmarshals the whole jsonb blob and all four keys already have json tags, and parser.go routes list rows through the same toOpenApiSession/topOpenApiReview mappers as the detail path. Both callers (the HTTP handler and the MCP sessions_list tool) scan by column name, so the added columns are additive for them too. s.origin is included so the two SELECTs read identically; it has no API surface (openapi.Session has no origin field) and is only read on the write path. s.blob_stream_id stays as-is: it is list-only and has no matching struct field, so GORM discards it — removing it is an unrelated cleanup. make generate-openapi-docs leaves gateway/api/openapi byte-identical, as expected: both schemas already promised these fields. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
services/sessions.js grows get, getFilteredByIds, getByBatchId, streamResult,
downloadFile and downloadInput, mapped from the CLJS events in
webapp/src/webapp/events/audit.cljs. The last three have no UI yet — they are
Wave 6 consumers, mapped now while that file is open so EVL-131/EVL-132
inherit the URLs instead of re-deriving them.
list() deliberately keeps returning the raw axios response while every new
method unwraps to .data: useConfigStatusStore destructures { data } off the
response and reads data.total, so unwrapping would not throw — it would
silently pin the sidebar "session ran" check to false. Commented in place.
getFilteredByIds drops the ?event_stream=base64 the CLJS version sent (no list
row renders anything from the event stream, so it downloaded a full base64
transcript per session and discarded it) and the 1000ms dispatch-later (a
uniform delay, not a stagger). It also preserves input order; CLJS prepended
each arrival, so what users saw was HTTP-race order.
getByBatchId uses axios params, fixing the unencoded batch_id CLJS
concatenated into the URI.
utils/datetime.js is the canonical port of formatters/time-parsed->full-date
plus local-day range bounds. CLJS built those bounds as
new Date("YYYY-MM-DD 00:00:00.000Z") — not a valid ISO string, so Safari
returns an Invalid Date and .toISOString() throws a RangeError. It also parsed
bare YYYY-MM-DD as UTC midnight, which lands on the previous day west of UTC;
parseDateInput builds from parts to avoid that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pages/Sessions/store.js holds three slices — list (URL-filtered), lookup (the ID search box and /sessions/filtered?id=) and batch (?batch_id=) — because their lifecycles genuinely differ. v1 collapsed the last two into one :audit->filtered-session-by-id key and they fought over it. Pagination is offset+append with a dedupe by session id. v1 refetched the whole query with limit = count + 20, which dead-ends at the gateway's 100 cap while has_next_page keeps reporting true, leaving a button that renders forever and does nothing. hasMore is derived from total rather than has_next_page (which is len(items) == limit, so wrongly true on every exact multiple) and additionally requires the last append to have added something — with ORDER BY created_at DESC and sessions arriving mid-paging, the window shifts and total grows. patchSession is the EVL-132 seam: it fans a partial update across all three slices and preserves slice identity where the id is absent, so a live tail on one surface never re-renders another. The reader itself must live in a module-level variable, never in store state — same idiom as useConfigStatusStore's inFlight. useSessionFilters keeps the query string as the single source of truth, as v1 did by reading window.location.search inside the event handler. Two differences: FILTER_KEYS is an allow-list (v1 forwarded every URL param to the API and relied on the gateway ignoring unknowns), and the fetch effect depends on a primitive queryKey projection rather than on the filters object, which is what keeps it from looping. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ports audit/views/{main,sessions_list,session_item,audit_filters}.cljs. The
route goes live immediately; /sessions/:id deliberately gets no React route and
keeps falling through the catch-all to the CLJS dedicated page (already in
production via Share links, and self-fetching) until EVL-132 flips it.
Rows are a real table with a header (User / Resource Role / Status / Started)
rather than v1's headerless 4-column grid of divs, matching every other
migrated page. Rather than putting role="link" on the <tr> — which would strip
its row semantics — the user cell holds a real anchor, so keyboard users get a
focus target and everyone gets open-in-new-tab. v1 rows had no tabIndex, role
or key handler at all.
The correlation-ID control is a faithful port of v1's Workflow popover: a
navigator to /workflows/:id, not a list filter, even though the gateway does
accept correlation_id.
Bugs fixed while porting, all of them v1 behaviour:
- no error state at all (:audit->get-sessions had no :on-failure, so a failed
fetch left the spinner running forever) — now an error state with a retry
- REVOKED / PROCESSING / EXECUTED / UNKNOWN reviews rendered nothing, making a
revoked session indistinguishable from an unreviewed one; every status is
mapped and the fallback shows the raw string
- a null user_name threw in the avatar and took the whole list down
- Enter in the ID search fired immediately *and* left the debounce running,
costing 2N requests
- "Load more" was an <a href="#"> with no preventDefault
The ID search is also capped at 50 ids, since each one costs a request, and
now surfaces ids that failed to load instead of silently omitting them.
usePaginatedConnections yields { value: connection.id }, but the gateway
matches ?connection= against the connection NAME — the Resource Role filter
remaps so the option value is the name. Passing the id writes a UUID and
silently returns zero rows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ports audit/views/sessions_filtered_by_id.cljs: sticky header, Share link, client-side search over connection/type/id/user_name, and real offset-based infinite scroll in batch mode (the id-list mode has no server pagination, same guard as v1). batch_id wins over id when both are present. Share uses the existing CopyButton, which already returns null when disable_clipboard_copy_cut is set — exactly the gate v1 put on its own button — and the URL is built with URLSearchParams, so a batch id containing & or a space now round-trips. v1 fetched twice for ?id= because both the panel (app.cljs:543-551) and the component dispatched the same event, costing 2N requests. One effect here, plus the store's key guard, which also covers StrictMode's double-invoke. With neither batch_id nor id in the URL v1 rendered a blank page; this shows the empty state explaining what the page needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MIGRATION_ROADMAP.md now sequences Wave 1 → 2 → 6 → 3 → 4 → 5 → 7 → Endgame. Wave 6 (session details) is pulled ahead because the "After Execution" work in the Major Features Visibility project depends on the reworked detail view (EVL-165). Wave labels are frozen — B3.1, B6.1 and the rest are live Linear ticket IDs — so the section order carries the schedule and a note at the top of Track B says so. Recorded with the move: only the jira prompt-gate slice of B5.1 is a Wave 6 prerequisite (parallel_mode, 1,393 LOC, stays in Wave 5), and B6.1's credentials block goes through useBridgeStore.resumeNativeClientCredentials to /resources until B4.0/EVL-123 lands, because dispatching the CLJS flow while that tree is parked would open the modal invisible. Coupled references updated so the doc doesn't contradict itself: the B2.1 row (now done), B4.0's Unblocks, B5.1's split scope and B5.2's rationale, the reviews_plugin.cljs deferral in Track A, the Track C "When" column (MS Clarity pinned to Wave 2 — "Wave 2–3" would now drift months later), the sequencing fence, and Top Risks, where playback fidelity is promoted to #1 since it loses most of its schedule slack. CONTEXT_MIGRATION.md: /sessions and /sessions/filtered marked React/Done, with /sessions/:id listed explicitly as intentionally still CLJS. COMPONENTS.md: documents the extended sessionsService (including why list() alone returns the raw response) and utils/datetime.js, and fixes four inaccuracies in files this PR consumes — AsyncValueFilter takes option objects rather than labels and scrolls with ScrollArea rather than useIntersection, useMinDelay is a named export, and Table striping is opt-in (that last one was wrong in the component's own JSDoc, not in COMPONENTS.md). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rogefm
force-pushed
the
rogerio/evl-145-b21-sessions-list-sessions-sessionsfiltered
branch
from
August 4, 2026 15:36
d2ff496 to
9a16f8c
Compare
Contributor
Migration Safety AnalysisNo database migrations were changed in this PR. Safe to deploy to sandbox. |
Contributor
📋 API ChangelogAPI Changelog unknown vs. unknownNo changes detected |
19 tasks
Contributor
|
✅ Build Completed with Success, Version=1668.0.0-g9a16f8c |
19 tasks
Feedback from running the page against the ClojureScript original. Four
corrections, three of which reverse choices I had proposed from ASCII mockups —
seeing it rendered is what caught them.
Table -> list. v1 was never a table: main.cljs wraps the rows in a plain
`rounded-lg border` container and each row is a Radix `Grid columns="4"` with a
bottom border and no header, so the column labels I added were inventing chrome
the original never had. SessionsTable is now SessionsList, built from Paper +
Grid. The real anchor in the user cell stays — it is what gives the list
keyboard access and open-in-new-tab, and it renders as plain text, so it costs
nothing visually.
Badge colours. v1 renders `[:> Badge {:color "green" :size "2"}]`, and Radix's
default Badge variant is `soft` — tinted, not solid. I had mapped them onto the
semantic active/warning/danger variants, which are filled; solid yellow in
particular is close to illegible. Now colour + `variant="light"`, the Mantine
equivalent of soft.
Filter label weight. Not a font-weight problem — v1 uses `font-semibold` too.
Mantine's `default` button variant ignores `color` and paints the label with
--mantine-color-text (near-black), where the legacy Radix trigger was
`color="gray"`. Fixed with `c="dimmed"` on both ValueFilter and
AsyncValueFilter. Note the blast radius: those are shared, so the Rulepacks and
Data Masking filters lighten too. That matches v1 everywhere, but it is a
change outside Sessions.
Screen flashing on every request. `lookupByIds` and `fetchBatch` reset their
slice to EMPTY_* when entering `loading`, which empties the list and hands the
whole content area back to PageLoader — so each ID search or batch fetch blanked
the page. Both now keep the previous rows mounted while loading, matching v1,
which only ever shows its spinner when the list is empty (main.cljs:57-64). The
main list already preserved its items, so filter changes were not affected by
this path.
Session details stays a full-page navigation rather than the v1 modal. A bridge
to the CLJS modal is not an option — on a React-only route the CLJS tree is
parked hidden by ClojureApp, so the modal would open invisible (same constraint
as the credentials block in EVL-123). It returns in EVL-131, in the DES-27
layout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page component subscribed to the `list` and `lookup` slices, so every response re-rendered the title, the count and the entire filter bar along with the rows. v1 never did that — re-frame repaints only the subtree that is subscribed — and it is what made the screen flash on each request. Split the two pieces that depend on fetched data into their own subscribers: `sections/SessionsResults` (loader / empty / error / list / load-more) and `sections/SessionsCount`. The page itself now subscribes to nothing but the stable `fetchList` action, so it re-renders only when the URL params change, which is exactly when the filter bar genuinely needs to update. SessionsCount also keeps the previous numbers on screen while a refetch is in flight, so the header does not collapse and reflow the page under the user. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First slice of the full session-details port. Row click opens a modal again, as in v1, instead of navigating away. The store gains a `detail` slice implementing v1's two-phase fetch verbatim (:audit->get-session-by-id + :audit->check-session-size, events/audit.cljs 206-273): phase 1 is a probe that exists only to read `event_size` and `script_size`, phase 2 re-fetches with an `expand` derived from them, and when both exceed the 4 MB threshold the second request is skipped entirely. Two round-trips for one modal is wasteful, but the sizes are not on the list payload and this is what v1 does. `SessionInfo` ports sessions/components/session_details.cljs (259 LOC): the pinned two-column rows, the review-group list, and the See more/See less expansion — which resets on mount, same as v1. Two things deliberately NOT ported: - `review-status-icon` / `review-status-text` (session_details.cljs:98-123) are dead code. Grep across src/ finds only their own definitions, and `review-status-icon`'s methods take zero args while the `identity` dispatch passes one, so calling it would throw. The live rendering is `review-group-item`, which is what SessionInfo follows. - `user-name-initials` (session_details.cljs:114) throws on a nil user_name; `initialsFor` guards it. This one is a fix, not parity — a render crash takes the whole modal down. The row anchor still points at /sessions/:id so keyboard access and open-in-new-tab keep working; a plain click is intercepted and opens the modal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ports the review block (session_details.cljs:220-267 and 408-458) plus its two sub-modals (reject_details_modal.cljs 32 LOC, time_window_modal.cljs 51 LOC). `canReview` and `canForceApprove` follow v1 exactly, including the branch where the force-approve set comes from the connection's `force_approve_groups` when the review carries no `access_request_rule_name`, and from the review's own `force_approval_groups` otherwise. That is the only reason the modal fetches `GET /connections/:name` at all. All four actions go through `reviewsService.addReview` → `PUT /reviews/:id`: approve, reject with a comment, force approve, and approve with a time window. Time windows are converted to UTC before sending and rendered back in local time, matching `local-time->utc-time` / `utc-time->display-time` — both now in utils/datetime.js. The 500ms delay before refetching is v1's (events/audit.cljs:560-566); the gateway propagates the status asynchronously. Two deliberate divergences: - The sub-modals stack instead of replacing. v1 has a single global modal slot, so opening either one destroyed the session-details modal underneath and ran its cleanup — the user never got back to the details after confirming. That reads as an implementation limit, not intent. - The time-window modal keeps v1's "continues to the next day" copy even though v1's own `is-within-time-window?` compares plain minutes-of-day and does not wrap. The gateway owns that behaviour; the copy is unchanged. Also fixes a bug introduced a moment earlier: the post-review list refresh passed empty filters, which would have silently dropped the user's filtering. The list slice now remembers its filters and exposes `refreshList`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ports the self-contained half of sessions/components/session_header.cljs (133 LOC) — a separate CLJS file from session_details.cljs, which the modal composes. View Timeline, Kill Session and Share, with v1's visibility rules intact: kill requires owner-or-admin on a non-done exec session, share hides when the gateway sets disable_clipboard_copy_cut, and the timeline opens in a new tab from the modal so the session stays open behind it. `killing-status` is component state here. In v1 it is a module-level atom (session_header.cljs:37) shared by every header instance, so two open sessions would have shown each other's spinner. Re-run is deliberately left out of this commit. Its script branch routes through the Jira template gate (events/audit.cljs:444-447), and shipping the button without that would silently skip a required prompt on any connection that has a template configured. It lands with the jira-templates port. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ection Ports the four independent info blocks and wires them into the modal in v1's order (session_details.cljs:349-355). Built in parallel by four agents, each reviewed against its ClojureScript original by a second agent; the findings below are theirs. Three defects the review caught and this commit fixes: - The AI analysis card swallowed unrecognized `action` values. v1 nests its `case` inside the bordered Box, so a truthy-but-unknown action still renders the separator and its spacing. Gating both on the lookup dropped them. - `color="orange"` (medium risk) is not in the theme, which defines only indigo/gray/green/amber/red/sky — it would have fallen back to Mantine's stock palette, bypassing the theme. Now `amber`. - The AI analysis and Guardrails cards had drifted apart: same root class in v1 (`w-full p-3 bg-[--gray-1] rounded-md border border-[--gray-3]`, rendered adjacent), different chrome here. They now share `components/InfoAccordion`, which also corrects the surface — Radix `--gray-1` is body-white, not the tinted `gray.0`, and Mantine's `contained` variant hardcodes a mid-slate item border that is unreachable from `--mantine-color-default-border`. The redaction report is fetched alongside the session, as v1 does from :audit->check-session-size, and never blocks the modal: data masking falls back to `session.metrics.data_analyzer` when the report is unavailable — again v1's own behaviour. Two things flagged and deliberately left as-is: - `violet` is also outside the theme palette. v1 uses Radix Violet for the data masking card and Mantine's stock violet is close, so it stays; the clean fix is adding a violet scale to theme.js, which is a design decision. - `RejectionReason.jsx` was swept into the previous commit (dce7aa4) by a `git add -A` that raced the agents writing it. Content is unaffected; the commit message there just does not mention it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ports session_details.cljs:295-346 plus `large-input-warning` (:46-65), in v1's block order. Metadata deliberately hides `credentials_expire_at`, `credentials_revoked_at` and `credential_session`: v1 excludes them from the rows because they drive the credentials block and the "Credentials Session" detail row instead, and the whole section disappears when nothing else remains. Values that parse as http(s) URLs render as links, matching v1's `is-url-http` check. The script area swaps for a download callout above the 4 MB input threshold — past that the gateway does not send the script at all. The download button honours the gateway's `disable_sessions_download`, which the user store did not expose; it now does, alongside `disableClipboard`. One divergence: v1 throws when `labels.runbookParameters` is malformed JSON. Here that is treated as absent. The strip is informational and not worth taking the whole modal down for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ports the results stack: results_container.cljs (94), ag_grid_table.cljs (153), logs_container.cljs (108) and results_download_menu.cljs (149). The three leaf components were built in parallel by agents and each reviewed against its CLJS original by a second agent. ag-grid is lazy-loaded. It lands in its own 1,026 kB chunk (276 kB gzipped) that only downloads when someone opens the SQL "Table" tab; the main bundle goes from 351 to 375 kB gzipped, and those 24 kB are papaparse + fancy-ansi + the new Sessions code, not the grid. Importing it eagerly would have roughly doubled the main bundle for every user. This mirrors how Router.jsx already defers the Dashboard's recharts. Worth knowing for planning: this does NOT remove ag-grid from the CLJS bundle. It has two independent consumers there — results_container (ported here) and webclient/log_area/main.cljs, the SQL editor at /client, which is Wave 7. Both bundles carry it until then. The roadmap already anticipated this: B6.2 lists a "shared AgGrid table wrapper (also serves the webclient)". Also adds `decodeB64`, a faithful port of utilities/decode-b64 including the escape/decodeURIComponent recovery and the ∞→tab substitution, and wires /result/stream into the store for payloads over the 4 MB threshold, where the session response omits the event stream entirely. The `connect` branch of the output section is not implemented yet — that is asciinema, the RDP canvas and the SSE live tail, which land with the playback commit. Exec sessions, which is what the output section mostly serves, are complete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The verification pass on 7129fc9 found real problems, two of which would have been obvious on first use. Blank output pane. LogsContainer gated on `status === 'ready'` while every caller passes `'success'`, so the Plain-text tab — the default tab for both SQL and non-SQL sessions — rendered "No logs to show" for every exec session with output. The port had renamed the vocabulary to the store's idle/loading/ready, on the false premise that the status came from the store; it is computed locally in SessionOutput. Now on v1's vocabulary (logs_container.cljs:15,21) with a comment at the seam, since that is where the two conventions meet. Wrong colour scheme. results_container.cljs:34 passes its arguments positionally — `results-heads results-body false true` — so `dark-mode?` is TRUE and v1's session grid is dark blue. ResultsContainer passed nothing and AgGridTable defaults to light, so the grid rendered light warm. Auto-sizing that v1 does not do. v1 asks for `:auto-size-columns? true`, but its implementation gates on `params.columnApi`, which ag-grid 33 removed — the branch is dead and columns are never auto-sized today. The port called `params.api.autoSizeAllColumns()` on `gridReady`, which both changes behaviour and fires before rows paint, so it would size to headers only. Defaulted off to match what users actually see; the intent is noted at the call site. Screen-reader label. v1 labels the output trigger "Output options"; the ActionMenu wrapper hardcoded "Actions" with no way to override. Added an `ariaLabel` prop — this also helps Settings/ApiKeys and AiAgentsIdentities, which render lists of identical "Actions" buttons. Lazy-load confirmed after wiring: ag-grid is a separate 1,026 kB chunk (276 kB gzipped); the main bundle is 375 kB gzipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warning
Do not merge without the session-details modal. Shipping the list alone
changes a user-facing behaviour — a row click opens a modal in v1, and would
navigate to a full page here — and adding the modal later would change it a
second time. Two breaks where there should be none. The full port of
session_details.cljslands on this branch before this is mergeable; scope andprogress below.
📝 Description
Ships EVL-145 (React Sessions list) and EVL-161 (the gateway fields it needs) together.
/sessionsand/sessions/filteredare now React./sessions/:iddeliberately gets no React route — it keeps falling through the catch-all to the CLJS dedicated page, which is already in production via Share links and self-fetches its own data (app.cljs:514-521). EVL-132 flips it. No feature flag; exposure is route registration.On the backend,
models.ListSessionsnever selecteds.ai_analysis, so it was alwaysnullonGET /sessions— even thoughopenapi.Sessionhas always declared it, because list and detail share that schema. The embeddedreviewobject had the same divergence. A list UI could not render a risk badge without N+1 calls to the detail endpoint.Zero CLJS files change in this PR:
webapp/src/webapp/app.cljsis owned by #1665, so the CLJS:sessions-panelstays registered. Harmless — React Router's exact route wins.📣 User-facing impact
The Sessions list is now a React page: filters round-trip through the URL so a filtered view can be bookmarked and shared, pagination works past 100 sessions, rows are keyboard-navigable and open in a new tab, and revoked/processing/executed reviews are finally visible instead of rendering an empty cell.
🔗 Related Issue
ai_analysis(and the missing review fields) onGET /sessions🚀 Type of Change
📋 Changes Made
Backend (EVL-161) —
gateway/models/session.go, SQL only, two hunks in the rows query ofListSessions:s.ai_analysisands.originto the SELECTtime_window,access_request_rule_name,min_approvals,force_approval_groupsto the reviewjsonb_build_objectNo Go changes are needed:
Session.AIAnalysisalready carriesgorm:"serializer:json",SessionReview's customScanunmarshals the whole blob and all four keys already have json tags, andparser.goroutes list rows through the sametoOpenApiSession/topOpenApiReviewmappers as the detail path. Both callers — the HTTP handler and the MCPsessions_listtool — scan by column name, so the extra columns are additive (MCP'ssessionToMapalready emittedai_analysisconditionally; it just never had a value).Two deliberate decisions worth a reviewer's attention:
s.originis included for literal alignment only. It has no API surface —openapi.Sessionhas nooriginfield — and is otherwise read only on the write path.s.blob_stream_idis left alone. It is list-only, has no matching struct field, and GORM discards it. Removing it is an unrelated cleanup that would muddy this diff.Frontend (EVL-145) —
webapp_v2/src/pages/Sessions/plus an extendedservices/sessions.jsand a newutils/datetime.js. Portsaudit/views/{main,sessions_list,session_item,audit_filters,sessions_filtered_by_id}.cljsand ~150 of the 786 LOC inevents/audit.cljs.events/audit.cljsis not deleted — the other ~636 LOC is Wave 6.Docs —
MIGRATION_ROADMAP.mdreordered to Wave 1 → 2 → 6 → 3 → 4 → 5 → 7 → Endgame (Wave 6 pulled ahead for EVL-165), plusCONTEXT_MIGRATION.mdandCOMPONENTS.md.Two deliberate deviations from v1
limit = count + 20refetch. The gateway clampslimitat 100 (session.go:46) and computeshas_next_pageaslen(items) == limit, so past 100 rows v1's button renders forever and adds nothing.hasMoreis derived fromtotaland additionally requires the last append to have added something — withORDER BY created_at DESCand sessions arriving mid-paging, the offset window shifts andtotalgrows.Rows are a real table with a header— reverted after seeing it rendered. v1 was never a table:main.cljswraps the rows in a plainrounded-lg bordercontainer and each row is a RadixGrid columns="4"with no header. The column labels were inventing chrome the original never had, so the list is back toPaper+Grid. The real anchor in the user cell stays — it is what restores keyboard access, and it renders as plain text.Bugs fixed while porting
All of these are current v1 behaviour:
:audit->get-sessionsregistered no:on-failuresession_item.cljs:31-32:defaultreturnedniluser_namethrew and took the whole list downRangeErroron Safarinew Date("YYYY-MM-DD 00:00:00.000Z")is not a valid ISO string (audit_filters.cljs:59-63)/sessions/filtered?id=fetched everything twiceapp.cljs:543-551and the component dispatchedbatch_idcontaining&broke the fetch and the Share link#to the URL and jumped scroll<a href="#">with nopreventDefaultdivs with notabIndex/role/key handlerRather than
role="link"on the<tr>(which strips row semantics), the user cell holds a real anchor — keyboard focus, accessible name, and open-in-new-tab.Visual-fidelity pass after manual testing
Running the page side by side with the CLJS original turned up four divergences, all fixed in
747b2079:[:> Badge {:color "green" :size "2"}], and Radix's default Badge variant issoft. I had mapped them onto the semanticactive/warning/dangervariants, which are filled — solid yellow especially is near-illegible. Nowcolor+variant="light".font-semiboldtoo. Mantine'sdefaultbutton variant ignorescolorand paints the label with--mantine-color-text(near-black) where the Radix trigger wascolor="gray". Fixed withc="dimmed". Blast radius:ValueFilter/AsyncValueFilterare shared, so the Rulepacks and Data Masking filters lighten too. That matches v1 everywhere, but it is a change outside Sessions.lookupByIdsandfetchBatchreset their slice toEMPTY_*on enteringloading, emptying the list and handing the content area back toPageLoader. Both now keep the previous rows mounted, matching v1 (main.cljs:57-64only shows its spinner when the list is empty). The main list already preserved its items.Session details stays a full-page navigation, not the v1 modal. A bridge to the CLJS modal is not available: on a React-only route the CLJS tree is parked hidden by
ClojureApp, so the modal would open invisible — the same constraint documented in EVL-123. It returns in EVL-131, in the DES-27 layout.Other notes for reviewers
/sessions; CLJS called/plugins/audit/sessions. Same handler (server.go:805is an alias) — flagged so nobody diffing network tabs files a parity bug.usePaginatedConnectionsyields{ value: connection.id }, but the gateway matches?connection=against the connection NAME. The Resource Role filter remaps so the option value is the name; passing the id writes a UUID and silently returns zero rows. Commented at the call site.FILTER_KEYSis an allow-list. v1 forwarded every URL param to the API and relied on the gateway ignoring unknowns./sessions?batch_id=therefore no longer filters — nothing links to it;/sessions/filtered?batch_id=is the supported URL and is unaffected.getFilteredByIdsdrops?event_stream=base64. No list row renders anything from the event stream; v1 downloaded a full base64 transcript per session and discarded it.sessionsService.list()still returns the raw axios response while every new method unwraps to.data.useConfigStatusStorereadsdata.totaloff the response — unwrapping would not throw, it would silently pin the sidebar checklist tofalse. Commented in the file and inCOMPONENTS.md.downloadFile/downloadInputfollow v1 in usingGET /sessions/:idwith blob params rather than the dedicated/downloadroutes (server.go:814-815). Wave 6 should choose deliberately rather than inherit this.🧪 Testing
Test Configuration:
Tests performed:
make test-ossgreenProof the SELECT fix reaches the wire — every review key in the detail response must also appear in the list response:
How to test
#in the URL, and the button disappears at the true end. Worth testing an org with exactly 20/40/60 sessions (thehas_next_pagebug) and one with >100 (where v1 dead-ends).key=. Confirm Resource Role puts a name in?connection=, not a UUID, and that rows actually narrow. Deep-link a fully-populated filter URL in a fresh tab; hit Back after three filter changes.RangeError, params areT-separated ISO.event_streamparam, rows in typed order; Enter mid-debounce still fires N not 2N; a bogus id raises a snackbar; clearing restores the filtered list with no extra fetch./sessions/:idpage fully loaded, no blank flash. Back restores the React list with filters, rows and loaded pages intact. Hard-reload/sessions/:idand confirm CLJS boots cold. Also try the CLJS page's own back/close to confirm the handoff to React/sessions(this seam can't be covered by CI —:sessions-panelis still registered on the CLJS side becauseapp.cljsis frozen by EVL-172 + EVL-159: show which features are active on a resource role in the Terminal #1665)./sessions/filtered?batch_id=X— sticky header, infinite scroll with no duplicates, Share copies a working URL (test a batch id containing&), Share is absent whendisable_clipboard_copy_cutis on, search filters without a request.?id=a,b,cfires exactly 3 requests with StrictMode on.?batch_id=X&id=a,b→ batch wins.connectsession pulses Live; a REVOKED review shows a visible badge; a machine-identity session with a nulluser_namerenders./sessions: an error with a retry, not an eternal spinner.sessionsService.liststayed raw).✅ Checklist
📄 Additional Notes
Reviewer focus, in order:
s.origin/s.blob_stream_idcalls in the backend commit — both are judgement calls, both explained above.SessionsFilterBar.jsx. It fails silently if wrong: the chip shows a plausible name and the list just returns nothing.Wave Nmentions were re-read in the new order.Deliberately out of scope:
pages/Settings/AuditLogskeeps its own copy of the date formatter (utils/datetime.jsis now the canonical one, noted inCOMPONENTS.mdfor whenever that page is next touched).