feat(nodes): add capability-based node compatibility negotiation (#350)

* 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.
This commit is contained in:
Anso
2026-04-03 00:06:34 -04:00
committed by GitHub
parent ec23f8c4c2
commit ee75811e25
20 changed files with 468 additions and 45 deletions
+60
View File
@@ -543,6 +543,33 @@ paths:
type: number
description: Process uptime in seconds.
/api/meta:
get:
operationId: getMeta
tags: [Health]
summary: Instance metadata
description: Returns this Sencho instance's version and list of supported capabilities. No authentication required. Used by nodes to negotiate feature compatibility.
security: []
responses:
"200":
description: Instance metadata.
content:
application/json:
schema:
type: object
required: [version, capabilities]
properties:
version:
type: string
description: Sencho version (semver).
example: "0.32.0"
capabilities:
type: array
description: List of feature capabilities this instance supports.
items:
type: string
example: ["stacks", "containers", "fleet", "auto-updates", "host-console"]
# ── Stacks ──────────────────────────────────────────────
/api/stacks:
get:
@@ -1767,6 +1794,39 @@ paths:
"500":
$ref: "#/components/responses/InternalError"
/api/nodes/{id}/meta:
get:
operationId: getNodeMeta
tags: [Nodes]
summary: Get node capabilities
description: Returns the version and supported capabilities for a specific node. For local nodes, returns this instance's own metadata. For remote nodes, relays the response from the remote instance's `/api/meta` endpoint. Returns `{ version: null, capabilities: [] }` if the remote node is offline or doesn't support the meta endpoint.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Node capability metadata.
content:
application/json:
schema:
type: object
required: [version, capabilities]
properties:
version:
type: string
nullable: true
description: Sencho version of the node, or null if unavailable.
example: "0.31.0"
capabilities:
type: array
description: List of feature capabilities the node supports. Empty array if unavailable.
items:
type: string
example: ["stacks", "containers", "fleet"]
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
# ── Fleet ───────────────────────────────────────────────
/api/fleet/overview:
get: