feat(FN-2663): add cached update-check setting, APIs, and banner

- Add global updateCheckEnabled setting to core schema/types and wire dashboard command to cache update checks in the CLI
- Implement dashboard server update-check cache module plus REST routes for status and refresh behavior
- Add dashboard client hook, legacy API helpers, and UpdateAvailableBanner UI to show cached CLI update notices
- Cover update-check server routes, hook behavior, banner rendering, and route registration with focused tests
- Document update-check configuration and API behavior in architecture and settings reference docs
This commit is contained in:
Fusion
2026-04-27 01:40:07 -07:00
committed by gsxdsm
parent 1cd40e3592
commit acbd6580b3
18 changed files with 827 additions and 0 deletions

View File

@@ -46,6 +46,7 @@ import {
resolveClaudeCliExtensionPaths,
setCachedClaudeCliResolution,
} from "./claude-cli-extension.js";
import { getCachedUpdateStatus, isUpdateCheckEnabled } from "../update-cache.js";
import { resolveSelfExtension } from "./self-extension.js";
import { DashboardTUI, DashboardLogSink, isTTYAvailable, type SystemInfo, type GitStatus, type GitCommit, type GitCommitDetail, type GitBranch, type GitWorktree, type FileEntry, type FileReadResult, type TaskStep as TUITaskStep, type TaskLogEntry as TUITaskLogEntry, type TaskDetailData, type TaskEvent } from "./dashboard-tui/index.js";
@@ -1812,6 +1813,30 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
? `${baseUrl}/?token=${encodeURIComponent(dashboardAuthToken)}`
: baseUrl;
const updateMessage = await (async (): Promise<string | null> => {
try {
const updateCheckEnabled = await Promise.race<boolean>([
isUpdateCheckEnabled(),
new Promise<boolean>((resolve) => {
setTimeout(() => resolve(false), 3_000);
}),
]);
if (!updateCheckEnabled) {
return null;
}
const cachedUpdate = getCachedUpdateStatus();
if (!cachedUpdate?.updateAvailable) {
return null;
}
return `⬆ Update available: v${cachedUpdate.latestVersion} (current: v${cachedUpdate.currentVersion})`;
} catch {
return null;
}
})();
// ── TTY Mode: Set system info on TUI ───────────────────────────────
//
// In TTY mode, we populate the TUI System panel instead of printing
@@ -2290,6 +2315,9 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
tui.log("AI engine paused");
}
tui.log("File watcher active");
if (updateMessage) {
tui.log(updateMessage);
}
} else {
// ── Non-TTY Mode: Print plain-text banner ───────────────────────────
//
@@ -2320,6 +2348,9 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
console.log(` • cron: scheduled task execution`);
}
console.log(` File watcher: ✓ active`);
if (updateMessage) {
console.log(` ${updateMessage}`);
}
console.log(` Press Ctrl+C to stop`);
console.log();
}

View File

@@ -0,0 +1,48 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { GlobalSettingsStore, resolveGlobalDir } from "@fusion/core";
type CachedUpdateStatus = {
updateAvailable: boolean;
latestVersion: string;
currentVersion: string;
};
type UpdateCachePayload = {
updateAvailable?: unknown;
latestVersion?: unknown;
currentVersion?: unknown;
};
export function getCachedUpdateStatus(): CachedUpdateStatus | null {
try {
const cachePath = join(resolveGlobalDir(), "update-check.json");
const raw = readFileSync(cachePath, "utf-8");
const parsed = JSON.parse(raw) as UpdateCachePayload;
if (
parsed.updateAvailable === true &&
typeof parsed.latestVersion === "string" &&
parsed.latestVersion.length > 0 &&
typeof parsed.currentVersion === "string" &&
parsed.currentVersion.length > 0
) {
return {
updateAvailable: true,
latestVersion: parsed.latestVersion,
currentVersion: parsed.currentVersion,
};
}
return null;
} catch {
return null;
}
}
export async function isUpdateCheckEnabled(): Promise<boolean> {
const store = new GlobalSettingsStore();
await store.init();
const settings = await store.getSettings();
return settings.updateCheckEnabled !== false;
}