feat(FN-1232): add remote node mesh dashboard integration

- Add core proxy API infrastructure (api-node.ts) with remote node communication
- Create NodeContext for centralized node state management
- Implement useNodeProxy hook for node operations (metrics, events, health)
- Implement useRemoteNodeData hook with 10-second polling for live metrics
- Implement useRemoteNodeEvents hook for real-time SSE event subscription
- Add NodeStatusIndicator component with animated status indicators
- Add comprehensive tests for all new hooks and components
- Update dashboard styles for node status visualization
This commit is contained in:
gsxdsm
2026-04-09 13:01:19 -07:00
parent 7ab96cc876
commit fc23e522a7
13 changed files with 1771 additions and 0 deletions

View File

@@ -1981,6 +1981,33 @@ function withProjectId(path: string, projectId?: string): string {
return `${path}${separator}projectId=${encodeURIComponent(projectId)}`;
}
/**
* Rewrite a path to route through the node proxy when viewing a remote node.
* When nodeId is provided and differs from localNodeId (i.e., it's a remote node),
* rewrites the path from `/tasks` to `/proxy/${encodeURIComponent(nodeId)}/tasks`.
* When nodeId is undefined or matches localNodeId, returns the path unchanged.
*/
export function withNodeId(path: string, nodeId?: string, localNodeId?: string): string {
if (!nodeId || nodeId === localNodeId) return path;
// Rewrite path to proxy endpoint: /tasks -> /proxy/:nodeId/tasks
// Strip leading /api prefix if present since proxyApi adds it
const apiPrefix = "/api";
const pathWithoutPrefix = path.startsWith(apiPrefix) ? path.slice(apiPrefix.length) : path;
return `/proxy/${encodeURIComponent(nodeId)}${pathWithoutPrefix}`;
}
/**
* Make an API request, optionally routing through the node proxy for remote nodes.
* When nodeId is provided and differs from localNodeId, the request is routed
* through /api/proxy/:nodeId/... instead of directly.
*/
export function proxyApi<T>(path: string, opts?: RequestInit & { nodeId?: string; localNodeId?: string }): Promise<T> {
// Extract nodeId/localNodeId from opts before passing to api()
const { nodeId, localNodeId, ...fetchOpts } = opts ?? {};
const resolvedPath = withNodeId(path, nodeId, localNodeId);
return api<T>(resolvedPath, fetchOpts);
}
/** Fetch all agents, optionally filtered by state or role */
export function fetchAgents(
filter?: { state?: AgentState; role?: AgentCapability },