Repository navigation
ui: add Operations and Audit log pages - #1254
Conversation
|
Thanks for opening this pull request! A SPACE maintainer will take a look. CONTRIBUTING.md explains how we review outside contributions. |
Review — 3 findingsMedium
Low
Verified against |
the-gigi-pplx
left a comment
There was a problem hiding this comment.
Approved. 3 non-blocking findings in the review comment above.
zibi-pplx
left a comment
There was a problem hiding this comment.
One small addition to the existing review.
- [low] The UI copy documents a backend inconsistency as expected behaviour.
ui/src/app/dashboard/operations/page.tsx:257,259: the tooltips tell operators that "Forks are listed under their source sandbox's bare UUID". That is true because fork operations recordtarget_idas the bare source UUID (crates/libs/api/src/fork_operations.rs:986), while every other sandbox operation usessbx_. Suggestion: open a tracking issue for recording forks under thesbx_ID, and reference it from the comment at:133, so the second query and this copy are removed together when it lands.
Withdrawing this approval for now. Any feedback above still stands.
f45c57f to
521216d
Compare
9987790 to
ba981c2
Compare
ba981c2 to
8f4dc49
Compare
The admin API records every lifecycle operation (GET /admin/api/v1/operations, with state, kind, org and target filters) and every admin mutation (GET /admin/api/v1/audit-events), but the console showed neither: a failed or stuck operation, or who changed what, was only visible through curl. Operations lists the newest operations across organizations with their state, target (sandboxes link to their page), organization, surface, recorded user, attempts and result or terminal error, filterable by state, organization, kind and target ID. Audit log lists admin mutations with actor, method, path, status and duration, searchable within the loaded page. Both read at most the API's 250 newest rows.
Operations: - A fork records its source sandbox by the bare UUID; its target now links to the sandbox page, with a tooltip saying why it is listed under that UUID. - The Result column shows the progress message or the terminal error's message, with the error code in a tag and the full error in a tooltip. Only failed and expired operations are shown as faults; canceled is not. - The kind and target ID boxes apply on Enter and on clear, so typed text never looks applied before it is. - Actor emails are looked up at most 6 at a time, and a failed lookup is retried on the next load instead of being remembered as the ID. Both pages drop answers to superseded requests, and a failed load says the rows shown are from the previous filters or load. The Audit log subtitle now says what it records: every admin API request other than GET, HEAD and OPTIONS by a signed-in operator, including refused ones.
- Text typed in the kind or target ID box without Enter is applied when another filter changes, so a box never shows a filter that the rows do not have. - A failed load with no rows to keep reads "Not loaded", on Operations and on the Audit log, instead of "No operations match these filters" or "No audit events". - A fork records its source sandbox by the bare UUID: an `sbx_` target ID now also reads that form and merges the answers, and the box says so. - Actor lookups go through one queue per page, at most 6 in flight across loads, and leaving the page cancels them (`adminApi.get` takes an abort signal). The per-load helper and its test, which had no other callers, are removed.
…t matches - Enter or clear in one text box, and Refresh, applied only that box: type an sbx_ ID in Target ID without Enter, press Enter in Kind, and the rows were not filtered by target while the box still showed it. One function now applies both boxes as typed, from the dropdowns, Enter in either box, clear (with the cleared value passed explicitly) and Refresh; loading still follows the applied filters. - An sbx_ target's UUID is lower-cased before querying: target IDs match exactly, and forks record the bare lower-case UUID. - A fork row's tooltip suggests filtering by the source's sbx_ ID, which also finds forks. - Merged rows (an sbx_ target reads twice) sort by their timestamps at full precision; the operation ID breaks only exact ties. - The audit log says "Not loaded" only when a failed load left no rows. With rows kept from an earlier load and a search that matches none of them, it says "No events match".
The merged Operations list sorted timestamps as text when they had the same length, assuming a fixed fractional width. The API's RFC 3339 strings trim trailing zeros and leave out a zero fraction, so timestamps of different lengths fell back to Date.parse, which keeps whole milliseconds, and the operation ID decided their order. Each timestamp now compares as its milliseconds, then its fraction padded to nine digits; the ID breaks only exact ties. The comparator moves to a library module with a test.
Only an sbx_ ID in Target ID also searched the bare UUID that forks record their source by. A pasted bare UUID went to the server as typed and returned only the forks, and an upper-case one returned nothing. An sbx_ ID or a bare UUID, dashed or not, in either case, is now lower-cased and searched in both forms, and the answers merged.
8f4dc49 to
91bdd9a
Compare
Pull request was closed
Summary
This change adds two read-only pages, each over an admin route the console did not use. Until now a failed or stuck operation, or who changed what, was visible only through curl.
Operations (
/dashboard/operations) usesGET /admin/api/v1/operations. It lists the newest lifecycle operations across organizations. Each row shows:It filters by state, organization, kind and target ID, through the API's own filters, and shows at most the API's 250 newest rows. Every path that applies filters (a dropdown, Enter in either text box, clearing a box, Refresh) applies both the kind and the target ID boxes as typed, so a box never shows a filter the rows do not have. A fork records its source sandbox by the bare lower-case UUID, so a target ID that is an
sbx_ID or a bare UUID, dashed or not, in upper or lower case, is lower-cased and searched in both recorded forms (sbx_<uuid>and the bare<uuid>), and the two answers are merged; any other target ID is searched as typed. The box's tooltip, and a fork row's tooltip, say so. The merged list is newest first: each timestamp is compared by itsDate.parsemilliseconds, then by its fraction padded to nine digits, because the API trims trailing zeros and omits a zero fraction; the operation ID breaks only exact ties. The comparator, the merge and the target forms are inui/src/lib/operation-list.ts, tested inoperation-list.test.mjs.Audit log (
/dashboard/audit) usesGET /admin/api/v1/audit-events. It lists every admin API request other than reads (GET, HEAD, OPTIONS) made by a signed-in operator, including ones the API refused, with its actor, method, path, status and duration (admin_request_should_persist_audit_eventincrates/libs/api/src/routes/admin/mod.rs). Console actions sent through the public API appear under Operations instead. The search box filters the loaded page, because the route has no filters.Both pages drop answers to superseded requests, and a failed load says that the rows shown are from the previous filters or load. When a failed load leaves no rows, the table reads "Not loaded" instead of "No operations match these filters" or "No audit events". When the audit log keeps rows from an earlier load and the search matches none of them, it reads "No events match".
The operations "Recorded for" column is labelled that way on purpose. A create through the admin API records the sandbox's owner there, while the audit log records the operator.
Stack
Stacked on #1246, whose menu changes this adds two entries to. Merge #1246 first.
Validation
npm ci,npm test(60 pass, includingsrc/lib/operation-list.test.mjs),npx tsc --noEmit -p .(no errors) andnpm run buildinui/.space-uiimage):delete_sandboxoperation;root@localas actor;main.src/lib/operation-list.test.mjscovers the ordering, the merge and every accepted target form. Its ordering test fails against the comparator this branch used before: for operationsatodat…00.1234Z,…00.12346Z,…00.12345Zand…00.123451Z, that comparator gaved, b, c, a; the expected order isb, d, c, a. Other cases cover a missing fraction (…00Zsorts below…00.000001Zand above…59.999999999Z) and the ID as the tie-break only for equal instants (.5Zand.500Z).target_id=sbx_<uuid>andtarget_id=<uuid>and listed the sandbox'ssuspend_sandboxandcreate_sandboxoperations; the earlier head sent only the typed form and listed none. The stack has no fork operations and no two operations in one millisecond, so the merged fork rows and the new ordering rest on the unit tests.Visual confirmation
Target ID set to a sandbox's UUID in upper case with dashes, on an earlier head of this branch and on this head: both recorded forms are now searched:
The same UUID in lower case without dashes, before and after:
Severity and risk
Severity: medium. Operations are where a support case's failure is recorded.
Risk: low. Two new pages, read-only. With no organization selected, no index serves the page's sort, and only the state filter has an index that leads with its column (
idx_public_operations_state_next_attempt). The default view and the kind and target ID filters can read the whole table, and a sandbox target ID (ansbx_ID or a bare UUID) sends two such reads. The index is filed separately. Roll back by reverting the commits.AI assistance: Claude Code wrote the code and this description, and ran the validation; a maintainer reviews it.