Surface entity lifecycle status control (apps + components) - #87
Merged
Conversation
Add EntityStatusControl consuming the gateway 0.6.0 lifecycle API
(GET/PUT /{apps,components}/{id}/status). Renders current readiness as
a badge and exposes the five lifecycle transitions (start, restart,
force-restart, shutdown, force-shutdown) as action buttons. A 501 from
the gateway (no lifecycle provider configured) is surfaced as a disabled
"not available" state instead of an error.
Add getStatus/setStatus dispatch helpers in api-dispatch.ts (narrowed to
the apps/components entity types that expose the lifecycle collection)
plus LifecycleAction/LifecycleStatus types. Mount the control on the app
header (AppsPanel) and the component header (EntityDetailPanel).
There was a problem hiding this comment.
Pull request overview
Adds a new UI control to surface gateway 0.6.0 lifecycle status/transition actions for apps and components, wiring it into the existing entity detail views via the typed OpenAPI client dispatch layer.
Changes:
- Added lifecycle status/action type definitions and API dispatch helpers (
getStatus/setStatus) for apps/components. - Introduced
EntityStatusControlUI component + Vitest coverage for status rendering and action handling (incl. 501 “not available”). - Integrated the control into Apps and Component detail UIs and documented the feature in the README.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/lib/types.ts |
Adds lifecycle action/status union types for gateway 0.6.0 lifecycle API. |
src/lib/api-dispatch.ts |
Adds app/component-only lifecycle status GET and transition PUT dispatch helpers. |
src/components/EntityStatusControl.tsx |
New UI control showing readiness badge + lifecycle transition buttons with 501 handling. |
src/components/EntityStatusControl.test.tsx |
New tests validating rendering, action calls, refresh behavior, and 501 handling. |
src/components/EntityDetailPanel.tsx |
Renders lifecycle control for components in the detail header. |
src/components/AppsPanel.tsx |
Renders lifecycle control for apps in the app header area. |
README.md |
Documents the new lifecycle status control feature. |
Prefer an entity's description (e.g. a component's host OS string 'Ubuntu 24.04.4 LTS on x86_64') over the raw name/hostname for the tree node label and the component detail header. The hostname/id stays discoverable via the tree node tooltip and the detail path. Falls back to the name when there is no description.
bburda
force-pushed
the
feat/entity-lifecycle-status
branch
from
June 25, 2026 12:19
2f5f6eb to
35c2b5d
Compare
…tus prop
The EntityStatusControl reads readiness from the shared store keyed by
entity, so the declared status prop was dead. Remove it and clarify the
status doc comment to reference the GET /apps/{id} and GET /components/{id}
responses. Also clear the local error on entity change so a failed
transition on one entity cannot linger after the selection switches.
mfaferek93
reviewed
Jul 28, 2026
Entity ids are not unique across gateways, so a `statusByEntity` entry or an in-flight status request that outlives a session renders one robot's readiness for another. Connect and disconnect now clear both the cache and the dedupe map, and a status response is only written when the client that issued it is still the current one.
…or body openapi-fetch leaves the error value falsy whenever a failed response carries nothing it can parse - undefined for 204, HEAD or `Content-Length: 0`, and the empty string for an empty body with no Content-Length. A 5xx from a proxy or an aborting gateway therefore reached the success path: a "requested" toast, a status refetch, and the gateway recorded as able to actuate. The outcome now comes from `response.ok`, with a status-derived message when the body says nothing.
Restart, Shutdown and Force shutdown were already unavailable on a notReady entity; Force restart sat enabled beside them, opened the confirmation dialog and dispatched a restart against a stopped entity. Start is now the only action offered in that state, which is what the README describes.
The control is rendered at a fixed position in EntityDetailPanel and AppsPanel, so selecting another entity changes entityId without remounting. A restart still in flight left its spinner and its disabled buttons on the new entity, and its failure wrote an inline error under the new entity's badge. The per-entity UI state is cleared on selection change, and a response is only written back when the entity it was dispatched against is still the one shown.
The lamp was an empty span, which maps to the generic role - aria-label is prohibited there and browsers drop it, so the readiness never reached the accessibility tree and colour was the only channel left. It now carries role="img", and shape distinguishes the states as well as colour: a filled disc for ready, a hollow ring for notReady, a square for a state the UI has not established.
Readiness was read once per mount and never again, so a tree lamp stayed at whatever the entity was when the branch was opened and a crashed app kept a green lamp indefinitely. The same gap made the post-transition read wrong: a gateway answers 202 (accepted) long before a node has restarted, so reading immediately returned the readiness from before the transition and the gating acted on it. Entities register interest through watchEntityStatus for as long as they are mounted, and a refresh loop re-reads exactly those. A transition now drops the cached value instead of reading it back, and a read carrying an older generation is discarded so one issued before the change cannot restore it.
Only 501 counted as "no lifecycle here", so the 404 a gateway without the routes returns fell through to unknown - and unknown has no entry in the gating table, which left all five actions live under a grey badge. The same gap covered the window before the first read landed. A 404 now reads as unavailable like a 501, and an unestablished readiness disables the actions with a tooltip that says what is being waited for.
A single 501 set one flag that disabled all five actions on every app and every component, and the only writer able to clear it was a 2xx transition - which no longer had an enabled button to come from, so a reconnect was the only way out. It also disagreed with the read side, which already scopes a 501 to the entity that produced it. A provider is registered per entity, so the transition side now remembers the answer per entity too.
Each disabled button sat inside a focusable span with no role and no name, so a keyboard user tabbed through five stops that announced nothing while the reason stayed on the buttons themselves, which a disabled attribute had already removed from the accessibility tree. The buttons now carry aria-disabled, keep their name and their place in the tab order, and reject the action in the handler; focusing one opens the tooltip that says why it cannot be used. The Radix floating layer constructs a ResizeObserver as soon as it opens, so the test setup provides one - jsdom has none, and without it a tooltip throws on render instead of failing an assertion.
The description replaced the name in the tree row and in the detail card title, leaving the name only in a title attribute that keyboard and touch users never see. A description is entity metadata, not an identifier - a component's is the host's OS, so every component on one host rendered the same truncated string. The name leads again in both places, with the description alongside it.
Comparing the parked promise against the one still being constructed reads a variable before it is assigned, which the project build rejects. The generation already captured for the write guard identifies the request just as precisely: a slot is only cleared while it still belongs to the generation that filled it.
The root tsconfig carries `files: []` and two project references, so `tsc --noEmit` against it resolved zero input files and reported success on any source at all. It now builds the referenced projects, which is what the build step already type-checks.
mfaferek93
approved these changes
Aug 22, 2026
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.
Summary
Add an
EntityStatusControlto the Apps and Components entity detail, consuming the gateway 0.6.0 lifecycle status API. It shows the live readiness status (ready / notReady) and offers the transition actions (start / restart / force-restart / shutdown / force-shutdown) via the typed client. The501"no lifecycle provider configured" case is surfaced as a disabled not-available state. Lifecycle status exists only for apps and components (not areas/functions).Issue
Type
Testing
In a worktree branched from
origin/main(post-0.6.0-migration):npm run lint(eslint) - cleannpm run typecheck(tsc --noEmit) - cleannpm test -- --run(vitest) - 421 passed (18 files, incl. 7 newEntityStatusControltests)npm run build- succeedsChecklist
npm run lint)npm run build)