Add the ability to check for outdated nodes and trigger over-the-air
updates from Fleet View. Nodes self-update by pulling the latest Docker
image and recreating their container via the "last breath" pattern.
Backend:
- SelfUpdateService: self-container identification via HOSTNAME + Docker
Compose labels, triggers pull + force-recreate
- CapabilityRegistry: runtime capability disabling via disableCapability()
- POST /api/system/update (202 + deferred self-update)
- GET /api/fleet/update-status (version comparison across fleet)
- POST /api/fleet/nodes/:nodeId/update (single node)
- POST /api/fleet/update-all (bulk remote update)
- In-memory update tracker with 5-min timeout
Frontend:
- Node Updates modal with summary stats, search filter, table layout,
per-node Update buttons, and bulk Update All
- Version badges and update-available indicators on node cards
- ReconnectingOverlay for local node updates (polls /api/health)
- 5s fast-poll when any node is actively updating
- UpdateStatusBadge shared component for consistent badge rendering
Requires Skipper (Pro) tier. Nodes must be deployed via Docker Compose
with Docker socket access.
* feat(nodes): add capability-based node compatibility negotiation
Each Sencho instance now exposes /api/meta with its version and supported
capabilities. When the user switches nodes, the frontend fetches this
metadata and disables features the remote node doesn't support via a
CapabilityGate overlay. Version is shown in the node switcher dropdown
and connection test results.
- Backend: CapabilityRegistry with static capability list and fetchRemoteMeta helper
- Backend: /api/meta (public) and /api/nodes/:id/meta (auth) endpoints
- Frontend: NodeContext enhanced with per-node meta caching (5min TTL)
- Frontend: CapabilityGate component with typed Capability union
- Frontend: 13 features wrapped with capability gates
- Docs: node-compatibility.mdx + OpenAPI spec updates
* fix(nodes): revert to require() for package.json version reading
The static import fails in the Docker multi-stage build because the
root package.json is not copied into the backend-builder stage. The
require() call resolves at runtime when the file is available.