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
@@ -22,6 +22,7 @@ import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import { apiFetch } from '@/lib/api';
import { toast } from '@/components/ui/toast-store';
import { ProGate } from '../ProGate';
import { CapabilityGate } from '../CapabilityGate';
import { LabelDot, type Label, type LabelColor } from '../LabelPill';
const LABEL_COLORS: LabelColor[] = ['teal', 'blue', 'purple', 'rose', 'amber', 'green', 'orange', 'pink', 'cyan', 'slate'];
@@ -123,6 +124,7 @@ export function LabelsSection() {
return (
<ProGate featureName="Stack Labels">
<CapabilityGate capability="labels" featureName="Stack Labels">
<div className="space-y-4">
<div className="flex items-center justify-between pr-8">
<div>
@@ -233,6 +235,7 @@ export function LabelsSection() {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CapabilityGate>
</ProGate>
);
}