diff --git a/client/ui/frontend/CLAUDE.md b/client/ui/frontend/CLAUDE.md index bd2de0a10..19a94d5dc 100644 --- a/client/ui/frontend/CLAUDE.md +++ b/client/ui/frontend/CLAUDE.md @@ -79,7 +79,7 @@ Page-specific chrome and providers live in the page, not the layout: - `typography/` — `Label`, `HelpText`. - `empty-state/` — `EmptyState`, `NoResults`, `NotConnectedState`, `DaemonUnavailableOverlay`. - Flat at root: `Badge`, `CopyToClipboard`, `DropdownMenu`, `SquareIcon`, `Tooltip`, `TruncatedText`, `VerticalTabs`, `LanguagePicker`, `ManagementServerSwitch`. -- `hooks/` — `useAutoSizeWindow.ts` (auto-size + `Window.Show` for auxiliary dialogs), `useKeyboardShortcut.ts`, `useManagementUrl.ts` (management-URL helpers: `CLOUD_MANAGEMENT_URL`, `isValidManagementUrl`, `normalizeManagementUrl`, `isCloudManagementUrl`, `checkManagementUrlReachable`). +- `hooks/` — `useAutoSizeWindow.ts` (auto-size + `Window.Show` for auxiliary dialogs), `useKeyboardShortcut.ts`, `useManagementUrl.ts` (management-URL helpers: `CLOUD_MANAGEMENT_URL`, `isValidManagementUrl`, `normalizeManagementUrl`, `isNetbirdCloud`, `checkManagementUrlReachable`). - `lib/` — pure utilities (no JSX, no React state): `cn.ts`, `errors.ts` (`formatErrorMessage` + the `errorDialog({Title, Message})` window wrapper), `formatters.ts` (byte/latency/relative-time + `shortenDns`), `sorting.ts` (`reconcileOrder` — order-preserving list reconciliation shared by the peers/networks/profiles lists), `i18n.ts`, `logs.ts` (forwards console + uncaught errors to the Go log pipeline), `platform.ts` (`isMacOS`/`isWindows`), `welcome.ts`. - `assets/` — fonts, logos, flags. diff --git a/client/ui/frontend/src/hooks/useManagementUrl.ts b/client/ui/frontend/src/hooks/useManagementUrl.ts index 6981efd7c..6a0ef1b81 100644 --- a/client/ui/frontend/src/hooks/useManagementUrl.ts +++ b/client/ui/frontend/src/hooks/useManagementUrl.ts @@ -4,6 +4,15 @@ import { useSettings } from "@/contexts/SettingsContext.tsx"; import { useConfirm } from "@/contexts/DialogContext.tsx"; export const CLOUD_MANAGEMENT_URL = "https://api.netbird.io:443"; +const CLOUD_MANAGEMENT_URLS = new Set([ + CLOUD_MANAGEMENT_URL, + "https://api.wiretrustee.com:443", // legacy cloud endpoint +]); + +export function isNetbirdCloud(url: string): boolean { + if (!url || url.trim() === "") return true; + return CLOUD_MANAGEMENT_URLS.has(url); +} // Matches http(s)://host[:port][/path][?query][#fragment]; host = domain, localhost, or IPv4. // Syntactic validation only — reachability is checked via checkManagementUrlReachable. @@ -30,11 +39,6 @@ export function isValidManagementUrl(input: string): boolean { return URL_PATTERN.test(trimmed); } -export function isCloudManagementUrl(url: string): boolean { - if (!url || url.trim() === "") return true; - return url === CLOUD_MANAGEMENT_URL; -} - // Can false-negative for self-hosted behind internal DNS / self-signed certs — treat as a soft warning, not a hard block. export async function checkManagementUrlReachable( url: string, @@ -60,7 +64,7 @@ export enum ManagementMode { } function modeFromUrl(url: string): ManagementMode { - return url === CLOUD_MANAGEMENT_URL ? ManagementMode.Cloud : ManagementMode.SelfHosted; + return isNetbirdCloud(url) ? ManagementMode.Cloud : ManagementMode.SelfHosted; } export function useManagementUrl() { @@ -69,14 +73,14 @@ export function useManagementUrl() { const { config, saveField } = useSettings(); const [modeState, setModeState] = useState(modeFromUrl(config.managementUrl)); const [url, setUrl] = useState( - config.managementUrl === CLOUD_MANAGEMENT_URL ? "" : config.managementUrl, + isNetbirdCloud(config.managementUrl) ? "" : config.managementUrl, ); const [checking, setChecking] = useState(false); const [unreachable, setUnreachable] = useState(false); useEffect(() => { setModeState(modeFromUrl(config.managementUrl)); - if (config.managementUrl !== CLOUD_MANAGEMENT_URL) { + if (!isNetbirdCloud(config.managementUrl)) { setUrl(config.managementUrl); } }, [config.managementUrl]); @@ -86,7 +90,7 @@ export function useManagementUrl() { }, [url, modeState]); const setMode = async (next: ManagementMode) => { - if (next === ManagementMode.Cloud && config.managementUrl !== CLOUD_MANAGEMENT_URL) { + if (next === ManagementMode.Cloud && !isNetbirdCloud(config.managementUrl)) { const ok = await confirm({ title: t("settings.general.management.switchCloudTitle"), description: t("settings.general.management.switchCloudMessage"), diff --git a/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx index 47193f0b1..742dd8e3a 100644 --- a/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx +++ b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx @@ -13,7 +13,7 @@ import { useProfile } from "@/contexts/ProfileContext"; import { useConfirm } from "@/contexts/DialogContext"; import { Settings as SettingsSvc } from "@bindings/services"; import { SetConfigParams } from "@bindings/services/models.js"; -import { CLOUD_MANAGEMENT_URL } from "@/hooks/useManagementUrl.ts"; +import { isNetbirdCloud } from "@/hooks/useManagementUrl.ts"; import { SectionGroup, SettingsBottomBar } from "@/modules/settings/SettingsSection.tsx"; import { cn } from "@/lib/cn"; import { reconcileOrder } from "@/lib/sorting"; @@ -108,7 +108,7 @@ export function ProfilesTab() { await addProfile(name); // SetConfig is keyed by profile name, so it writes the not-yet-active // profile. Write before switching so any reconnect targets the right deployment. - if (managementUrl !== CLOUD_MANAGEMENT_URL) { + if (!isNetbirdCloud(managementUrl)) { await SettingsSvc.SetConfig( new SetConfigParams({ profileName: name, username, managementUrl }), ); diff --git a/client/ui/frontend/src/modules/welcome/WelcomeDialog.tsx b/client/ui/frontend/src/modules/welcome/WelcomeDialog.tsx index cff0e4de5..5ded5e6bd 100644 --- a/client/ui/frontend/src/modules/welcome/WelcomeDialog.tsx +++ b/client/ui/frontend/src/modules/welcome/WelcomeDialog.tsx @@ -10,7 +10,7 @@ import { ConfirmDialog } from "@/components/dialog/ConfirmDialog"; import { useAutoSizeWindow } from "@/hooks/useAutoSizeWindow"; import { errorDialog, formatErrorMessage } from "@/lib/errors"; import i18next from "@/lib/i18n"; -import { isCloudManagementUrl } from "@/hooks/useManagementUrl"; +import { isNetbirdCloud } from "@/hooks/useManagementUrl"; import { WelcomeStepTray } from "./WelcomeStepTray"; import { WelcomeStepManagement } from "./WelcomeStepManagement"; @@ -25,7 +25,7 @@ function shouldShowManagementStep( ): boolean { if (activeProfile !== "default") return false; if (email.trim() !== "") return false; - return isCloudManagementUrl(managementUrl); + return isNetbirdCloud(managementUrl); } type InitialState = { diff --git a/client/ui/frontend/src/modules/welcome/WelcomeStepManagement.tsx b/client/ui/frontend/src/modules/welcome/WelcomeStepManagement.tsx index 37843a2fd..c636e293d 100644 --- a/client/ui/frontend/src/modules/welcome/WelcomeStepManagement.tsx +++ b/client/ui/frontend/src/modules/welcome/WelcomeStepManagement.tsx @@ -10,7 +10,7 @@ import { CLOUD_MANAGEMENT_URL, ManagementMode, checkManagementUrlReachable, - isCloudManagementUrl, + isNetbirdCloud, isValidManagementUrl, normalizeManagementUrl, } from "@/hooks/useManagementUrl"; @@ -27,7 +27,7 @@ export function WelcomeStepManagement({ onContinue, }: Readonly) { const { t } = useTranslation(); - const startsCloud = isCloudManagementUrl(initialUrl); + const startsCloud = isNetbirdCloud(initialUrl); const [mode, setMode] = useState( startsCloud ? ManagementMode.Cloud : ManagementMode.SelfHosted, );