Files
fusion/packages/dashboard/app/versionCheck.ts
Fusion Agent 2cb071fff5 FN-171: remove dashboard auto-reload setting
Retire the configurable version-change auto-reload option and make dashboard reload behavior mandatory.\n\n- Remove the auto-reload setting from schemas, UI, translations, persistence, and documentation.\n- Simplify version checking and update affected tests and settings guards.\n- Add a breaking-release changeset documenting the migration.\n\nFiles changed:\n .changeset/fn-171-removal.md                       |   7 +\n docs/dashboard-guide.md                            |   2 +\n docs/settings-reference.md                         |   3 +\n .../core/src/__tests__/settings-defaults.test.ts   |   9 +\n packages/core/src/config/settings-schema.ts        |   5 +\n packages/core/src/types/settings/settings-scope.ts |   5 -\n .../dashboard/app/__tests__/versionCheck.test.ts   | 141 ++++++++-------------\n packages/dashboard/app/components/SettingsModal.css     |   1 -\n .../__tests__/EngineControlMenu.test.tsx           |   4 -\n .../app/components/settings/save-split.ts          |   1 -\n .../sections/GlobalGeneralSection.search.ts        |  10 --\n .../settings/sections/GlobalGeneralSection.tsx     |  14 +-\n ...lobalGeneralSection.auto-reload-removed.test.tsx |  48 +++++++\n .../settings-default-descriptions.test.tsx         |   1 -\n packages/dashboard/app/hooks/useAppSettings.ts     |  25 +---\n packages/dashboard/app/versionCheck.ts             |  58 +--------\n packages/i18n/locales/en/app.json                  |   2 -\n packages/i18n/locales/es/app.json                  |   2 -\n packages/i18n/locales/fr/app.json                  |   2 -\n packages/i18n/locales/ko/app.json                  |   2 -\n packages/i18n/locales/pt-BR/app.json               |   2 -\n packages/i18n/locales/zh-CN/app.json               |   2 -\n packages/i18n/locales/zh-TW/app.json               |   2 -\n packages/i18n/src/resources.d.ts                   |   2 -\n 24 files changed, 129 insertions(+), 221 deletions(-)

Fusion-Task-Id: FN-171

Fusion-Task-Lineage: b76ed733-0036-419d-ae5a-e0563433e957

Co-authored-by: Fusion <noreply@runfusion.ai>
2026-08-23 04:21:28 +00:00

234 lines
6.9 KiB
TypeScript

import { pushTrace } from "./utils/dashboardTraceBuffer";
declare const __BUILD_VERSION__: string;
const RELOAD_FLAG = "fusion:version-reload";
const VERSION_UPDATE_FLAG = "fusion:version-update";
const RELOADED_REMOTE_VERSION_FLAG = "fusion:version-reloaded-remote";
/** Exported for testing — resets internal state and clears installed polling. */
export function _resetState(): void {
lastCheckTime = 0;
checkInFlight = false;
try {
sessionStorage.removeItem(RELOADED_REMOTE_VERSION_FLAG);
} catch {
// ignore
}
if (pollIntervalId !== null) {
window.clearInterval(pollIntervalId);
pollIntervalId = null;
}
_resetMismatchState();
}
export function consumeVersionUpdateFlag(): boolean {
try {
if (sessionStorage.getItem(VERSION_UPDATE_FLAG)) {
sessionStorage.removeItem(VERSION_UPDATE_FLAG);
return true;
}
} catch {
// ignore (e.g. storage disabled)
}
return false;
}
export function reloadOnce(reason: string): void {
const alreadySet = Boolean(sessionStorage.getItem(RELOAD_FLAG));
pushTrace("versionCheck", "reload-attempt", { reason, alreadySet });
if (alreadySet) {
console.warn("[versionCheck] reload already attempted, suppressing", reason);
return;
}
sessionStorage.setItem(RELOAD_FLAG, "1");
/*
FNXC:VersionAutoReload 2026-08-23-04:03:
Reloading after a version change is mandatory with no operator opt-out. The session and per-remote-version flags prevent loops; they do not disable the behavior.
*/
console.info("[versionCheck] reloading:", reason);
window.location.reload();
}
function getReloadedRemoteVersion(): string | null {
try {
return sessionStorage.getItem(RELOADED_REMOTE_VERSION_FLAG);
} catch {
return null;
}
}
function setReloadedRemoteVersion(version: string): void {
try {
sessionStorage.setItem(RELOADED_REMOTE_VERSION_FLAG, version);
} catch {
// ignore
}
}
function clearReloadedRemoteVersion(): void {
try {
sessionStorage.removeItem(RELOADED_REMOTE_VERSION_FLAG);
} catch {
// ignore
}
}
export function isStaleChunkError(error: unknown): boolean {
const message =
error instanceof Error
? error.message
: typeof error === "string"
? error
: "";
return /Failed to fetch dynamically imported module|error loading dynamically imported module|Importing a module script failed|is not a valid JavaScript MIME type|ChunkLoadError|Unable to preload (?:CSS|module) for/i.test(
message,
);
}
export function handleChunkLoadError(error: unknown): boolean {
if (!isStaleChunkError(error)) return false;
reloadOnce(`chunk load error: ${(error as Error)?.message ?? error}`);
return true;
}
async function fetchRemoteVersion(): Promise<string | null> {
try {
const res = await fetch("/version.json", {
cache: "no-store",
headers: { Accept: "application/json" },
});
if (!res.ok) return null;
const ct = res.headers.get("content-type") ?? "";
if (!ct.includes("application/json")) return null;
const data = (await res.json()) as { version?: unknown };
return typeof data.version === "string" ? data.version : null;
} catch {
return null;
}
}
export const MIN_CHECK_INTERVAL_MS = 60_000; // 1 minute
export const POLL_INTERVAL_MS = 5 * 60_000; // 5 minutes
type VersionCheckTrigger = "visibilitychange" | "focus" | "initial" | "poll";
let lastCheckTime = 0;
let checkInFlight = false;
let lastMismatchedRemote: string | null = null;
let lastMismatchAt = 0;
let pollIntervalId: number | null = null;
/** Exported for testing — resets internal cooldown state */
export function _resetCheckState(): void {
lastCheckTime = 0;
checkInFlight = false;
_resetMismatchState();
}
export function _resetMismatchState(): void {
lastMismatchedRemote = null;
lastMismatchAt = 0;
}
export async function checkVersion(trigger: VersionCheckTrigger = "initial"): Promise<void> {
if (checkInFlight || document.visibilityState !== "visible") return;
if (Date.now() - lastCheckTime < MIN_CHECK_INTERVAL_MS) return;
lastCheckTime = Date.now();
checkInFlight = true;
try {
const remote = await fetchRemoteVersion();
if (remote === null) {
pushTrace("versionCheck", "remote-unavailable", {
trigger,
visibilityState: document.visibilityState,
});
lastMismatchedRemote = null;
return;
}
if (remote === __BUILD_VERSION__) {
lastMismatchedRemote = null;
clearReloadedRemoteVersion();
return;
}
pushTrace("versionCheck", "mismatch", {
local: __BUILD_VERSION__,
remote,
trigger,
visibilityState: document.visibilityState,
});
console.info("[versionCheck] mismatch", { local: __BUILD_VERSION__, remote, trigger });
if (lastMismatchedRemote !== remote) {
lastMismatchedRemote = remote;
lastMismatchAt = Date.now();
pushTrace("versionCheck", "mismatch-pending", {
local: __BUILD_VERSION__,
remote,
trigger,
visibilityState: document.visibilityState,
});
return;
}
pushTrace("versionCheck", "mismatch-confirmed", {
remote,
trigger,
elapsedMs: Date.now() - lastMismatchAt,
});
if (getReloadedRemoteVersion() === remote) {
pushTrace("versionCheck", "reload-suppressed", {
remote,
trigger,
reason: "already-reloaded-remote-version",
});
console.info("[versionCheck] reload already attempted for remote version", remote);
return;
}
try {
sessionStorage.setItem(VERSION_UPDATE_FLAG, "1");
} catch {
// ignore
}
const alreadyAttempted = Boolean((() => { try { return sessionStorage.getItem(RELOAD_FLAG); } catch { return null; } })());
if (!alreadyAttempted) {
setReloadedRemoteVersion(remote);
}
reloadOnce(`build version changed: ${__BUILD_VERSION__} -> ${remote}`);
} finally {
checkInFlight = false;
}
}
/**
* Install production version-change detection.
*
* The checker runs on initial load, tab visibility/focus events, and a
* lightweight periodic poll so foreground tabs detect newly deployed bundles
* even when the user never switches away. `checkVersion()` applies visibility
* and cooldown guards, so the interval does not fetch for hidden tabs or more
* frequently than the minimum check interval.
*/
export function installVersionCheck(): void {
if (!import.meta.env.PROD) return;
// Clear stale flag once a fresh page has rendered successfully.
window.setTimeout(() => sessionStorage.removeItem(RELOAD_FLAG), 5_000);
document.addEventListener("visibilitychange", () => {
void checkVersion("visibilitychange");
});
window.addEventListener("focus", () => {
void checkVersion("focus");
});
// Initial check after load to catch tabs restored from bfcache.
window.setTimeout(() => void checkVersion("initial"), 2_000);
if (pollIntervalId === null) {
pollIntervalId = window.setInterval(() => {
void checkVersion("poll");
}, POLL_INTERVAL_MS);
}
}