Files
fusion/packages/dashboard/app/hooks/useUpdateCheck.ts
gsxdsm 74dacd0758 FN-9188: respect externally managed updates
Managed deployments can now suppress self-update offers while unsupported installs receive actionable guidance.

- Add an install-level environment policy for externally managed updates across CLI, server, and dashboard surfaces.
- Detect missing npm shell failures and replace raw stderr with deployment-aware remediation.
- Document managed-update behavior and add regression coverage for checks, installs, automation, and UI actions.

Files changed:
 .changeset/fn-9188-managed-updates.md              |  7 +++
 docs/architecture.md                               |  6 +--
 docs/cli-reference.md                              |  2 +-
 docs/dashboard-guide.md                            |  2 +-
 docs/settings-reference.md                         |  4 +-
 packages/cli/src/__tests__/update-cache.test.ts    | 11 ++++-
 .../update-command-externally-managed.test.ts      | 39 +++++++++++++++
 packages/cli/src/commands/update.ts                | 42 +++++++++++-----
 packages/cli/src/update-cache.ts                   |  3 +-
 .../core/src/__tests__/update-management.test.ts   | 15 ++++++
 packages/core/src/config/update-management.ts      | 21 ++++++++
 packages/core/src/index.gate.ts                    |  5 ++
 packages/core/src/index.ts                         |  5 ++
 packages/dashboard/app/api/client/health.ts        |  3 ++
 .../dashboard/app/components/SettingsModal.tsx     |  4 ++
 .../__tests__/SettingsModal.general.test.tsx       | 20 ++++++++
 .../__tests__/UpdateAvailableBanner.test.tsx       | 26 ++++++++++
 .../components/__tests__/settings-mobile.test.tsx  | 24 +++++++++-
 .../__tests__/SystemControlsArea.test.tsx          | 20 ++++++++
 .../command-center/areas/SystemControlsArea.tsx    |  6 +++
 .../app/hooks/__tests__/useUpdateCheck.test.ts     |  1 +
 packages/dashboard/app/hooks/useUpdateCheck.ts     |  3 +-
 .../dashboard/src/__tests__/auto-update.test.ts    | 10 ++++
 .../dashboard/src/__tests__/update-check.test.ts   | 56 ++++++++++++++++++++++
 packages/dashboard/src/auto-update.ts              |  9 +++-
 .../__tests__/register-update-check-routes.test.ts | 24 +++++++++-
 .../src/routes/register-update-check-routes.ts     | 47 +++++++++++++++++-
 packages/dashboard/src/update-check.ts             | 50 ++++++++++++++++---
 28 files changed, 433 insertions(+), 32 deletions(-)

Fusion-Task-Id: FN-9188

Fusion-Task-Lineage: 60b114b9-8bf7-434f-9d26-0c954aea45ba

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-21 10:18:45 -07:00

73 lines
2.4 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
import { checkForUpdate } from "../api";
import type { UpdateInstallResponse } from "../api";
import { pendingUpdateInstallState, usePendingUpdateInstall } from "./usePendingUpdateInstall";
const UPDATE_BANNER_DISMISSED_KEY = "kb-update-banner-dismissed";
export interface UseUpdateCheckResult {
updateAvailable: boolean;
latestVersion: string | null;
currentVersion: string | null;
loading: boolean;
dismissed: boolean;
pendingInstall?: UpdateInstallResponse;
dismiss: () => void;
}
export function useUpdateCheck(): UseUpdateCheckResult {
const pendingInstall = usePendingUpdateInstall({ hydrate: false });
const [loading, setLoading] = useState(true);
const [updateAvailable, setUpdateAvailable] = useState(false);
const [latestVersion, setLatestVersion] = useState<string | null>(null);
const [currentVersion, setCurrentVersion] = useState<string | null>(null);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
const isDismissed = sessionStorage.getItem(UPDATE_BANNER_DISMISSED_KEY) === "true";
setDismissed(isDismissed);
let cancelled = false;
void checkForUpdate()
.then((result) => {
// Record before ordinary update state so a hydrated pending restart never
// flashes a second Update now action through a competing stale response.
pendingUpdateInstallState.record(result.pendingInstall);
// Externally managed deployments are disabled by the server and must never mount an update offer.
if (cancelled || result.disabled || result.externallyManaged) return;
setUpdateAvailable(result.updateAvailable === true);
setLatestVersion(typeof result.latestVersion === "string" ? result.latestVersion : null);
setCurrentVersion(typeof result.currentVersion === "string" ? result.currentVersion : null);
})
.catch(() => {
// Fail silently. Update checks are best-effort.
})
.finally(() => {
if (!cancelled) {
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, []);
const dismiss = useCallback(() => {
setDismissed(true);
sessionStorage.setItem(UPDATE_BANNER_DISMISSED_KEY, "true");
}, []);
return {
updateAvailable,
latestVersion,
currentVersion,
loading,
dismissed,
pendingInstall,
dismiss,
};
}