From 9a1c21f373db55a7619c3d965c70c91688cde900 Mon Sep 17 00:00:00 2001 From: Fusion Date: Tue, 28 Apr 2026 10:52:14 -0700 Subject: [PATCH] fix(FN-2867): simplify remote access settings activation flow - Remove the separate "Activate Provider" action from SettingsModal and rely on save-only provider configuration - Ensure saving remote settings enables the selected active provider and keeps provider flags consistent - Seed remoteAccess defaults in remote settings routes when project settings are missing instead of returning conflicts - Expand dashboard tests to cover first-use/default remote settings behavior and updated provider lifecycle expectations --- .../app/components/SettingsModal.tsx | 76 ++++++++++++++----- .../__tests__/SettingsModal.test.tsx | 41 ++++++++-- .../__tests__/remote-access-routes.test.ts | 24 +++++- .../__tests__/routes-remote-access.test.ts | 54 ++++++++++++- .../routes/register-settings-memory-routes.ts | 17 +---- 5 files changed, 173 insertions(+), 39 deletions(-) diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index 72317d190..c8d395c5e 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef, lazy, Suspense, type MouseEve import { Globe, Folder, RefreshCw, Star, HelpCircle, Loader2 } from "lucide-react"; import { THINKING_LEVELS, isGlobalSettingsKey, isProjectSettingsKey, getErrorMessage } from "@fusion/core"; import type { Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, AgentPromptsConfig, ThinkingLevel } from "@fusion/core"; -import { fetchSettings, fetchSettingsByScope, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, saveApiKey, clearApiKey, fetchModels, testNtfyNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemoryFile, fetchMemoryFiles, saveMemoryFile, compactMemory, fetchGlobalConcurrency, updateGlobalConcurrency, installQmd, testMemoryRetrieval, triggerMemoryDreams, fetchGitRemotesDetailed, fetchDashboardHealth, checkForUpdates, fetchRemoteSettings, updateRemoteSettings, fetchRemoteStatus, activateRemoteProvider, startRemoteTunnel, stopRemoteTunnel, regenerateRemotePersistentToken, generateShortLivedRemoteToken, fetchRemoteQr, fetchRemoteUrl } from "../api"; +import { fetchSettings, fetchSettingsByScope, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, saveApiKey, clearApiKey, fetchModels, testNtfyNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemoryFile, fetchMemoryFiles, saveMemoryFile, compactMemory, fetchGlobalConcurrency, updateGlobalConcurrency, installQmd, testMemoryRetrieval, triggerMemoryDreams, fetchGitRemotesDetailed, fetchDashboardHealth, checkForUpdates, fetchRemoteSettings, updateRemoteSettings, fetchRemoteStatus, startRemoteTunnel, stopRemoteTunnel, regenerateRemotePersistentToken, generateShortLivedRemoteToken, fetchRemoteQr, fetchRemoteUrl } from "../api"; import type { AuthProvider, ModelInfo, BackupListResponse, SettingsExportData, MemoryFileInfo, MemoryRetrievalTestResult, GitRemoteDetailed, RemoteSettings, RemoteStatus, UpdateCheckResponse } from "../api"; import { useMemoryBackendStatus } from "../hooks/useMemoryBackendStatus"; import { useOverlayDismiss } from "../hooks/useOverlayDismiss"; @@ -1451,13 +1451,18 @@ export function SettingsModal({ }; const handleSaveRemoteSettings = useCallback(async () => { + const activeProvider = ((form as Record).remoteActiveProvider as "tailscale" | "cloudflare" | null) ?? null; const nextSettings: Partial = { - remoteActiveProvider: ((form as Record).remoteActiveProvider as "tailscale" | "cloudflare" | null) ?? null, - remoteTailscaleEnabled: Boolean((form as Record).remoteTailscaleEnabled), + remoteActiveProvider: activeProvider, + remoteTailscaleEnabled: activeProvider === "tailscale" + ? true + : Boolean((form as Record).remoteTailscaleEnabled), remoteTailscaleHostname: String((form as Record).remoteTailscaleHostname ?? ""), remoteTailscaleTargetPort: Number((form as Record).remoteTailscaleTargetPort ?? 4040), remoteTailscaleAcceptRoutes: Boolean((form as Record).remoteTailscaleAcceptRoutes), - remoteCloudflareEnabled: Boolean((form as Record).remoteCloudflareEnabled), + remoteCloudflareEnabled: activeProvider === "cloudflare" + ? true + : Boolean((form as Record).remoteCloudflareEnabled), remoteCloudflareQuickTunnel: Boolean((form as Record).remoteCloudflareQuickTunnel), remoteCloudflareTunnelName: String((form as Record).remoteCloudflareTunnelName ?? ""), remoteCloudflareTunnelToken: (((form as Record).remoteCloudflareTunnelToken as string | null) || null), @@ -2410,6 +2415,55 @@ export function SettingsModal({ /> Maximum concurrent planning agents +
+ + + {(() => { + const selectedNode = nodes.find((node) => node.id === form.defaultNodeId); + if (!selectedNode) return null; + return ( +
+
+ ); + })()} + Used when a task has no node override. Node status is shown for safer routing selection. +
+
+ + +
Cloudflare Tunnel
-