Files
fusion/packages/dashboard/app/api-node.ts
Fusion 6f3173abe7 feat(FN-3506): add discovery contract and integrate remote node discovery i
Merges node discovery onboarding (FN-3506) with two steps: a pre-registration discovery contract wired into the AddNodeModal UI, and full remote discovery integration with documentation. Also adds a local embedded runtime manager to the desktop app (FN-3404) and wires permanent-agent approval contex

Fusion-Task-Id: FN-3506
2026-05-07 23:21:05 -07:00

139 lines
4.9 KiB
TypeScript

/**
* Remote Node API functions for fetching data from remote nodes via the proxy.
* All functions route through /api/proxy/:nodeId/... when a remote node is targeted.
*/
import type { NodeProjectMappingInput, ProjectInfo } from "./api";
import type { ProjectHealth, ProjectNodePathMapping, Task } from "@fusion/core";
import { api, proxyApi, upsertProjectPathMapping } from "./api";
/** Health information for a remote node */
export interface RemoteNodeHealth {
status: string;
version: string;
nodeId: string;
}
/** Fetch health information from a remote node */
export async function fetchRemoteNodeHealth(nodeId: string): Promise<RemoteNodeHealth> {
return proxyApi<RemoteNodeHealth>("/health", { nodeId });
}
/** Fetch all projects from a remote node */
export async function fetchRemoteNodeProjects(nodeId: string): Promise<ProjectInfo[]> {
return proxyApi<ProjectInfo[]>("/projects", { nodeId });
}
/** Fetch tasks from a specific project on a remote node */
export async function fetchRemoteNodeTasks(
nodeId: string,
projectId: string,
searchQuery?: string,
): Promise<Task[]> {
const params = new URLSearchParams({ projectId });
if (searchQuery && searchQuery.trim()) {
params.set("q", searchQuery.trim());
}
return proxyApi<Task[]>(`/tasks?${params.toString()}`, { nodeId });
}
/** Fetch project health from a remote node */
export async function fetchRemoteNodeProjectHealth(
nodeId: string,
projectId: string,
): Promise<ProjectHealth> {
return proxyApi<ProjectHealth>(`/project-health?projectId=${encodeURIComponent(projectId)}`, {
nodeId,
});
}
// ── Node Settings Sync API ──────────────────────────────────────────────────────
/** Settings scopes returned by GET /api/nodes/:id/settings */
export interface NodeSettingsScopes {
global: Record<string, unknown>;
project: Record<string, unknown>;
}
/** Result from settings push/pull operations */
export interface NodeSettingsSyncResult {
success: boolean;
syncedFields?: string[];
appliedFields?: string[];
skippedFields?: string[];
error?: string;
}
/** Sync status returned by GET /api/nodes/:id/settings/sync-status */
export interface NodeSettingsSyncStatus {
lastSyncAt: string | null;
lastSyncDirection: string | null;
localUpdatedAt: string;
remoteReachable: boolean;
diff: {
global: string[];
project: string[];
};
/** Overall auth credential sync state: "match" if credentials match between local and remote,
* "differs" if they differ, "not-synced" if auth sync has never been performed. */
authMatch?: "match" | "differs" | "not-synced";
/** Per-provider auth match details. Map of provider name to match status. */
authDiff?: Record<string, "match" | "differs">;
}
/** Result from auth sync operation */
export interface NodeAuthSyncResult {
success: boolean;
syncedProviders: string[];
}
/** Fetch settings from a remote node */
export async function fetchNodeSettings(nodeId: string): Promise<NodeSettingsScopes> {
return api<NodeSettingsScopes>(`/nodes/${encodeURIComponent(nodeId)}/settings`);
}
/** Push local settings to a remote node */
export async function pushNodeSettings(nodeId: string): Promise<NodeSettingsSyncResult> {
return api<NodeSettingsSyncResult>(`/nodes/${encodeURIComponent(nodeId)}/settings/push`, {
method: "POST",
body: JSON.stringify({}),
});
}
/** Pull settings from a remote node and apply locally */
export async function pullNodeSettings(nodeId: string): Promise<NodeSettingsSyncResult> {
return api<NodeSettingsSyncResult>(`/nodes/${encodeURIComponent(nodeId)}/settings/pull`, {
method: "POST",
body: JSON.stringify({ conflictResolution: "last-write-wins" }),
});
}
/** Get the sync status for a node (last sync time, diff summary, etc.) */
export async function fetchNodeSettingsSyncStatus(nodeId: string): Promise<NodeSettingsSyncStatus> {
return api<NodeSettingsSyncStatus>(`/nodes/${encodeURIComponent(nodeId)}/settings/sync-status`);
}
/** Synchronize model auth credentials with a remote node */
export async function syncNodeAuth(nodeId: string): Promise<NodeAuthSyncResult> {
return api<NodeAuthSyncResult>(`/nodes/${encodeURIComponent(nodeId)}/auth/sync`, {
method: "POST",
body: JSON.stringify({}),
});
}
/** Fetch all project path mappings for a node */
export async function fetchNodeProjectPathMappings(nodeId: string): Promise<ProjectNodePathMapping[]> {
return api<ProjectNodePathMapping[]>(`/nodes/${encodeURIComponent(nodeId)}/path-mappings`);
}
/** Persist one mapping per selected project for a newly-created node. */
export async function persistNodeProjectPathMappings(
nodeId: string,
projectMappings: NodeProjectMappingInput[],
): Promise<ProjectNodePathMapping[]> {
return Promise.all(
projectMappings.map(({ projectId, path }) => upsertProjectPathMapping(projectId, nodeId, path)),
);
}