Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
587444a
feat: add entity lifecycle status control for apps and components
bburda Jun 25, 2026
1439545
feat: add entity lifecycle status store slice
bburda Jun 25, 2026
0bf092c
feat: gate lifecycle actions by status with tooltips
bburda Jun 25, 2026
3c8e43d
feat: confirm destructive lifecycle transitions before dispatch
bburda Jun 25, 2026
01c842f
feat: show lifecycle readiness lamp on app/component tree nodes
bburda Jun 25, 2026
4698342
docs: note lifecycle gating, confirmation, and tree readiness lamp
bburda Jun 25, 2026
b123b6d
feat: track gateway lifecycle actuation support in the store
bburda Jun 25, 2026
f6f2fc1
fix: give response-driven feedback for lifecycle transitions
bburda Jun 25, 2026
3b1b1e7
feat: surface 'transitions not implemented' state on the control
bburda Jun 25, 2026
35c2b5d
feat: show entity description as the label when available
bburda Jun 25, 2026
0c81157
fix: reset stale lifecycle error on entity switch and drop unused sta…
bburda Jun 25, 2026
571c768
fix: scope lifecycle readiness to the gateway session that read it
bburda Aug 21, 2026
eef32bf
fix: decide a lifecycle transition by its HTTP status, not by the err…
bburda Aug 21, 2026
7437242
fix: disable Force restart on an entity that is not running
bburda Aug 21, 2026
49fc037
fix: keep an in-flight transition off the next selected entity
bburda Aug 21, 2026
0514ecf
fix: expose the tree readiness lamp to assistive technology
bburda Aug 21, 2026
5650908
feat: keep lifecycle readiness current while an entity is on screen
bburda Aug 21, 2026
b7b7401
fix: offer no transition on a readiness the UI does not have
bburda Aug 21, 2026
155435f
fix: record missing lifecycle actuation per entity, not gateway-wide
bburda Aug 21, 2026
a079e95
fix: keep an unavailable transition reachable by keyboard
bburda Aug 22, 2026
707ff6f
fix: keep the entity name as its label and put the description beside it
bburda Aug 22, 2026
d5df0d6
fix: release the readiness request slot by generation, not by identity
bburda Aug 22, 2026
aa87ce2
fix: make the typecheck script check something
bburda Aug 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ Simple, open-source web UI for browsing SOVD (Service-Oriented Vehicle Diagnosti
ros2_medkit_web_ui is a lightweight single-page application that connects to a SOVD server and visualizes the entity hierarchy. It provides:

- **Server Connection Dialog** - Enter the URL of your SOVD server (supports both `http://ip:port` and `ip:port` formats)
- **Entity Tree Sidebar** - Browse the hierarchical structure of SOVD entities with lazy-loading
- **Entity Tree Sidebar** - Browse the hierarchical structure of SOVD entities with lazy-loading, with a readiness lamp on app and component nodes (a green disc for ready, an amber ring for not ready, a grey square for a readiness the UI has not established). The lamp is re-read while the branch is open, so it tracks an entity that stops or comes back
- **Entity Detail Panel** - View raw JSON details of any selected entity
- **Entity Lifecycle Status Control** - View readiness and request lifecycle transitions (start, restart, force-restart, shutdown, force-shutdown) for apps and components, degrading gracefully on an entity with no lifecycle provider, without taking the entities that have one with it. Actions are gated by the current status (a transition the current status does not allow is marked unavailable and rejected, and stays focusable so the tooltip explaining why reaches a screen reader), and every destructive transition (all but Start) asks for confirmation before dispatch. A transition is only reported as requested when the gateway accepts it; because acceptance is not completion, the readiness is dropped and re-established by the refresh rather than read back straight away

This tool is designed for developers and integrators working with SOVD-compatible systems who need a quick way to explore and debug the entity structure.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"test:coverage": "vitest --coverage",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
"typecheck": "tsc -b --emitDeclarationOnly false --noEmit",
"prepare": "husky"
},
"lint-staged": {
Expand Down
6 changes: 6 additions & 0 deletions src/components/AppsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isResourceTabId,
type ResourceTabId,
} from '@/components/ResourceTabs';
import { EntityStatusControl } from '@/components/EntityStatusControl';
import type { ComponentTopic, Operation, Fault } from '@/lib/types';

type AppTab = 'overview' | ResourceTabId;
Expand Down Expand Up @@ -136,6 +137,11 @@ export function AppsPanel({ appId, appName, fqn, nodeName, namespace, componentI
</CardDescription>
</div>
</div>

{/* Lifecycle status control (gateway 0.6.0 lifecycle API) */}
<div className="mt-4">
<EntityStatusControl entityType="apps" entityId={appId} />
</div>
Comment thread
bburda marked this conversation as resolved.
</CardHeader>

{/* Tab Navigation */}
Expand Down
26 changes: 24 additions & 2 deletions src/components/EntityDetailPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { TooltipProvider } from '@/components/ui/tooltip';
import { EntityDetailPanel } from './EntityDetailPanel';

// Mock heavy child components - we only care about the top-level routing
Expand Down Expand Up @@ -49,6 +50,9 @@ vi.mock('@/lib/store', () => ({
// The breadcrumb builder resolves segment types via findNode; these tests
// don't load a tree, so it always falls back to position-based inference.
findNode: () => null,
// EntityStatusControl (rendered for component/subcomponent entities) reads
// the status cache keyed by entityStatusKey.
entityStatusKey: (entityType: string, entityId: string) => `${entityType}:${entityId}`,
}));

function setStore(overrides: Record<string, unknown>) {
Expand All @@ -63,6 +67,12 @@ function setStore(overrides: Record<string, unknown>) {
refreshSelectedEntity: mockRefreshSelectedEntity,
prefetchResourceCounts: mockPrefetchResourceCounts,
fetchEntityData: mockFetchEntityData,
// EntityStatusControl reads these from the store; provide inert values
// so the rendered control mounts without touching the network.
client: null,
statusByEntity: {},
actuationByEntity: {},
watchEntityStatus: vi.fn(() => () => {}),
...overrides,
};
}
Expand All @@ -84,7 +94,13 @@ describe('EntityDetailPanel - nested entity types', () => {
},
});

render(<EntityDetailPanel onConnectClick={() => {}} />);
// App.tsx wraps the whole tree in a TooltipProvider, and the panel
// renders tooltips for any disabled lifecycle action.
render(
<TooltipProvider>
<EntityDetailPanel onConnectClick={() => {}} />
</TooltipProvider>
);

// Bug repro: subcomponent should fetch resource counts using the
// 'components' entity type (gateway routes subcomponents through
Expand Down Expand Up @@ -115,7 +131,13 @@ describe('EntityDetailPanel - nested entity types', () => {
},
});

render(<EntityDetailPanel onConnectClick={() => {}} />);
// App.tsx wraps the whole tree in a TooltipProvider, and the panel
// renders tooltips for any disabled lifecycle action.
render(
<TooltipProvider>
<EntityDetailPanel onConnectClick={() => {}} />
</TooltipProvider>
);

// Bug repro: subarea should fetch resource counts using the 'areas'
// entity type (gateway routes subareas through /api/v1/areas/{id}/...).
Expand Down
12 changes: 12 additions & 0 deletions src/components/EntityDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { FunctionsPanel } from '@/components/FunctionsPanel';
import { ServerInfoPanel } from '@/components/ServerInfoPanel';
import { FaultsDashboard } from '@/components/FaultsDashboard';
import { UpdatesDashboard } from '@/components/UpdatesDashboard';
import { EntityStatusControl } from '@/components/EntityStatusControl';
import { useAppStore, findNode, type AppState } from '@/lib/store';
import type { ComponentTopic, Parameter, SovdResourceEntityType } from '@/lib/types';

Expand Down Expand Up @@ -778,6 +779,11 @@ export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntit
</div>
<div>
<CardTitle className="text-xl">{selectedEntity.name}</CardTitle>
{selectedEntity.description && (
<p className="text-sm text-muted-foreground">
{selectedEntity.description}
</p>
)}
<CardDescription className="flex items-center gap-2">
<Badge variant="outline">{selectedEntity.type}</Badge>
<span className="text-muted-foreground">•</span>
Expand All @@ -803,6 +809,12 @@ export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntit
</Button>
</div>
</div>
{/* Lifecycle status control (gateway 0.6.0 lifecycle API) */}
{isComponent && (
<div className="mt-4">
<EntityStatusControl entityType="components" entityId={entityId} />
Comment thread
bburda marked this conversation as resolved.
</div>
)}
</CardHeader>

{/* Tab Navigation for Components */}
Expand Down
Loading
Loading