FN-8138: add restart controls after Settings updates
Expose a supervised restart affordance after an in-app update succeeds. - Detect restart capability while update results are visible and handle scheduled or failed restart requests - Add accessible restart status, capability guidance, responsive styling, and English translations - Cover restart states across desktop and mobile Settings modal tests Files changed: .../fn-8138-settings-restart-after-update.md | 7 ++ docs/dashboard-guide.md | 2 +- .../dashboard/app/components/SettingsModal.css | 21 +++- .../dashboard/app/components/SettingsModal.tsx | 110 ++++++++++++++-- .../__tests__/SettingsModal.general.test.tsx | 139 ++++++++++++++++++++- .../__tests__/SettingsModal.test-harness.tsx | 4 + .../components/__tests__/settings-mobile.test.tsx | 4 +- packages/i18n/locales/en/app.json | 4 + 8 files changed, 274 insertions(+), 17 deletions(-) Fusion-Task-Id: FN-8138 Fusion-Task-Lineage: 24e8cb9a-ad64-4419-b3ce-461850624365 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8138-settings-restart-after-update.md
Normal file
7
.changeset/fn-8138-settings-restart-after-update.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Add a one-click "Restart Fusion" button to the Settings modal after an in-app update.
|
||||
category: feature
|
||||
dev: Adds a capability-aware restart affordance to SettingsModal's footer update-success state; reuses POST /api/system/restart via requestSystemRestart and restartSupported, with a disabled manual-restart fallback when unsupervised.
|
||||
@@ -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, 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.
|
||||
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, both the Settings update-success state and dashboard update banner offer 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`), either action remains disabled and explains that Fusion must be restarted manually.
|
||||
|
||||
## Settings discovery
|
||||
|
||||
|
||||
@@ -375,17 +375,23 @@ Fix the invariant for BOTH presentations (standalone modal + embedded SettingsVi
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:SettingsUpdate 2026-07-16-00:00:
|
||||
FN-8138 adds a restart control and manual-restart explanation after an update.
|
||||
Let this inner result flow wrap on narrow screens while the existing footer rail
|
||||
remains horizontally scrollable, so the new controls never create a clipped shell.
|
||||
*/
|
||||
.settings-modal .settings-update-check {
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
row-gap: 0;
|
||||
flex-wrap: wrap;
|
||||
row-gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.settings-modal .settings-update-result {
|
||||
align-self: center;
|
||||
flex: 0 1 auto;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
line-height: normal;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.settings-modal .settings-footer-help-btn {
|
||||
@@ -517,6 +523,13 @@ Fix the invariant for BOTH presentations (standalone modal + embedded SettingsVi
|
||||
animation: settings-update-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.settings-update-install-succeeded {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.settings-update-install-status {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef, type CSSProperties, type Dispatch, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent, type PointerEvent as ReactPointerEvent, type SetStateAction } from "react";
|
||||
import { Globe, Folder, GitBranch, RefreshCw, Star, Settings as SettingsIcon, Search, X as SearchToggleCloseIcon } from "lucide-react";
|
||||
import { Globe, Folder, GitBranch, Power, RefreshCw, Star, Settings as SettingsIcon, Search, X as SearchToggleCloseIcon } from "lucide-react";
|
||||
import {
|
||||
getErrorMessage,
|
||||
resolveGitlabConfig,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@fusion/core";
|
||||
import type { Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset } from "@fusion/core";
|
||||
import { DEFAULT_GLOBAL_SETTINGS } from "@fusion/core";
|
||||
import { fetchSettings, fetchSettingsByScope, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, cancelProviderLogin, saveApiKey, clearApiKey, fetchModels, testNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemoryFile, fetchMemoryFiles, saveMemoryFile, compactMemory, fetchGlobalConcurrency, updateGlobalConcurrency, installQmd, testMemoryRetrieval, triggerMemoryDreams, fetchGitRemotes, fetchGitRemotesDetailed, fetchGitBranches, fetchProjects, fetchDashboardHealth, checkForUpdates, installUpdate, fetchRemoteSettings, fetchRemoteStatus, installCloudflared, fetchRemoteQr, fetchRemoteUrl, submitProviderManualCode } from "../api";
|
||||
import { fetchSettings, fetchSettingsByScope, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, cancelProviderLogin, saveApiKey, clearApiKey, fetchModels, testNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemoryFile, fetchMemoryFiles, saveMemoryFile, compactMemory, fetchGlobalConcurrency, updateGlobalConcurrency, installQmd, testMemoryRetrieval, triggerMemoryDreams, fetchGitRemotes, fetchGitRemotesDetailed, fetchGitBranches, fetchProjects, fetchDashboardHealth, checkForUpdates, installUpdate, fetchSystemInfo, requestSystemRestart, fetchRemoteSettings, fetchRemoteStatus, installCloudflared, fetchRemoteQr, fetchRemoteUrl, submitProviderManualCode } from "../api";
|
||||
import type { AuthProvider, ManualOAuthCodeInfo, ModelInfo, BackupListResponse, SettingsExportData, MemoryFileInfo, MemoryRetrievalTestResult, GitRemote, GitRemoteDetailed, ProjectInfo, RemoteStatus, UpdateCheckResponse, UpdateInstallResponse, OAuthDeviceCodeInfo } from "../api";
|
||||
import { resolveScopedMcpSettings, splitSettingsSave, type McpSettingsScope } from "./settings/save-split";
|
||||
import {
|
||||
@@ -1307,6 +1307,10 @@ export function SettingsModal({
|
||||
const [updateCheckResult, setUpdateCheckResult] = useState<UpdateCheckResponse | null>(null);
|
||||
const [updateInstallLoading, setUpdateInstallLoading] = useState(false);
|
||||
const [updateInstallResult, setUpdateInstallResult] = useState<UpdateInstallResponse | null>(null);
|
||||
const [restartSupported, setRestartSupported] = useState<boolean | undefined>();
|
||||
const [restartLoading, setRestartLoading] = useState(false);
|
||||
const [restartScheduled, setRestartScheduled] = useState(false);
|
||||
const [restartError, setRestartError] = useState<string | null>(null);
|
||||
const gitHubStarCount = useGitHubStarCount();
|
||||
const [starClicked, markStarClicked] = useStarClickedFlag();
|
||||
const [prefixError, setPrefixError] = useState<string | null>(null);
|
||||
@@ -1830,6 +1834,10 @@ export function SettingsModal({
|
||||
const handleCheckForUpdates = useCallback(async () => {
|
||||
setUpdateCheckLoading(true);
|
||||
setUpdateInstallResult(null);
|
||||
setRestartSupported(undefined);
|
||||
setRestartLoading(false);
|
||||
setRestartScheduled(false);
|
||||
setRestartError(null);
|
||||
|
||||
try {
|
||||
const result = await checkForUpdates();
|
||||
@@ -1855,6 +1863,9 @@ export function SettingsModal({
|
||||
const handleInstallUpdate = useCallback(async () => {
|
||||
setUpdateInstallLoading(true);
|
||||
setUpdateInstallResult(null);
|
||||
setRestartLoading(false);
|
||||
setRestartScheduled(false);
|
||||
setRestartError(null);
|
||||
|
||||
try {
|
||||
const result = await installUpdate(projectId);
|
||||
@@ -1882,6 +1893,53 @@ export function SettingsModal({
|
||||
}
|
||||
}, [addToast, appVersion, projectId, t, updateCheckResult]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!updateCheckResult?.updateAvailable && updateInstallResult?.updated !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setRestartSupported(undefined);
|
||||
|
||||
void fetchSystemInfo()
|
||||
.then((info) => {
|
||||
if (!cancelled) setRestartSupported(info.restartSupported);
|
||||
})
|
||||
.catch(() => {
|
||||
// Fail closed: system capability fetch errors must not expose an unavailable restart action.
|
||||
if (!cancelled) setRestartSupported(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [updateCheckResult?.updateAvailable, updateInstallResult?.updated]);
|
||||
|
||||
/*
|
||||
FNXC:SettingsUpdate 2026-07-16-00:00:
|
||||
After a successful in-app update, the Settings footer must offer the same supervised
|
||||
one-click restart as SystemControlsArea. The FN-8134-deferred Settings surface keeps
|
||||
the control disabled with manual-restart guidance unless restartSupported is true.
|
||||
*/
|
||||
const handleRestart = useCallback(async () => {
|
||||
if (restartLoading || restartSupported !== true) return;
|
||||
|
||||
setRestartLoading(true);
|
||||
setRestartError(null);
|
||||
try {
|
||||
const result = await requestSystemRestart("settings-update");
|
||||
if (result.scheduled) {
|
||||
setRestartScheduled(true);
|
||||
} else {
|
||||
setRestartError(t("settings.general.restartFailed", "Restart could not be scheduled. Try restarting Fusion manually."));
|
||||
}
|
||||
} catch (error) {
|
||||
setRestartError(getErrorMessage(error) || t("settings.general.restartFailed", "Restart could not be scheduled. Try restarting Fusion manually."));
|
||||
} finally {
|
||||
setRestartLoading(false);
|
||||
}
|
||||
}, [restartLoading, restartSupported, t]);
|
||||
|
||||
const renderUpdateCheckResultContent = useCallback(() => {
|
||||
if (!updateCheckResult) {
|
||||
return null;
|
||||
@@ -1909,10 +1967,48 @@ export function SettingsModal({
|
||||
</a>
|
||||
</span>
|
||||
{installSucceeded ? (
|
||||
<span className="settings-update-install-status settings-update-install-status--success" aria-live="polite">
|
||||
{t("settings.general.updateSuccess", "Updated to v{{version}} — restart Fusion to apply", {
|
||||
version: updateInstallResult.latestVersion ?? updateCheckResult.latestVersion,
|
||||
})}
|
||||
<span className="settings-update-install-succeeded">
|
||||
<span className="settings-update-install-status settings-update-install-status--success" aria-live="polite">
|
||||
{t("settings.general.updateSuccess", "Updated to v{{version}} — restart Fusion to apply", {
|
||||
version: updateInstallResult.latestVersion ?? updateCheckResult.latestVersion,
|
||||
})}
|
||||
</span>
|
||||
{restartScheduled ? (
|
||||
<span className="settings-update-install-status" aria-live="polite">
|
||||
{t("settings.general.restarting", "Restarting… Your connection will close shortly.")}
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm settings-update-now-btn"
|
||||
onClick={() => {
|
||||
void handleRestart();
|
||||
}}
|
||||
disabled={restartSupported !== true || restartLoading}
|
||||
>
|
||||
{restartLoading ? (
|
||||
<>
|
||||
<RefreshCw size={12} className="spinning" aria-hidden="true" />
|
||||
{t("settings.general.restarting", "Restarting…")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Power size={12} aria-hidden="true" />
|
||||
{t("settings.general.restartNow", "Restart Fusion")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{restartSupported !== true && (
|
||||
<span className="settings-update-install-status" aria-live="polite">
|
||||
{t("settings.general.restartUnavailable", "Needs a supervising parent — restart Fusion manually without --no-supervise.")}
|
||||
</span>
|
||||
)}
|
||||
{restartError && (
|
||||
<span className="settings-update-install-status settings-update-install-status--error" aria-live="polite">
|
||||
{restartError}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
@@ -1943,7 +2039,7 @@ export function SettingsModal({
|
||||
}
|
||||
|
||||
return t("settings.general.upToDate", "You're up to date ✓");
|
||||
}, [handleInstallUpdate, t, updateCheckResult, updateInstallLoading, updateInstallResult]);
|
||||
}, [handleInstallUpdate, handleRestart, restartError, restartLoading, restartScheduled, restartSupported, t, updateCheckResult, updateInstallLoading, updateInstallResult]);
|
||||
|
||||
// Load auth status when the authentication section is active
|
||||
const loadAuthStatus = useCallback(async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, it, expect, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, within, cleanup } from "@testing-library/react";
|
||||
import path from "path";
|
||||
import { SettingsModal } from "../SettingsModal";
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
mockFetchDashboardHealth,
|
||||
mockCheckForUpdates,
|
||||
mockInstallUpdate,
|
||||
mockFetchSystemInfo,
|
||||
mockRequestSystemRestart,
|
||||
mockFetchRemoteSettings,
|
||||
mockUpdateRemoteSettings,
|
||||
mockFetchRemoteStatus,
|
||||
@@ -117,6 +119,8 @@ vi.mock("../../api", async (importOriginal) => {
|
||||
fetchDashboardHealth: (...args: unknown[]) => mockFetchDashboardHealth(...args),
|
||||
checkForUpdates: (...args: unknown[]) => mockCheckForUpdates(...args),
|
||||
installUpdate: (...args: unknown[]) => mockInstallUpdate(...args),
|
||||
fetchSystemInfo: (...args: unknown[]) => mockFetchSystemInfo(...args),
|
||||
requestSystemRestart: (...args: unknown[]) => mockRequestSystemRestart(...args),
|
||||
fetchRemoteSettings: (...args: unknown[]) => mockFetchRemoteSettings(...args),
|
||||
updateRemoteSettings: (...args: unknown[]) => mockUpdateRemoteSettings(...args),
|
||||
fetchRemoteStatus: (...args: unknown[]) => mockFetchRemoteStatus(...args),
|
||||
@@ -151,11 +155,13 @@ vi.mock("../../hooks/useConfirm", () => ({
|
||||
useConfirm: () => ({ confirm: (...args: unknown[]) => mockConfirm(...args) }),
|
||||
}));
|
||||
|
||||
let viewportMode: "mobile" | "desktop" = "mobile";
|
||||
|
||||
vi.mock("../../hooks/useViewportMode", () => ({
|
||||
MOBILE_MEDIA_QUERY: "(max-width: 768px), (max-height: 480px)",
|
||||
getViewportMode: () => "mobile",
|
||||
isMobileViewport: () => true,
|
||||
useViewportMode: () => "mobile",
|
||||
getViewportMode: () => viewportMode,
|
||||
isMobileViewport: () => viewportMode === "mobile",
|
||||
useViewportMode: () => viewportMode,
|
||||
}));
|
||||
vi.mock("lucide-react", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("lucide-react")>();
|
||||
@@ -198,6 +204,131 @@ vi.mock("../FileBrowser", () => ({
|
||||
describe("SettingsModal", () => {
|
||||
installSettingsModalEnv();
|
||||
|
||||
afterEach(() => {
|
||||
viewportMode = "mobile";
|
||||
});
|
||||
|
||||
const availableUpdate = {
|
||||
currentVersion: "1.2.3",
|
||||
latestVersion: "2.0.0",
|
||||
updateAvailable: true,
|
||||
};
|
||||
|
||||
async function renderUpdatedSettings() {
|
||||
mockCheckForUpdates.mockResolvedValue(availableUpdate);
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
await settingsModalUser.click(screen.getByRole("button", { name: "Check for updates" }));
|
||||
await screen.findByRole("button", { name: "Update now" });
|
||||
await settingsModalUser.click(screen.getByRole("button", { name: "Update now" }));
|
||||
return screen.findByRole("button", { name: "Restart Fusion" });
|
||||
}
|
||||
|
||||
describe("update restart affordance", () => {
|
||||
it("renders an enabled restart button after a successful update on desktop", async () => {
|
||||
viewportMode = "desktop";
|
||||
|
||||
const restartButton = await renderUpdatedSettings();
|
||||
|
||||
expect(restartButton).toBeEnabled();
|
||||
expect(restartButton).toHaveAccessibleName("Restart Fusion");
|
||||
});
|
||||
|
||||
it("renders a wrapping, enabled restart control after a successful update on mobile", async () => {
|
||||
const restartButton = await renderUpdatedSettings();
|
||||
|
||||
expect(restartButton).toBeEnabled();
|
||||
expect(restartButton).toHaveAccessibleName("Restart Fusion");
|
||||
expect(restartButton.closest(".settings-update-install-succeeded")).toBeInTheDocument();
|
||||
expect(settingsModalCss).toMatch(/\.settings-modal \.settings-update-check\s*\{[^}]*flex-wrap: wrap;/s);
|
||||
});
|
||||
|
||||
it("requests the supervised restart with the Settings update reason", async () => {
|
||||
const restartButton = await renderUpdatedSettings();
|
||||
|
||||
await settingsModalUser.click(restartButton);
|
||||
|
||||
expect(mockRequestSystemRestart).toHaveBeenCalledTimes(1);
|
||||
expect(mockRequestSystemRestart).toHaveBeenCalledWith("settings-update");
|
||||
expect(await screen.findByText(/Restarting… Your connection will close shortly/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the restart button disabled with manual guidance when unsupported", async () => {
|
||||
mockFetchSystemInfo.mockResolvedValue({ supervised: false, restartSupported: false });
|
||||
|
||||
const restartButton = await renderUpdatedSettings();
|
||||
|
||||
expect(restartButton).toBeDisabled();
|
||||
expect(screen.getByText(/Needs a supervising parent/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the restart button disabled while system information is loading", async () => {
|
||||
mockFetchSystemInfo.mockReturnValue(new Promise(() => {}));
|
||||
|
||||
const restartButton = await renderUpdatedSettings();
|
||||
|
||||
expect(restartButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("fails closed with manual guidance when system information cannot load", async () => {
|
||||
mockFetchSystemInfo.mockRejectedValue(new Error("unavailable"));
|
||||
|
||||
const restartButton = await renderUpdatedSettings();
|
||||
|
||||
await waitFor(() => expect(restartButton).toBeDisabled());
|
||||
expect(screen.getByText(/Needs a supervising parent/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables the restart button and shows a spinner while scheduling", async () => {
|
||||
mockRequestSystemRestart.mockReturnValue(new Promise(() => {}));
|
||||
const restartButton = await renderUpdatedSettings();
|
||||
|
||||
await settingsModalUser.click(restartButton);
|
||||
|
||||
expect(restartButton).toBeDisabled();
|
||||
expect(within(restartButton).getByTestId("icon-refresh")).toHaveClass("spinning");
|
||||
});
|
||||
|
||||
it("shows an inline error and allows retry when restart scheduling rejects", async () => {
|
||||
mockRequestSystemRestart.mockRejectedValue(new Error("Restart unavailable"));
|
||||
const restartButton = await renderUpdatedSettings();
|
||||
|
||||
await settingsModalUser.click(restartButton);
|
||||
|
||||
expect(await screen.findByText("Restart unavailable")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Restart Fusion" })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("shows an inline error and allows retry when restart scheduling returns false", async () => {
|
||||
mockRequestSystemRestart.mockResolvedValue({ scheduled: false });
|
||||
const restartButton = await renderUpdatedSettings();
|
||||
|
||||
await settingsModalUser.click(restartButton);
|
||||
|
||||
expect(await screen.findByText(/Restart could not be scheduled/)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Restart Fusion" })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("keeps the retry path and hides restart when update installation fails", async () => {
|
||||
mockInstallUpdate.mockResolvedValue({
|
||||
currentVersion: "1.2.3",
|
||||
latestVersion: "2.0.0",
|
||||
updated: false,
|
||||
error: "Install failed",
|
||||
});
|
||||
mockCheckForUpdates.mockResolvedValue(availableUpdate);
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
await settingsModalUser.click(screen.getByRole("button", { name: "Check for updates" }));
|
||||
await screen.findByRole("button", { name: "Update now" });
|
||||
await settingsModalUser.click(screen.getByRole("button", { name: "Update now" }));
|
||||
|
||||
expect(await screen.findByText(/Update failed: Install failed/)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Update now" })).toBeEnabled();
|
||||
expect(screen.queryByRole("button", { name: "Restart Fusion" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const deepwikiServer = {
|
||||
name: "deepwiki",
|
||||
transport: "stdio" as const,
|
||||
|
||||
@@ -63,6 +63,8 @@ export const mockFetchProjects = vi.fn();
|
||||
export const mockFetchDashboardHealth = vi.fn();
|
||||
export const mockCheckForUpdates = vi.fn();
|
||||
export const mockInstallUpdate = vi.fn();
|
||||
export const mockFetchSystemInfo = vi.fn();
|
||||
export const mockRequestSystemRestart = vi.fn();
|
||||
export const mockFetchRemoteSettings = vi.fn();
|
||||
export const mockUpdateRemoteSettings = vi.fn();
|
||||
export const mockFetchRemoteStatus = vi.fn();
|
||||
@@ -368,6 +370,8 @@ export function installSettingsModalEnv() {
|
||||
mockFetchDashboardHealth.mockResolvedValue({ status: "ok", version: "1.2.3", uptime: 123 });
|
||||
mockCheckForUpdates.mockResolvedValue(undefined);
|
||||
mockInstallUpdate.mockResolvedValue({ currentVersion: "1.2.3", latestVersion: "2.0.0", updated: true });
|
||||
mockFetchSystemInfo.mockResolvedValue({ supervised: true, restartSupported: true });
|
||||
mockRequestSystemRestart.mockResolvedValue({ scheduled: true });
|
||||
mockFetchRemoteSettings.mockResolvedValue({
|
||||
settings: {
|
||||
remoteActiveProvider: null,
|
||||
|
||||
@@ -114,6 +114,8 @@ vi.mock("../../api", () => ({
|
||||
fetchDashboardHealth: vi.fn(() => Promise.resolve({ status: "ok", version: "1.2.3", uptime: 120 })),
|
||||
checkForUpdates: vi.fn(() => Promise.resolve({ currentVersion: "1.0.0", latestVersion: "2.0.0", updateAvailable: true })),
|
||||
installUpdate: vi.fn(() => Promise.resolve({ currentVersion: "1.0.0", latestVersion: "2.0.0", updated: true })),
|
||||
fetchSystemInfo: vi.fn(() => Promise.resolve({ supervised: true, restartSupported: true })),
|
||||
requestSystemRestart: vi.fn(() => Promise.resolve({ scheduled: true })),
|
||||
fetchGlobalSettings: vi.fn(() => Promise.resolve({ ...defaultSettings })),
|
||||
// SettingsModal renders ProjectDefaultWorkflowField → WorkflowSelector, which loads these on mount.
|
||||
fetchWorkflows: vi.fn(() => Promise.resolve([])),
|
||||
@@ -788,7 +790,7 @@ describe("SettingsModal mobile adaptations", () => {
|
||||
expectMobileRule(css, ".settings-modal .settings-modal-footer-version", "flex: 0 0 auto;");
|
||||
expectMobileRule(css, ".settings-modal .settings-modal-footer-version", "min-width: max-content;");
|
||||
expectMobileRule(css, ".settings-modal .settings-update-check", "align-items: center;");
|
||||
expectMobileRule(css, ".settings-modal .settings-update-check", "flex-wrap: nowrap;");
|
||||
expectMobileRule(css, ".settings-modal .settings-update-check", "flex-wrap: wrap;");
|
||||
expectMobileRule(css, ".settings-modal .settings-version-check-btn", "line-height: 1;");
|
||||
expectMobileRule(css, ".settings-modal .settings-version-check-btn", "white-space: nowrap;");
|
||||
expectMobileRule(css, ".settings-modal .settings-modal-version", "display: inline-flex;");
|
||||
|
||||
@@ -5909,6 +5909,10 @@
|
||||
"updateNow": "Update now",
|
||||
"updateSuccess": "Updated to v{{version}} — restart Fusion to apply",
|
||||
"updateSuccessToast": "Update installed. Restart Fusion to apply it.",
|
||||
"restartFailed": "Restart could not be scheduled. Try restarting Fusion manually.",
|
||||
"restartNow": "Restart Fusion",
|
||||
"restartUnavailable": "Needs a supervising parent — restart Fusion manually without --no-supervise.",
|
||||
"restarting": "Restarting… Your connection will close shortly.",
|
||||
"updating": "Updating…",
|
||||
"upperBoundOnMessagesFetchedFromTheRoom": "Upper bound on messages fetched from the room store for compaction consideration. Default: 200.",
|
||||
"upToDate": "You're up to date ✓",
|
||||
|
||||
Reference in New Issue
Block a user