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>
234 lines
6.9 KiB
TypeScript
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);
|
|
}
|
|
}
|