Skip to content

Surface entity lifecycle status control (apps + components) - #87

Merged
bburda merged 23 commits into
mainfrom
feat/entity-lifecycle-status
Aug 22, 2026
Merged

Surface entity lifecycle status control (apps + components)#87
bburda merged 23 commits into
mainfrom
feat/entity-lifecycle-status

Conversation

@bburda

@bburda bburda commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Add an EntityStatusControl to 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. The 501 "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

  • Bug fix
  • New feature
  • Breaking change
  • Documentation only

Testing

In a worktree branched from origin/main (post-0.6.0-migration):

  • npm run lint (eslint) - clean
  • npm run typecheck (tsc --noEmit) - clean
  • npm test -- --run (vitest) - 421 passed (18 files, incl. 7 new EntityStatusControl tests)
  • npm run build - succeeds

Checklist

  • Breaking changes are clearly described (none - additive feature)
  • Linting passes (npm run lint)
  • Build succeeds (npm run build)
  • Docs were updated if behavior or public API changed (README feature list)

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).
Copilot AI review requested due to automatic review settings June 25, 2026 08:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 EntityStatusControl UI 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.

Comment thread src/components/EntityStatusControl.tsx Outdated
Comment thread src/components/AppsPanel.tsx
Comment thread src/lib/types.ts
Comment thread src/components/EntityStatusControl.tsx Outdated
Comment thread src/components/EntityDetailPanel.tsx
@bburda
bburda force-pushed the feat/entity-lifecycle-status branch from 2f5f6eb to 35c2b5d Compare June 25, 2026 12:19
…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.
@bburda bburda self-assigned this Jul 28, 2026
Comment thread src/components/EntityStatusControl.tsx Outdated
Comment thread src/lib/store.ts Outdated
Comment thread src/components/EntityStatusControl.tsx Outdated
Comment thread src/components/EntityStatusControl.tsx Outdated
Comment thread src/components/EntityTreeNode.tsx Outdated
Comment thread src/components/EntityTreeNode.tsx
Comment thread src/components/EntityStatusControl.tsx Outdated
Comment thread src/lib/store.ts Outdated
Comment thread src/components/EntityStatusControl.tsx Outdated
Comment thread src/components/EntityTreeNode.tsx Outdated
bburda added 12 commits August 21, 2026 20:39
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.
@bburda
bburda merged commit 9118083 into main Aug 22, 2026
5 checks passed
@bburda
bburda deleted the feat/entity-lifecycle-status branch August 22, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Surface entity lifecycle status control (apps + components)

3 participants