feat(FN-2523): add safe restart restore lifecycle diagnostics

- Add ProjectEngine restore lifecycle core to perform safe restarts and surface detailed restore state transitions
- Expose restore diagnostics through remote-access status types and settings/memory route context, including legacy API mapping updates
- Add comprehensive regression coverage for restore lifecycle behavior in engine and dashboard headless remote-access tests
- Document the restore lifecycle contract in architecture/settings docs and include a patch changeset for @runfusion/fusion
This commit is contained in:
Fusion
2026-04-26 05:18:12 -07:00
committed by gsxdsm
parent a4c219f89b
commit e5ed696922
12 changed files with 956 additions and 19 deletions

View File

@@ -96,6 +96,14 @@ export async function getProjectContext(
}
}
if (!projectId && options?.engine) {
try {
return { store: options.engine.getTaskStore(), engine: options.engine, projectId };
} catch {
// Fall back to scoped store resolution.
}
}
const scopedStore = await getScopedStore(req, store);
return { store: scopedStore, engine: undefined, projectId };
}

View File

@@ -303,13 +303,24 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
router.get("/remote/status", async (req, res) => {
try {
const { store: scopedStore } = await getProjectContext(req);
const { store: scopedStore, engine } = await getProjectContext(req);
const settings = await scopedStore.getSettings();
const manager = engine?.getRemoteTunnelManager();
const tunnelStatus = manager?.getStatus();
const restore = engine?.getRemoteTunnelRestoreDiagnostics();
res.json({
provider: settings.remoteAccess?.activeProvider ?? null,
state: "stopped",
url: null,
lastError: null,
provider: tunnelStatus?.provider ?? settings.remoteAccess?.activeProvider ?? null,
state: tunnelStatus?.state ?? "stopped",
url: tunnelStatus?.url ?? null,
lastError: tunnelStatus?.lastError?.message ?? null,
lastErrorCode: tunnelStatus?.lastError?.code ?? null,
restore: restore ?? {
outcome: "skipped",
reason: "not_attempted",
at: new Date().toISOString(),
provider: null,
},
});
} catch (err: unknown) {
if (err instanceof ApiError) throw err;
@@ -345,25 +356,57 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
router.post("/remote/tunnel/start", async (req, res) => {
try {
const { store: scopedStore } = await getProjectContext(req);
const { store: scopedStore, engine } = await getProjectContext(req);
const settings = await scopedStore.getSettings();
const provider = settings.remoteAccess?.activeProvider ?? null;
if (!provider) {
throw new ApiError(409, "No active provider configured", { code: "NO_ACTIVE_PROVIDER" });
}
res.json({ state: "starting", provider });
if (!engine) {
res.json({ state: "starting", provider });
return;
}
const status = await engine.startRemoteTunnel();
res.json({
state: status.state,
provider: status.provider,
url: status.url,
lastError: status.lastError?.message ?? null,
lastErrorCode: status.lastError?.code ?? null,
});
} catch (err: unknown) {
if (err instanceof ApiError) throw err;
const message = err instanceof Error ? err.message : String(err);
if (message.startsWith("invalid_config:") || message.startsWith("runtime_prerequisite_missing:")) {
throw new ApiError(409, message.split(":").slice(1).join(":") || "Remote tunnel prerequisites are not met", {
code: "REMOTE_TUNNEL_PREREQUISITE_MISSING",
});
}
rethrowAsApiError(err, "Failed to start remote tunnel");
}
});
router.post("/remote/tunnel/stop", async (req, res) => {
try {
const { store: scopedStore } = await getProjectContext(req);
const { store: scopedStore, engine } = await getProjectContext(req);
const settings = await scopedStore.getSettings();
const provider = settings.remoteAccess?.activeProvider ?? null;
res.json({ state: "stopped", provider });
if (!engine) {
res.json({ state: "stopped", provider });
return;
}
const status = await engine.stopRemoteTunnel();
res.json({
state: status.state,
provider: status.provider,
url: status.url,
lastError: status.lastError?.message ?? null,
lastErrorCode: status.lastError?.code ?? null,
});
} catch (err: unknown) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err, "Failed to stop remote tunnel");