Repository navigation
ui: add a node page with health, occupancy, sandboxes and node actions - #1256
Conversation
|
Thanks for opening this pull request! A SPACE maintainer will take a look. CONTRIBUTING.md explains how we review outside contributions. |
Nodes had no page of their own: their sandboxes were a drawer of bare
UUIDs, health was a status tag, and the console offered only gentle
drain, undrain, reconcile and delete, while the admin API also supports
cordon, uncordon, forced drain, uploading node-local snapshots and
terminate.
/dashboard/nodes/{id} shows the node's identity and scheduling state,
its occupancy (active sandboxes, node-local snapshots, network slots),
its health (reachability, services and each node-ctl reconciler's last
success and error) and its sandboxes, paged as the API pages them and
linking to each. It offers the node actions that apply to its drain
phase, each behind a confirmation that says what happens to resident
sandboxes. Node names on the nodes page link to it; the active-sandbox
count opens the existing drawer.
Cordon and uncordon are both offered outside a drain because the admin
node response does not report whether the substrate node is cordoned.
The node page now follows the nodes list's rules: actions only for worker and builder nodes, drain-family actions only on substrates that support drain, and upload only while the drain gate blocks wakes. Forced drain and terminate are left to space-admin-cli. Both drain confirmations say that a drained node is terminated automatically unless it carries the drain-keep annotation, and dangerous confirmations focus Cancel. The upload result lists failed and deferred sandboxes and the per-call cap. The node reloads after every action, failed or not. A failed sandbox list shows an error with Retry instead of an empty node, older answers are dropped, and Refresh reloads the list. The health card shows the status snapshot's time and node-ctl's own error.
1e3ca1d to
1ef7ae9
Compare
Node reads replaced the node and its actions with no ordering: a Refresh read that started before Drain could land after the action's own reload and restore the undrained view, hiding Undrain while the node was draining. Node reads now carry a request generation, an action invalidates earlier reads as it starts, and the actions stay disabled until the post-action read settles. The upload result counted queued uploads as uploaded snapshots. It now says upload requests were accepted, that a snapshot is durable only once its upload completes and registers it, and keeps failed requests separate. The confirmation and the result explain the two per-call limits apart: at most 32 uploads attempted, at most 256 sandboxes listed, with the rest "not attempted in this call", and that a retry starts with the same sandboxes because the list is in ID order. Health is now read from the node response's own status snapshot, with that snapshot's time, instead of a separate read that could pair new metadata with older health.
zibi-pplx
left a comment
There was a problem hiding this comment.
The confirmations, and keeping forced drain and terminate off the dashboard, look right. One maintainability note.
- [low] The confirmation text mirrors server constants with nothing linking the two.
ui/src/lib/node-actions.ts:18-19copiesFORCE_UPLOAD_MAX_PER_INVOCATION = 32andFORCE_UPLOAD_ENUMERATION_CAP = 256(crates/libs/api/src/routes/admin/nodes.rs:1805,1818), andnode-actions.test.mjsasserts on the literal numbers. If either constant changes, the dialog describes the old limits and the test still passes. Suggestion: add a comment next to the Rust constants pointing atnode-actions.ts. Alternatively, word the confirmation without the numbers and rely on the response'sdeferredandtruncatedfields, which the result view already reads.
Withdrawing this approval for now. Any feedback above still stands.
The upload confirmation and the result view repeated the server's per-call limits (32 attempted, 256 listed), and the test asserted those numbers, so a change to either constant in routes/admin/nodes.rs would have left the console wrong. The confirmation now says one call lists a limited number of pinned sandboxes and attempts only the first of them. The result view says what the call left out from the answer's deferred and truncated fields alone, through a helper with a test.
johnlouie-pplx
left a comment
There was a problem hiding this comment.
Approving with one nonblocking follow-up. I reviewed f8169b3; the UI build and all 56 tests passed in my earlier validation of this same head. I also checked the page against a mocked admin API, not a live cluster. The upload dialog now reports what the server returned instead of repeating its limits.
In ui/src/app/dashboard/nodes/[id]/page.tsx, loadNode still requests include_occupancy=true. If the count query fails, the entire page becomes "Could not load the node", hiding health, slots and the recovery buttons. A transient failure on Refresh does the same even after a successful load.
Could we fetch counts separately, as #1252 does, so a count failure doesn't hide the rest of the page? For transient refresh failures, keep the last loaded node with an explicit stale/error notice. Permission failures and not-found responses should still replace the page.
Summary
Each node now has a page of its own, with its health, occupancy and sandboxes, and the node actions the nodes list allows, plus cordon, uncordon and uploading node-local snapshots.
Before this change:
/dashboard/nodes/{id}shows:status.health, withstatus.observed_at), so health is never paired with metadata from a different read. Only worker and builder nodes have health; other roles show no Health card.The page offers actions by the nodes list's rules: only on worker and builder nodes, and drain, cordon and uncordon only on substrates that support node drain (the Direct substrate answers 501). Undrain clears an earlier drain on any substrate. Without a drain: Cordon, Uncordon and a gentle Drain. While draining or drained: Undrain. Upload local snapshots only where the API accepts it: drained, terminating, or a forced drain (it refuses during a gentle drain). Each action sits behind a confirmation that says what happens to resident sandboxes; dangerous ones focus Cancel. Both drain confirmations, here and on the nodes list, say that once the node is drained it is terminated automatically wherever the drained-node terminator is enabled, unless the node carries the
space.pplx.net/drain-keepannotation.Forced drain and terminate stay CLI-only (
space-admin-cli drain-node --forced,space-admin-cli terminate-node), as the nodes list already decided: they are not dashboard buttons one misclick away from evacuating or ending a node.Uploading node-local snapshots asks the node to queue each upload; an accepted request is not yet a durable snapshot, which it becomes once the upload completes and registers it in the object store. One call lists a limited number of pinned sandboxes, in sandbox ID order, and attempts uploads for only the first of them. The confirmation says so, and that the result says what was left out, without naming numbers: the limits are the server's (
FORCE_UPLOAD_MAX_PER_INVOCATIONandFORCE_UPLOAD_ENUMERATION_CAPincrates/libs/api/src/routes/admin/nodes.rs) and are not copied into the console. The result is titled "Upload requests accepted for N sandboxes" and lists the requests that failed with their errors. What the call left out comes only from the answer:deferredgives "N listed sandboxes not attempted in this call", andtruncatedgives "More sandboxes are pinned to this node than this call listed". The result also notes that a retry starts with the same sandboxes, failed ones included, because the order is stable.After every action, successful or not, the page reloads the node, because some actions commit and then report an error. Node reads carry a request generation and an action invalidates earlier reads as it starts, so a Refresh that began before an action cannot land after it and restore the previous state; the actions stay disabled until the post-action read settles.
Node names on the nodes page link here. The active-sandbox count opens the existing drawer.
The admin node response does not report whether the substrate node is cordoned, so Cordon and Uncordon are both offered outside a drain.
Stack
This is stacked on "ui: show each node's sandbox and slot occupancy" (#1252); both change the nodes page, and #1252 reads occupancy as a separate best-effort read. Merge #1252 first. The node page itself has no polling interval: it reads on open, on Refresh and after an action.
Validation
npm ci,npm test(56 pass, includingsrc/lib/node-actions.test.mjsfor the action rules across roles, substrates and drain phases, a check that the upload confirmation names no numbers, and the result notes built fromdeferredandtruncated),npx tsc --noEmit -p .(no errors) andnpm run buildinui/. Noui/source mentions the server's upload limits.space-uiimage):cluster-role offers none;main.truncated), so nothing on the node changed. The earlier head's confirmation and result named 32 and 256. This head's confirmation names no numbers, and its result says "3 listed sandboxes not attempted in this call." and "More sandboxes are pinned to this node than this call listed."Visual confirmation
Drain on the node page, with the termination warning; Cancel has focus:
Video: after.
The upload confirmation, on an earlier head of this branch and on this head: it no longer names the server's limits:
The upload result for a synthetic answer with deferred sandboxes and
truncated, before and after: what was left out now comes from the answer alone, without 32 or 256:Severity and risk
Severity: medium. Node health and drain state are what an operator checks first in a capacity incident.
Risk: medium. The page exposes cordon, uncordon, gentle drain and snapshot upload, which change where sandboxes run. A gentle drain can end with the node terminated where the drained-node terminator is enabled; the confirmation says so. Forced drain and terminate are not offered. Each action needs a confirmation that describes the effect, and the API's own guards still apply. Roll back by reverting the commits.
AI assistance: Claude Code wrote the code and this description, and ran the validation; a maintainer reviews it.