mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 06:46:23 +00:00
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:
@@ -0,0 +1,58 @@
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* Static registry of capabilities supported by THIS Sencho instance.
|
||||
* Append-only: when a new feature ships, add its capability string here.
|
||||
* The frontend uses these flags (not semver comparisons) to gate features
|
||||
* on nodes that may be running older versions.
|
||||
*/
|
||||
export const CAPABILITIES = [
|
||||
'stacks',
|
||||
'containers',
|
||||
'resources',
|
||||
'templates',
|
||||
'global-logs',
|
||||
'system-stats',
|
||||
'fleet',
|
||||
'auto-updates',
|
||||
'labels',
|
||||
'webhooks',
|
||||
'network-topology',
|
||||
'notifications',
|
||||
'notification-routing',
|
||||
'host-console',
|
||||
'audit-log',
|
||||
'scheduled-ops',
|
||||
'sso',
|
||||
'api-tokens',
|
||||
'users',
|
||||
'registries',
|
||||
] as const;
|
||||
|
||||
export type Capability = (typeof CAPABILITIES)[number];
|
||||
|
||||
export function getSenchoVersion(): string {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
return require('../../../package.json').version;
|
||||
}
|
||||
|
||||
export interface RemoteMeta {
|
||||
version: string | null;
|
||||
capabilities: string[];
|
||||
}
|
||||
|
||||
/** Fetch /api/meta from a remote Sencho instance. Returns empty data on failure. */
|
||||
export async function fetchRemoteMeta(baseUrl: string, apiToken: string): Promise<RemoteMeta> {
|
||||
try {
|
||||
const res = await axios.get(`${baseUrl}/api/meta`, {
|
||||
headers: { Authorization: `Bearer ${apiToken}` },
|
||||
timeout: 5000,
|
||||
});
|
||||
return {
|
||||
version: res.data.version ?? null,
|
||||
capabilities: Array.isArray(res.data.capabilities) ? res.data.capabilities : [],
|
||||
};
|
||||
} catch {
|
||||
return { version: null, capabilities: [] };
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import Docker from 'dockerode';
|
||||
import axios from 'axios';
|
||||
import { DatabaseService, Node } from './DatabaseService';
|
||||
import { fetchRemoteMeta } from './CapabilityRegistry';
|
||||
|
||||
/**
|
||||
* NodeRegistry: Manages connections for multiple nodes.
|
||||
@@ -165,21 +166,25 @@ export class NodeRegistry {
|
||||
|
||||
// Step 2: Fetch Docker stats in parallel. Use allSettled so a slow or missing
|
||||
// endpoint doesn't fail the whole test - each field falls back to '-' gracefully.
|
||||
const [statsResult, sysResult, imagesResult] = await Promise.allSettled([
|
||||
const [statsResult, sysResult, imagesResult, metaResult] = await Promise.allSettled([
|
||||
axios.get(`${baseUrl}/api/stats`, { headers, timeout: 8000 }),
|
||||
axios.get(`${baseUrl}/api/system/stats`, { headers, timeout: 8000 }),
|
||||
axios.get(`${baseUrl}/api/system/images`, { headers, timeout: 8000 }),
|
||||
fetchRemoteMeta(baseUrl, node.api_token!),
|
||||
]);
|
||||
|
||||
const stats = statsResult.status === 'fulfilled' ? statsResult.value.data : null;
|
||||
const sys = sysResult.status === 'fulfilled' ? sysResult.value.data : null;
|
||||
const images = imagesResult.status === 'fulfilled' ? imagesResult.value.data : null;
|
||||
const meta = metaResult.status === 'fulfilled' ? metaResult.value : null;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
info: {
|
||||
name: node.name,
|
||||
serverVersion: 'Remote Sencho',
|
||||
senchoVersion: meta?.version ?? null,
|
||||
capabilities: meta?.capabilities ?? [],
|
||||
os: 'Remote',
|
||||
architecture: 'Remote',
|
||||
containers: stats?.total ?? '-',
|
||||
|
||||
Reference in New Issue
Block a user