Skip to content

ui: show a sandbox's placement, timeline, operations and snapshots - #1245

Merged
ac-pplx merged 8 commits into
mainfrom
ac/admin-ui-sandbox-detail
Nov 2, 2026
Merged

ac-pplx merged 8 commits into
mainfrom
ac/admin-ui-sandbox-detail

Conversation

@ac-pplx

@ac-pplx ac-pplx commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The sandbox detail page now shows what the control plane records about a sandbox, not just the row's current fields. Operators can answer "where is it running, what happened to it, and who did that" without leaving the console.

The page reads two admin routes that already exist and already sit behind the operator gate:

  • GET /admin/api/v1/debug/sandbox/{uuid}
  • GET /admin/api/v1/operations?target_type=sandbox&target_id={id}&org_id={org}, read twice: once with the sbx_ ID, and once with the bare UUID, under which a fork records its source sandbox.

It adds these cards:

  • Placement and pods: active and target node (Kubernetes node name, role, address), and every pod with its name (copyable), namespace, node, generation, state, IP, and created and terminated times.
  • Timeline: the debug route's merged sandbox, observed-state, pod, snapshot and port events, newest first. The debug route returns at most the newest 200 events, and the card says so when it reaches that cap.
  • Operations: each lifecycle operation with its kind, state, API surface, recorded user, attempts, and progress or terminal error, including forks of this sandbox. The newest 50 are shown, and the card says when older ones exist. It also says that finished operations are removed 7 days after they were requested and unfinished ones are kept (DEFAULT_OPERATION_GC_DAYS in crates/libs/api/src/public_operations.rs sets gc_after, and terminal operations past it are deleted). The card looks up each distinct recorded user once, not once per row.
  • Resources and runtime: vCPU, memory and disk (or "template default"), network counters, last observation, missed node reports, environment variable names, metadata, and port exposures. The debug route returns the environment variables' values to the browser along with everything else; the page shows only their names, because values can hold credentials, but the values are in the response an operator's browser receives. Redacting them in the route is a separate backend change and not part of this PR.
  • Snapshots: the existing card reads the public snapshot route, an org-scoped hidden operation. For another org's sandbox, or wherever snapshot operations are not enabled, it said "Snapshots are not enabled for your organization", even for a suspended sandbox whose only copy of its state is a ready snapshot. It now falls back to the snapshots the debug route reports (state, codec, age, size, tier, failure), read-only, and says why rollback is unavailable. Where the public listing works, the card and its rollback are unchanged. For another org's sandbox the card no longer calls the org-scoped public route. Until a listing has loaded, any listing failure other than the hidden-operation answer shows a warning with the error above the recorded snapshots, so an outage does not look like "not enabled". A failure after a good listing keeps that listing and its Rollback column, with a warning above it naming the error and when the shown listing loaded; once a listing has loaded, only the hidden-operation answer switches the card to the read-only view.

Other changes:

  • The header shows the sandbox name and full sbx_ ID (copyable). Org and Owner link to the org and user pages by name and email.
  • A deleted sandbox's "not found" page lists the operations recorded against its ID, and keeps refreshing them. Those rows outlive the sandbox row, so the page can now answer "who deleted this and when". It says "the sandbox row is gone" only for a 404; any other failure reads "Could not load the sandbox", with Retry. A failed operations read is shown on that page too. A 404 while the sandbox's create operation is still queued or running says the sandbox is being created instead; the page keeps reading the sandbox and shows it, with its debug and operations cards, as soon as it appears.
  • A sandbox deleted while its page is open shows the same "Sandbox not found" view as a page opened after the delete, with its operations, including the delete, listed at once; the debug reads stop.
  • The debug and operations reads run on load, on Refresh, after an action, every fifth tick of the page's existing sandbox poll while the tab is visible, and as soon as a sandbox that was missing appears. For a sandbox that is not found, only the operations are read. The sandbox row's existing poll is unchanged. Only the newest of these requests may write its answer, so a slow older one cannot overwrite a newer one; the public snapshot listing has its own request counter for the same reason.
  • A failed debug read, or a failed section of it, never renders as empty data: the affected card says "Could not load …" with the error instead of "No pods recorded", "No events recorded", "none" or "template default", and shows loading until the first answer.
  • The public snapshot listing is read again on Refresh, after each action and with the 20 s reads, so a card whose first listing failed recovers instead of staying read-only for the rest of the page's life.
  • Timeline summaries use the detail keys the debug route's timeline source builds for each event (crates/libs/api/src/routes/debug/sources/timeline.rs), such as last_error and apply_state for port exposures.
  • formatBytes has one NaN-safe copy, in ui/src/lib/sandbox-debug.ts, which the sandbox list and detail pages import. The templates page keeps its own copy.

The "Recorded for" column is labelled that way on purpose. A create through the admin API records the sandbox's owner as the operation's actor, while the admin audit log records the operator. That is a backend issue, not changed here.

Stack

Stacked on #1242, which adds parseSandboxRef; this page uses it to turn its ID into the bare UUID the debug route takes. Merge #1242 first. It is independent of #1244.

Validation

  • npm ci, npm test (77 pass, including src/lib/sandbox-debug.test.mjs for section errors, timeline ordering and summaries for each event kind, merging operation reads, ages, and sizes including formatBytes(NaN)), npx tsc --noEmit -p . (no errors) and npm run build in ui/.
  • A Playwright loop against a local Kind stack with synthetic data, run on the parent branch (production build) and on this branch (the Kind space-ui image):
    • every sandbox state's detail page;
    • a suspended sandbox's snapshot is listed with its size;
    • another org's sandbox shows its placement, pods, timeline and operations;
    • a deleted sandbox shows its recorded operations.
  • The loop found no new console errors, page errors or failed requests, compared with main.
  • A second Playwright run on the same Kind stack compared production builds of an earlier head of this branch and of this head. With the public snapshot listing answered synthetically (two snapshots, then a 500 on the 20 s re-list), the earlier head switched the card to the read-only view and dropped Rollback; this head kept both snapshots and their Rollback buttons under a warning. A sandbox force-deleted through the admin API while its page was open showed the "Sandbox not found" view with its create and delete operations, where the earlier head kept showing it as started with "Could not load placement: sandbox_not_found".
  • In the same run, opening a suspended sandbox with two operations by one user made 2 user lookups instead of 3: one for the Operations card and one for the owner link.
  • Not covered: snapshot inventory locations. The Kind stack's inventory was empty, so the card does not render locations yet. Also not captured: a first snapshot listing that fails, followed by one that succeeds.

Visual confirmation

A suspended sandbox, before and after:

Before After
before after

Scrolling the new page for a suspended sandbox:

after: detail page

Videos: before, after.

A deleted sandbox, before and after:

Before After
before after

A snapshot listing that fails after a good one, on an earlier head of this branch and on this head: the listing and its Rollback column now stay, under a warning:

Before After
before after

A sandbox deleted while its page is open, on an earlier head of this branch and on this head: the page now shows the "Sandbox not found" view with the create and delete operations:

Before After
before after

Severity and risk

Severity: high. The detail page is where a support case starts, and it hid the timeline, pods and snapshots that the API already serves.

Risk: low to medium.

  • The change is additive: four read-only cards, a snapshot fallback, and links.
  • Each open detail page adds these reads: the debug route and the operations list under the sbx_ ID and under the bare UUID, at the times above; for a sandbox in the signed-in organization, the public snapshot listing at the same times; and the organization and owner once. The Operations card looks up each distinct "Recorded for" user once, however many rows name that user.
  • A section the debug route fails to load shows a warning in its card instead of failing the page.
  • Roll back by reverting the commit.

AI assistance: Claude Code wrote the code, the tests and this description, and ran the validation; a maintainer reviews it.

@github-actions github-actions Bot added the triage:pending Waiting for a maintainer to triage label Oct 31, 2026
@github-actions

Copy link
Copy Markdown

Thanks for opening this pull request! A SPACE maintainer will take a look.

CONTRIBUTING.md explains how we review outside contributions.

@ac-pplx
ac-pplx force-pushed the ac/admin-ui-sandbox-detail branch from 1f50456 to 9338329 Compare October 31, 2026 23:58
@ac-pplx
ac-pplx marked this pull request as ready for review November 1, 2026 00:04
@the-gigi-pplx

Copy link
Copy Markdown
Collaborator

Review — 4 findings

Medium

  1. ui/src/app/dashboard/sandboxes/[id]/sandbox-detail.tsx:494: one failed background re-list takes rollback away mid-session.
    reloadKey bumps on every insight load (every 20 s and after each action), so a healthy Snapshots card re-lists /sandboxes/{id}/snapshots for as long as the page is open, not only after a failure as the prop comment says. If one of those background reads fails transiently, the catch sets snapshotsOff and listingError, and the rollback table is replaced by the read-only control-plane view until a later tick succeeds.
    Either retry from reloadKey only while listingError is set, or keep the last good listing (and the Rollback column) when a background refresh fails and show the error as a warning above it. Don't stop retrying entirely: the retry is what recovers a card whose first listing failed.

Low

  1. ui/src/app/dashboard/sandboxes/[id]/sandbox-detail.tsx:1175: a sandbox deleted while its page is open keeps getting debug reads.
    refresh() doesn't clear sandbox/sandboxRef on a 404, so loadInsight(!!sandboxRef.current) keeps passing true. The debug route answers 404 sandbox_not_found for a missing row (crates/libs/api/src/routes/debug/sandbox.rs), and every insight card switches to "Could not load …: sandbox_not_found" over a page that still shows the sandbox as live. The "a deleted sandbox has no debug record" path only works when the page was opened after the delete. Clearing the row (or tracking "last read was 404") on a 404 would route both cases through the existing not-found view.
  2. ui/src/app/dashboard/sandboxes/[id]/sandbox-insight.tsx:324: OperationsCard mounts one UserLink per row, and each one fetches GET /v1/users/{id} on its own.
    Operations on a sandbox are almost all recorded for the same owner, so a page of 10 rows sends 10 identical requests, and every pagination change sends another 10. Resolving the distinct actor_user_ids once in the card and passing the emails down fixes it, like the queue in the Operations page PR.
  3. ui/src/lib/sandbox-debug.ts:177: this adds a third formatBytes. Copies already exist at sandbox-detail.tsx:193 and sandboxes/page.tsx:104, and only the new one guards NaN. sandbox-detail.tsx now imports formatAge from this module, so it could import formatBytes too and drop its local copy.

Verified against 93383290ec5759625a8c589834a40cc08bb84864. I checked the client types and claims against the server: the debug response's { error, data: null } section shape (routes/debug/response.rs), the admin operations query fields under deny_unknown_fields including target_type, the timeline's LIMIT 200 and detail keys, and the 7-day gc_after retention of finished operations. I ran npm test in ui/: the new sandbox-debug.test.mjs cases pass. The one failure is curiosity-theme.test.mjs, which can't resolve antd without installed dependencies and isn't touched here. I didn't run next build or a typecheck (I couldn't install dependencies), and I didn't exercise the page in a browser.

@the-gigi-pplx the-gigi-pplx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. 4 non-blocking findings in the review comment above.

zibi-pplx
zibi-pplx previously approved these changes Nov 1, 2026

@zibi-pplx zibi-pplx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One addition to the existing review, about tracking the backend gaps this page works around.

  • [low] Link tracking issues for the three backend gaps the page compensates for.

    • The debug route returns environment variable values, and the page now fetches it about every 20 s while a detail page is open. The page shows names only, but the values reach the browser.
    • Fork operations record their source under the bare UUID (crates/libs/api/src/fork_operations.rs:986, target_id: Some(&prepared.source_id)), while other operations use sbx_, so the page reads operations twice and merges them.
    • An admin create records the sandbox owner as the operation's actor, hence the "Recorded for" label (sandbox-insight.tsx:321).

    The description names all three as separate backend changes. Linking an issue for each, and referencing it in the code comments, would make the double read and the label easy to remove once they are fixed.

@zibi-pplx
zibi-pplx dismissed their stale review November 1, 2026 22:29

Withdrawing this approval for now. Any feedback above still stands.

Base automatically changed from ac/admin-ui-sandbox-id-forms to main November 2, 2026 06:47
The sandbox detail page showed only the sandbox row's current fields.
An operator working a support case had no pod name, no history and no
operation log, and the Snapshots card read the public snapshot route,
which is an org-scoped hidden operation: for any other org's sandbox,
or wherever snapshot operations are not enabled, it said "Snapshots are
not enabled" even when a suspended sandbox's only copy of its state was
a ready snapshot.

The page now reads the admin debug route and the operations log, which
already return all of this under the operator gate:

- Placement and pods: active and target node, and every pod with its
  name, namespace, Kubernetes node, generation, state and times.
- Timeline: the debug route's merged sandbox, pod, snapshot and port
  events, newest first.
- Operations: each lifecycle operation with its actor, attempts and
  result or terminal error.
- Resources and runtime: shape, network counters, environment variable
  names (never values), metadata and port exposures.
- Snapshots: when the public listing is unavailable, the snapshots the
  debug route reports (state, codec, age, size, failure), read-only.

The header shows the sandbox name and full ID, Org and Owner link to
their pages, and a deleted sandbox's not-found page lists the operations
recorded against its ID, which outlive the row.
…bled

The detail page's Snapshots card fell back to the admin view for every
error from the public snapshot listing, so an outage of that route
looked the same as snapshot operations being off. It now falls back
silently only for the hidden-operation answer, and does not call the
org-scoped public route at all for another org's sandbox. Any other
failure still shows the snapshots the control plane records, with a
warning naming the error.

Metadata values that are not strings render as compact JSON instead of
"[object Object]".
When the debug read failed, or one of its sections did, the placement,
pods, timeline, ports, resources and snapshot views said "none", "No pods
recorded" or "template default". Each now says it could not load, with
the error, and shows loading until the first answer. Only the newest
debug and operations request may write its answer.

A failed snapshot listing is retried on Refresh and with the page's
other reads, instead of switching the card to the read-only view for
the rest of the page's life.

The not-found view says the sandbox row is gone only for a 404; other
failures read "Could not load the sandbox" with Retry. It shows a failed
operations read, and a deleted sandbox's operations keep refreshing.

The operations card also lists forks, which record their source by its
bare UUID, says when more than the newest 50 exist and that operations
are removed 7 days after they were requested, and scopes its reads to
the sandbox's org. Timeline summaries use the keys the debug route's
timeline source builds.
- Snapshot listings carry a request counter, like the insight reads,
  so a slow failed retry cannot replace a newer successful listing.
- A 404 while the sandbox's create operation is still queued or
  running says the sandbox is being created instead of "the sandbox
  row is gone". The page keeps polling and loads the sandbox, with its
  debug and operations cards, as soon as it appears.
- The operations card says finished operations are removed 7 days
  after they were requested and unfinished ones are kept.
- The timeline says when it reaches the debug route's 200-event cap.
The Snapshots card lists its snapshots again on every insight load,
every 20 s and after each action, so one failed background listing
switched a healthy card to the read-only admin view and removed its
Rollback column. A failed listing after a good one now keeps that
listing and its rollback, with a warning above it saying when it
loaded. A card whose first listing failed still shows the admin view
and recovers at the next listing that succeeds. Only the hidden-
operation answer switches the card to the admin view.
A 404 from the sandbox read kept the last loaded sandbox on screen, so
the debug reads went on and every insight card said "Could not load …:
sandbox_not_found" over a page that still showed the sandbox as live.
A 404 now clears the loaded sandbox: the page shows the not-found view
a page opened after the delete shows, stops the debug reads, and loads
the operations at once so the delete is listed.
Every operations row mounted its own user link, and each fetched
GET /v1/users/{id}, so a sandbox with 50 operations by one user made
50 identical requests. The card now looks up each distinct
actor_user_id once and passes the emails to its rows; a failed lookup
is tried again when the set of actors changes.
The sandbox list, the sandbox detail page and the debug helpers each
had a copy. The list and detail pages now import the helpers' copy,
which also renders NaN as a dash.
@ac-pplx
ac-pplx force-pushed the ac/admin-ui-sandbox-detail branch from db385f9 to 09797bf Compare November 2, 2026 06:54
@ac-pplx
ac-pplx added this pull request to the merge queue Nov 2, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 2, 2026
@ac-pplx
ac-pplx added this pull request to the merge queue Nov 2, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 2, 2026
@ac-pplx
ac-pplx added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit f5970df Nov 2, 2026
23 checks passed
@ac-pplx
ac-pplx deleted the ac/admin-ui-sandbox-detail branch November 2, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage:pending Waiting for a maintainer to triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants