diff --git a/.changeset/fn-8134-restart-after-update.md b/.changeset/fn-8134-restart-after-update.md new file mode 100644 index 0000000000..dfa621ad44 --- /dev/null +++ b/.changeset/fn-8134-restart-after-update.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add a one-click "Restart Fusion" button to the update banner after an in-app update. +category: feature +dev: Reuses POST /api/system/restart via requestSystemRestart and the SystemInfoResponse.restartSupported capability flag; button degrades to a disabled state with a manual-restart note when unsupervised. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index b600bf96bc..08d4e52d25 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -6,7 +6,7 @@ The Fusion dashboard is the main control plane for tasks, agents, missions, sett ## Dashboard Updates -When Fusion detects a newer `@runfusion/fusion` release, the Settings modal footer shows the available version with **Learn more** and **Update now** actions. **Update now** installs the latest global package with npm; after it succeeds, restart Fusion to apply the new version because the already-running dashboard server is unchanged until restart. +When Fusion detects a newer `@runfusion/fusion` release, the Settings modal footer shows the available version with **Learn more** and **Update now** actions. **Update now** installs the latest global package with npm; after it succeeds, the dashboard update banner offers a one-click **Restart Fusion** action because the already-running dashboard server is unchanged until restart. When Fusion is unsupervised (for example, started with `--no-supervise`), that banner action is disabled and explains that Fusion must be restarted manually. ## Settings discovery diff --git a/packages/dashboard/app/components/UpdateAvailableBanner.css b/packages/dashboard/app/components/UpdateAvailableBanner.css index 2361d33207..a9ba64ddfe 100644 --- a/packages/dashboard/app/components/UpdateAvailableBanner.css +++ b/packages/dashboard/app/components/UpdateAvailableBanner.css @@ -53,7 +53,8 @@ gap: var(--space-xs); } -.update-available-banner__update-btn { +.update-available-banner__update-btn, +.update-available-banner__restart-btn { display: inline-flex; align-items: center; gap: var(--space-xs); @@ -63,7 +64,8 @@ color: var(--color-warning); } -.update-available-banner__update-btn svg.spinning { +.update-available-banner__update-btn svg.spinning, +.update-available-banner__restart-btn svg.spinning { animation: update-available-banner-spin 1s linear infinite; } @@ -113,6 +115,10 @@ align-items: stretch; } + .update-available-banner__actions { + align-items: flex-start; + } + .update-available-banner__dismiss { align-self: flex-end; } diff --git a/packages/dashboard/app/components/UpdateAvailableBanner.tsx b/packages/dashboard/app/components/UpdateAvailableBanner.tsx index 10d11f76d0..23dfdb0a9a 100644 --- a/packages/dashboard/app/components/UpdateAvailableBanner.tsx +++ b/packages/dashboard/app/components/UpdateAvailableBanner.tsx @@ -1,9 +1,9 @@ import "./UpdateAvailableBanner.css"; -import { useState } from "react"; -import { RefreshCw, X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Power, RefreshCw, X } from "lucide-react"; import { useTranslation, Trans } from "react-i18next"; import { getErrorMessage } from "@fusion/core"; -import { installUpdate } from "../api"; +import { fetchSystemInfo, installUpdate, requestSystemRestart } from "../api"; import type { UpdateInstallResponse } from "../api"; interface UpdateAvailableBannerProps { @@ -16,6 +16,27 @@ export function UpdateAvailableBanner({ latestVersion, currentVersion, onDismiss const { t } = useTranslation("app"); const [installLoading, setInstallLoading] = useState(false); const [installResult, setInstallResult] = useState(null); + const [restartSupported, setRestartSupported] = useState(); + const [restartLoading, setRestartLoading] = useState(false); + const [restartScheduled, setRestartScheduled] = useState(false); + const [restartError, setRestartError] = useState(null); + + useEffect(() => { + let active = true; + + void fetchSystemInfo() + .then((info) => { + if (active) setRestartSupported(info.restartSupported); + }) + .catch(() => { + // Fail closed: an unavailable capability response must not offer a restart that cannot run. + if (active) setRestartSupported(false); + }); + + return () => { + active = false; + }; + }, []); const handleInstallUpdate = async () => { setInstallLoading(true); @@ -35,8 +56,33 @@ export function UpdateAvailableBanner({ latestVersion, currentVersion, onDismiss } }; + /* + FNXC:UpdateBanner 2026-07-16-00:00: + Issue #1799 requires a successful in-app update to offer the supervised restart hook in-place. + Hosts without restart support keep the control visible but disabled with manual-restart guidance. + */ + const handleRestart = async () => { + if (restartLoading || restartSupported !== true) return; + + setRestartLoading(true); + setRestartError(null); + try { + const result = await requestSystemRestart("update-banner"); + if (result.scheduled) { + setRestartScheduled(true); + } else { + setRestartError(t("updateBanner.restartFailed", "Restart could not be scheduled. Try restarting Fusion manually.")); + } + } catch (error) { + setRestartError(getErrorMessage(error) || t("updateBanner.restartFailed", "Restart could not be scheduled. Try restarting Fusion manually.")); + } finally { + setRestartLoading(false); + } + }; + const installSucceeded = installResult?.updated === true; const installError = installResult?.error; + const restartUnavailable = restartSupported !== true; return (
@@ -63,11 +109,49 @@ export function UpdateAvailableBanner({ latestVersion, currentVersion, onDismiss

{installSucceeded ? ( - - {t("updateBanner.updateSuccess", "Updated to v{{version}} — restart Fusion to apply", { - version: installResult.latestVersion ?? latestVersion, - })} - + <> + + {t("updateBanner.updateSuccess", "Updated to v{{version}} — restart Fusion to apply", { + version: installResult.latestVersion ?? latestVersion, + })} + + {restartScheduled ? ( + + {t("updateBanner.restarting", "Restarting… Your connection will close shortly.")} + + ) : ( + + )} + {restartUnavailable && ( + + {t("updateBanner.restartUnavailable", "Needs a supervising parent — restart Fusion manually without --no-supervise.")} + + )} + {restartError && ( + + {restartError} + + )} + ) : (