feat(FN-2664): add dashboard updates check endpoint and settings flow

- Add GET /api/updates/check endpoint in dashboard server with coverage for success, failure, and disabled update scenarios
- Extend legacy dashboard API client with check-for-updates request support
- Add Settings modal update-check UI and styling for trigger, loading, and result states
- Expand Settings modal tests and document the updates check endpoint in architecture docs
This commit is contained in:
Fusion
2026-04-27 02:19:44 -07:00
committed by gsxdsm
parent 4330eef126
commit 04157d0ad3
7 changed files with 408 additions and 7 deletions

View File

@@ -68,6 +68,52 @@ const PACKAGE_VERSION = (() => {
return process.env.npm_package_version ?? "0.0.0";
})();
const CLI_PACKAGE_VERSION = (() => {
try {
const packageJsonPath = join(__dirname, "..", "..", "cli", "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
version?: unknown;
};
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
return packageJson.version;
}
} catch {
// Fall through to environment fallback.
}
return process.env.npm_package_version ?? "0.0.0";
})();
function parseVersion(version: string): number[] {
return version
.split(".")
.slice(0, 3)
.map((part) => Number.parseInt(part, 10))
.map((value) => (Number.isFinite(value) ? value : 0));
}
function isRemoteVersionNewer(remoteVersion: string, currentVersion: string): boolean {
const remote = parseVersion(remoteVersion);
const current = parseVersion(currentVersion);
const maxLength = Math.max(remote.length, current.length, 3);
for (let i = 0; i < maxLength; i += 1) {
const remotePart = remote[i] ?? 0;
const currentPart = current[i] ?? 0;
if (remotePart > currentPart) {
return true;
}
if (remotePart < currentPart) {
return false;
}
}
return false;
}
const DEFAULT_AI_SESSION_TTL_MS = SESSION_CLEANUP_DEFAULT_MAX_AGE_MS;
const MIN_AI_SESSION_TTL_MS = 10 * 60 * 1000;
const MAX_AI_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
@@ -921,6 +967,45 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
});
});
app.get("/api/updates/check", async (_req, res) => {
const currentVersion = CLI_PACKAGE_VERSION;
res.set("Cache-Control", "no-store");
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
const response = await fetch("https://registry.npmjs.org/@runfusion/fusion/latest", {
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`registry request failed: ${response.status}`);
}
const payload = (await response.json()) as { version?: unknown };
if (typeof payload.version !== "string" || payload.version.trim().length === 0) {
throw new Error("registry response missing version");
}
const latestVersion = payload.version;
res.json({
currentVersion,
latestVersion,
updateAvailable: isRemoteVersionNewer(latestVersion, currentVersion),
});
} catch {
res.status(200).json({
currentVersion,
latestVersion: null,
updateAvailable: false,
error: "Failed to check for updates",
});
} finally {
clearTimeout(timeout);
}
});
app.get("/remote-login", async (req, res) => {
const remoteToken = typeof req.query.rt === "string" ? req.query.rt : undefined;