import React from 'react'; import QRCode from 'qrcode'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { NumberInput } from '@/components/ui/number-input'; import { Switch } from '@/components/ui/switch'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout'; import { useDesktopSshStore } from '@/stores/useDesktopSshStore'; import { useUIStore } from '@/stores/useUIStore'; import { toast } from '@/components/ui'; import { Checkbox } from '@/components/ui/checkbox'; import { Radio } from '@/components/ui/radio'; import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import { copyTextToClipboard } from '@/lib/clipboard'; import { openExternalUrl } from '@/lib/url'; import { useI18n, type I18nKey } from '@/lib/i18n'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import type { PendingPairingRecord, RemoteClientRecord } from '@/lib/api/types'; import { buildPairingConnectionPayload, encodePairingConnectionPayload, parsePairingConnectionPayload, type PairingEndpointCandidate } from '@/lib/connectionPayload'; import { desktopSshLogsClear, desktopSshLogs, type DesktopSshInstance, type DesktopSshPortForward, type DesktopSshPortForwardType, } from '@/lib/desktopSsh'; import { desktopHostsGet, desktopHostsSet, desktopInstallIdGet, normalizeHostUrl, redactSensitiveUrl, resolveDesktopHostUrl, relayHostDisplayUrl, type DesktopHost, type DesktopHostRelay, } from '@/lib/desktopHosts'; import { createRelayTunnelClient } from '@/lib/relay/tunnel-client'; import { getDesktopLanAddress, isDesktopLocalOriginActive, isDesktopShell } from '@/lib/desktop'; import { runtimeFetch } from '@/lib/runtime-fetch'; import { getRuntimeApiBaseUrl, switchRuntimeEndpoint } from '@/lib/runtime-switch'; const randomPort = (): number => { return Math.floor(20000 + Math.random() * 30000); }; const isPortInUseError = (error: unknown): boolean => { const message = (error instanceof Error ? error.message : String(error)).toLowerCase(); return message.includes('address already in use') || message.includes('eaddrinuse') || message.includes('port already in use'); }; // Platform this desktop reports about itself when redeeming a pairing link — // display-only metadata for the issuing server's device list. const desktopPlatformName = (): string | undefined => { if (typeof navigator === 'undefined') return undefined; const ua = (navigator.userAgent || '').toLowerCase(); if (ua.includes('mac')) return 'macos'; if (ua.includes('win')) return 'windows'; if (ua.includes('linux')) return 'linux'; return undefined; }; // Friendly label for a device's self-reported platform in the device list. const devicePlatformLabel = (platform?: string | null): string | null => { switch ((platform || '').toLowerCase()) { case 'ios': return 'iOS'; case 'android': return 'Android'; case 'macos': case 'darwin': return 'macOS'; case 'windows': case 'win32': return 'Windows'; case 'linux': return 'Linux'; default: return null; } }; const phaseLabelKey = (phase?: string): I18nKey => { switch (phase) { case 'config_resolved': return 'settings.remoteInstances.page.phase.resolvingConfiguration'; case 'auth_check': return 'settings.remoteInstances.page.phase.checkingAuth'; case 'master_connecting': return 'settings.remoteInstances.page.phase.establishingSsh'; case 'remote_probe': return 'settings.remoteInstances.page.phase.probingRemote'; case 'installing': return 'settings.remoteInstances.page.phase.installingOpenChamber'; case 'updating': return 'settings.remoteInstances.page.phase.updatingOpenChamber'; case 'server_detecting': return 'settings.remoteInstances.page.phase.detectingServer'; case 'server_starting': return 'settings.remoteInstances.page.phase.startingServer'; case 'forwarding': return 'settings.remoteInstances.page.phase.forwardingPorts'; case 'ready': return 'settings.remoteInstances.sidebar.phase.ready'; case 'degraded': return 'settings.remoteInstances.page.phase.reconnecting'; case 'error': return 'settings.remoteInstances.sidebar.phase.error'; default: return 'settings.remoteInstances.sidebar.phase.idle'; } }; const CONNECTING_PHASES = new Set([ 'config_resolved', 'auth_check', 'master_connecting', 'remote_probe', 'installing', 'updating', 'server_detecting', 'server_starting', 'forwarding', ]); const isConnectingPhase = (phase?: string): boolean => { return Boolean(phase && CONNECTING_PHASES.has(phase)); }; const phaseDotClass = (phase?: string): string => { if (phase === 'ready') { return 'bg-[var(--status-success)] animate-pulse'; } if (phase === 'error') { return 'bg-[var(--status-error)] animate-pulse'; } if (phase === 'degraded' || isConnectingPhase(phase)) { return 'bg-[var(--status-warning)] animate-pulse'; } return 'bg-muted-foreground/40'; }; const buildForwardLabel = (forward: DesktopSshPortForward): string => { if (forward.type === 'dynamic') { return `${forward.localHost || '127.0.0.1'}:${forward.localPort || 0}`; } if (forward.type === 'remote') { return `${forward.remoteHost || '127.0.0.1'}:${forward.remotePort || 0} -> ${forward.localHost || '127.0.0.1'}:${forward.localPort || 0}`; } return `${forward.localHost || '127.0.0.1'}:${forward.localPort || 0} -> ${forward.remoteHost || '127.0.0.1'}:${forward.remotePort || 0}`; }; const makeForward = (): DesktopSshPortForward => { return { id: `forward-${Date.now()}-${Math.random().toString(16).slice(2)}`, enabled: true, type: 'local', localHost: '127.0.0.1', localPort: randomPort(), remoteHost: '127.0.0.1', remotePort: 80, }; }; const suggestConcreteHost = (pattern: string): string => { const value = pattern.trim().replace(/\*/g, 'host').replace(/\?/g, 'x'); return value || 'user@host'; }; const HintLabel: React.FC<{ label: string; hint: React.ReactNode }> = ({ label, hint }) => { return ( {label}
{hint}
); }; const forwardTypeDescriptionKey = (type: DesktopSshPortForwardType): I18nKey => { switch (type) { case 'remote': return 'settings.remoteInstances.page.forwardTypeDescription.remote'; case 'dynamic': return 'settings.remoteInstances.page.forwardTypeDescription.dynamic'; default: return 'settings.remoteInstances.page.forwardTypeDescription.local'; } }; const formatEndpoint = (host: string | undefined, port: number | undefined): string => { const value = (host || '').trim(); const normalizedHost = !value || value === '127.0.0.1' || value === '::1' ? 'localhost' : value; return `${normalizedHost}:${port || 0}`; }; const toBrowserHost = (host: string | undefined): string => { const value = (host || '').trim(); if (!value || value === '0.0.0.0' || value === '::') { return '127.0.0.1'; } return value; }; const formatLogLine = (line: string): string => { const match = line.match(/^\[(\d{10,})\]\s*(?:\[([A-Z]+)\]\s*)?(.*)$/); if (!match) { return line; } const millis = Number(match[1]); const iso = Number.isFinite(millis) ? new Date(millis).toISOString() : match[1]; const level = (match[2] || 'INFO').toUpperCase(); const message = match[3] || ''; return `[${iso}] [${level}] ${message}`; }; type HeaderDraft = { id: string; name: string; value: string; }; const createHeaderDraft = (name = '', value = ''): HeaderDraft => ({ id: typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : `header-${Date.now()}-${Math.random().toString(16).slice(2)}`, name, value, }); const isReservedRequestHeaderName = (name: string): boolean => name.trim().toLowerCase() === 'authorization'; const buildRequestHeaders = (headers: HeaderDraft[]): Record | undefined => { const next: Record = {}; for (const header of headers) { const name = header.name.trim(); const value = header.value.trim(); if (name && value && !isReservedRequestHeaderName(name)) next[name] = value; } return Object.keys(next).length > 0 ? next : undefined; }; const readRequestHeaderDrafts = (headers: Record | undefined): HeaderDraft[] => { return Object.entries(headers || {}).map(([name, value]) => createHeaderDraft(name, value)); }; const getRuntimePort = (): number | null => { if (typeof window === 'undefined') { return null; } const runtimeApiBaseUrl = getRuntimeApiBaseUrl(); const portSource = runtimeApiBaseUrl || window.location.href; try { const port = Number(new URL(portSource).port || window.location.port); return Number.isFinite(port) && port > 0 ? port : null; } catch { const port = Number(window.location.port); return Number.isFinite(port) && port > 0 ? port : null; } }; const isLoopbackUrl = (value: string): boolean => { try { const host = new URL(value).hostname.toLowerCase(); return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; } catch { return false; } }; const resolvePairingServerUrl = async (): Promise => { const fallback = normalizeHostUrl(getRuntimeApiBaseUrl()) || window.location.origin; if (!isDesktopShell() || !isDesktopLocalOriginActive()) { return fallback; } let response: Response; try { response = await runtimeFetch('/api/config/settings', { method: 'GET', headers: { Accept: 'application/json' }, }); } catch { return fallback; } if (!response.ok) return fallback; const settings = (await response.json().catch(() => null)) as null | { desktopLanAccessActive?: unknown; }; if (settings?.desktopLanAccessActive !== true) { return fallback; } const address = await getDesktopLanAddress(); const port = getRuntimePort(); if (!address || !port) { return fallback; } return `http://${address}:${port}`; }; const navigateToUrl = (rawUrl: string): void => { const target = rawUrl.trim(); if (!target) { return; } try { window.location.assign(target); } catch { window.location.href = target; } }; const normalizeForSave = (instance: DesktopSshInstance): DesktopSshInstance => { const trimmedCommand = instance.sshCommand.trim(); const nickname = instance.nickname?.trim(); const forwards = instance.portForwards.map((forward) => ({ ...forward, localHost: forward.localHost?.trim() || '127.0.0.1', localPort: typeof forward.localPort === 'number' ? Math.max(1, Math.min(65535, Math.round(forward.localPort))) : undefined, remoteHost: forward.remoteHost?.trim(), remotePort: typeof forward.remotePort === 'number' ? Math.max(1, Math.min(65535, Math.round(forward.remotePort))) : undefined, })); return { ...instance, sshCommand: trimmedCommand, ...(nickname ? { nickname } : { nickname: undefined }), connectionTimeoutSec: Math.max(5, Math.min(240, Math.round(instance.connectionTimeoutSec || 60))), localForward: { ...instance.localForward, bindHost: instance.localForward.bindHost === 'localhost' || instance.localForward.bindHost === '0.0.0.0' ? instance.localForward.bindHost : '127.0.0.1', preferredLocalPort: typeof instance.localForward.preferredLocalPort === 'number' ? Math.max(1, Math.min(65535, Math.round(instance.localForward.preferredLocalPort))) : undefined, }, remoteOpenchamber: { ...instance.remoteOpenchamber, preferredPort: typeof instance.remoteOpenchamber.preferredPort === 'number' ? Math.max(1, Math.min(65535, Math.round(instance.remoteOpenchamber.preferredPort))) : undefined, }, portForwards: forwards, }; }; export const RemoteInstancesPage: React.FC = () => { const { t } = useI18n(); const { clientAuth } = useRuntimeAPIs(); const showInstanceManagement = isDesktopShell(); const instances = useDesktopSshStore((state) => state.instances); const statusesById = useDesktopSshStore((state) => state.statusesById); const importCandidates = useDesktopSshStore((state) => state.importCandidates); const isLoading = useDesktopSshStore((state) => state.isLoading); const isImportsLoading = useDesktopSshStore((state) => state.isImportsLoading); const isSaving = useDesktopSshStore((state) => state.isSaving); const error = useDesktopSshStore((state) => state.error); const load = useDesktopSshStore((state) => state.load); const loadImports = useDesktopSshStore((state) => state.loadImports); const refreshStatuses = useDesktopSshStore((state) => state.refreshStatuses); const upsertInstance = useDesktopSshStore((state) => state.upsertInstance); const createFromCommand = useDesktopSshStore((state) => state.createFromCommand); const removeInstance = useDesktopSshStore((state) => state.removeInstance); const connect = useDesktopSshStore((state) => state.connect); const disconnect = useDesktopSshStore((state) => state.disconnect); const retry = useDesktopSshStore((state) => state.retry); const selectedId = useUIStore((state) => state.settingsRemoteInstancesSelectedId); const setSelectedId = useUIStore((state) => state.setSettingsRemoteInstancesSelectedId); const selectedInstance = React.useMemo(() => { if (!selectedId) return null; return instances.find((instance) => instance.id === selectedId) || null; }, [instances, selectedId]); const [draft, setDraft] = React.useState(null); const [logDialogOpen, setLogDialogOpen] = React.useState(false); const [logDialogLoading, setLogDialogLoading] = React.useState(false); const [logDialogError, setLogDialogError] = React.useState(null); const [logDialogLines, setLogDialogLines] = React.useState([]); const [patternHost, setPatternHost] = React.useState(null); const [patternDestination, setPatternDestination] = React.useState(''); const [patternCreating, setPatternCreating] = React.useState(false); const [expandedForwards, setExpandedForwards] = React.useState>({}); const [isPrimaryActionPending, setIsPrimaryActionPending] = React.useState(false); const [isRetryPending, setIsRetryPending] = React.useState(false); const [clockMs, setClockMs] = React.useState(() => Date.now()); const [directHosts, setDirectHosts] = React.useState([]); const [directDefaultHostId, setDirectDefaultHostId] = React.useState('local'); const [directLoading, setDirectLoading] = React.useState(false); const [directSaving, setDirectSaving] = React.useState(false); const [directLabel, setDirectLabel] = React.useState(''); const [directUrl, setDirectUrl] = React.useState(''); const [directToken, setDirectToken] = React.useState(''); const [directHeaders, setDirectHeaders] = React.useState([]); const [directConnectLink, setDirectConnectLink] = React.useState(''); const [directError, setDirectError] = React.useState(null); const [directAddDialogOpen, setDirectAddDialogOpen] = React.useState(false); const [directImportDialogOpen, setDirectImportDialogOpen] = React.useState(false); const [directEditingId, setDirectEditingId] = React.useState(null); const [directEditLabel, setDirectEditLabel] = React.useState(''); const [directEditUrl, setDirectEditUrl] = React.useState(''); const [directEditToken, setDirectEditToken] = React.useState(''); const [directEditHeaders, setDirectEditHeaders] = React.useState([]); const [remoteClients, setRemoteClients] = React.useState([]); const [pendingPairings, setPendingPairings] = React.useState([]); const [remoteClientsLoading, setRemoteClientsLoading] = React.useState(false); const [remoteClientLabel, setRemoteClientLabel] = React.useState(''); const [remoteClientError, setRemoteClientError] = React.useState(null); const [pairingUrl, setPairingUrl] = React.useState(null); const [pairingQrDataUrl, setPairingQrDataUrl] = React.useState(null); const [pairingCopied, setPairingCopied] = React.useState(false); // "Add a device" dialog: a configure phase (name + transport + fallback) then a // result phase (QR + link). The QR only ever shows inside this dialog. const [addDeviceOpen, setAddDeviceOpen] = React.useState(false); const [addDevicePhase, setAddDevicePhase] = React.useState<'configure' | 'result'>('configure'); const [addDeviceCreating, setAddDeviceCreating] = React.useState(false); const [addDeviceTransport, setAddDeviceTransport] = React.useState<'local' | 'lan' | 'relay'>('relay'); const [addDeviceFallback, setAddDeviceFallback] = React.useState(true); const [transportOptions, setTransportOptions] = React.useState<{ localUrl: string | null; lanUrl: string | null; relayAvailable: boolean } | null>(null); const revokedClientCount = React.useMemo(() => remoteClients.filter((client) => Boolean(client.revokedAt)).length, [remoteClients]); const [sshAddDialogOpen, setSshAddDialogOpen] = React.useState(false); const [sshCommandDraft, setSshCommandDraft] = React.useState('ssh user@example.com'); const [sshNameDraft, setSshNameDraft] = React.useState(''); React.useEffect(() => { void load(); void loadImports(); }, [load, loadImports]); const loadDirectHosts = React.useCallback(async () => { setDirectLoading(true); setDirectError(null); try { const config = await desktopHostsGet(); setDirectHosts(config.hosts || []); setDirectDefaultHostId(config.defaultHostId || 'local'); } catch (err) { setDirectError(err instanceof Error ? err.message : String(err)); } finally { setDirectLoading(false); } }, []); React.useEffect(() => { void loadDirectHosts(); }, [loadDirectHosts]); const persistDirectHosts = React.useCallback(async (hosts: DesktopHost[], defaultHostId: string | null = directDefaultHostId) => { setDirectSaving(true); setDirectError(null); try { await desktopHostsSet({ hosts, defaultHostId, initialHostChoiceCompleted: true }); setDirectHosts(hosts); setDirectDefaultHostId(defaultHostId); } catch (err) { setDirectError(err instanceof Error ? err.message : String(err)); } finally { setDirectSaving(false); } }, [directDefaultHostId]); const handleAddDirectHost = React.useCallback(async () => { const resolved = resolveDesktopHostUrl(directUrl); if (!resolved) { setDirectError(t('desktopHostSwitcher.error.invalidUrl')); return; } const url = resolved.persistedUrl; const id = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : `host-${Date.now()}-${Math.random().toString(16).slice(2)}`; const host: DesktopHost = { id, label: directLabel.trim() || redactSensitiveUrl(url), url, apiUrl: url, ...(directToken.trim() ? { clientToken: directToken.trim() } : {}), ...(buildRequestHeaders(directHeaders) ? { requestHeaders: buildRequestHeaders(directHeaders) } : {}), }; await persistDirectHosts([host, ...directHosts], directDefaultHostId); setDirectLabel(''); setDirectUrl(''); setDirectToken(''); setDirectHeaders([]); setDirectAddDialogOpen(false); if (resolved.redeemUrl) { navigateToUrl(resolved.redeemUrl); } }, [directDefaultHostId, directHeaders, directHosts, directLabel, directToken, directUrl, persistDirectHosts, t]); const importDirectConnectLink = React.useCallback(async () => { const payload = parsePairingConnectionPayload(directConnectLink); if (!payload) { setDirectError(t('settings.remoteInstances.direct.error.invalidConnectLink')); return; } // The redeem body is identical across every transport (the desktop is the // same device however it reaches the server). The install-id dedupe key // collapses re-pairing / re-auth of this desktop into one device record. const installId = await desktopInstallIdGet().catch(() => ''); const redeemBody = JSON.stringify({ pairingId: payload.pairingId, secret: payload.secret, clientLabel: payload.label || 'OpenChamber Desktop', clientKind: 'desktop', deviceName: 'OpenChamber Desktop', devicePlatform: desktopPlatformName(), ...(installId ? { dedupeKey: `desktop:${installId}` } : {}), }); const redeemInit: RequestInit = { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: redeemBody, }; const tokenFromResponse = async (response: Response): Promise => { if (!response.ok) return null; const body = (await response.json().catch(() => null)) as { clientToken?: unknown } | null; const token = typeof body?.clientToken === 'string' ? body.clientToken.trim() : ''; return token || null; }; // Try direct (LAN/tunnel) candidates first — they're cheaper and don't need // relay infrastructure — then fall back to relay. Ordered by payload priority. const ordered = [...payload.candidates].sort( (a, b) => (a.type === 'relay' ? 1 : 0) - (b.type === 'relay' ? 1 : 0), ); let redeemed: | { kind: 'direct'; url: string; token: string } | { kind: 'relay'; relay: DesktopHostRelay; token: string } | null = null; for (const candidate of ordered) { if (candidate.type === 'relay') { // Open a throwaway E2EE tunnel just to redeem the one-time secret; the // grant (if any) authorizes admission to the relay for this serverId. const tunnel = createRelayTunnelClient({ relayUrl: candidate.relayUrl, serverId: candidate.serverId, hostEncPubJwk: candidate.hostEncPubJwk, ...(candidate.grant ? { grant: candidate.grant } : {}), }); try { const response = await tunnel.fetch('/api/client-auth/pairing/redeem', redeemInit); const token = await tokenFromResponse(response); if (token) { redeemed = { kind: 'relay', // grant is intentionally not persisted (one-time pairing artifact). relay: { relayUrl: candidate.relayUrl, serverId: candidate.serverId, hostEncPubJwk: candidate.hostEncPubJwk }, token, }; break; } } catch { // Relay unreachable / handshake failed — try the next candidate. } finally { tunnel.close(); } continue; } // Direct: the remote instance is a user-provided URL, so a plain // cross-origin fetch is correct here (not the active runtime). const candidateUrl = normalizeHostUrl(candidate.url); if (!candidateUrl) continue; try { const response = await fetch(`${candidateUrl}/api/client-auth/pairing/redeem`, redeemInit); const token = await tokenFromResponse(response); if (token) { redeemed = { kind: 'direct', url: candidateUrl, token }; break; } } catch { // Unreachable candidate — try the next one. } } if (!redeemed) { setDirectError(t('desktopHostSwitcher.error.invalidUrl')); return; } const makeId = (): string => (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : `host-${Date.now()}-${Math.random().toString(16).slice(2)}`); if (redeemed.kind === 'relay') { const { relay, token } = redeemed; // Relay hosts are keyed by serverId (one host per server, regardless of // which relay routes it), so re-importing updates the existing record. const existing = directHosts.find((host) => host.relay?.serverId === relay.serverId); const displayUrl = relayHostDisplayUrl(relay.serverId); if (existing) { const nextHosts = directHosts.map((host) => host.id === existing.id ? { ...host, label: payload.label || host.label, url: displayUrl, apiUrl: undefined, clientToken: token, relay } : host); await persistDirectHosts(nextHosts, directDefaultHostId); } else { // payload.label is normally the issuing server's hostname; the pseudo-URL // is only a last-resort display name. await persistDirectHosts([{ id: makeId(), label: payload.label || displayUrl, url: displayUrl, clientToken: token, relay }, ...directHosts], directDefaultHostId); } } else { const { url, token } = redeemed; const existing = directHosts.find((host) => !host.relay && normalizeHostUrl(host.apiUrl || host.url) === url); if (existing) { const nextHosts = directHosts.map((host) => host.id === existing.id ? { ...host, label: payload.label || host.label, url, apiUrl: url, clientToken: token } : host); await persistDirectHosts(nextHosts, directDefaultHostId); } else { await persistDirectHosts([{ id: makeId(), label: payload.label || redactSensitiveUrl(url), url, apiUrl: url, clientToken: token }, ...directHosts], directDefaultHostId); } } setDirectConnectLink(''); setDirectError(null); setDirectImportDialogOpen(false); }, [directConnectLink, directDefaultHostId, directHosts, persistDirectHosts, t]); const handleRemoveDirectHost = React.useCallback(async (id: string) => { const nextHosts = directHosts.filter((host) => host.id !== id); const nextDefault = directDefaultHostId === id ? 'local' : directDefaultHostId; await persistDirectHosts(nextHosts, nextDefault); if (directEditingId === id) { setDirectEditingId(null); } }, [directDefaultHostId, directEditingId, directHosts, persistDirectHosts]); const beginEditDirectHost = React.useCallback((host: DesktopHost) => { setDirectEditingId(host.id); setDirectEditLabel(host.label); setDirectEditUrl(host.apiUrl || host.url); setDirectEditToken(host.clientToken || ''); setDirectEditHeaders(readRequestHeaderDrafts(host.requestHeaders)); setDirectError(null); }, []); const saveDirectHostEdit = React.useCallback(async () => { if (!directEditingId) return; const resolved = resolveDesktopHostUrl(directEditUrl); if (!resolved) { setDirectError(t('desktopHostSwitcher.error.invalidUrl')); return; } const url = resolved.persistedUrl; const nextHosts = directHosts.map((host) => host.id === directEditingId ? { ...host, label: directEditLabel.trim() || redactSensitiveUrl(url), url, apiUrl: url, clientToken: directEditToken.trim() || undefined, requestHeaders: buildRequestHeaders(directEditHeaders), } : host); await persistDirectHosts(nextHosts, directDefaultHostId); setDirectEditingId(null); if (resolved.redeemUrl) { navigateToUrl(resolved.redeemUrl); } }, [directDefaultHostId, directEditHeaders, directEditLabel, directEditToken, directEditUrl, directEditingId, directHosts, persistDirectHosts, t]); const createSshInstanceFromDialog = React.useCallback(async () => { const command = sshCommandDraft.trim(); if (!command) { toast.error(t('settings.remoteInstances.page.toast.sshCommandRequired')); return; } const id = `ssh-${Date.now()}-${Math.random().toString(16).slice(2)}`; try { await createFromCommand(id, command, sshNameDraft.trim() || t('settings.remoteInstances.sidebar.newSshInstanceName')); setSelectedId(id); setSshAddDialogOpen(false); setSshCommandDraft('ssh user@example.com'); setSshNameDraft(''); toast.success(t('settings.remoteInstances.page.toast.instanceCreated')); } catch (error) { toast.error(t('settings.remoteInstances.sidebar.toast.createFailed'), { description: error instanceof Error ? error.message : String(error), }); } }, [createFromCommand, setSelectedId, sshCommandDraft, sshNameDraft, t]); const setDefaultDirectHost = React.useCallback(async (id: string) => { await persistDirectHosts(directHosts, id); }, [directHosts, persistDirectHosts]); const loadRemoteClients = React.useCallback(async (options?: { silent?: boolean }) => { if (!clientAuth) return; if (!options?.silent) setRemoteClientsLoading(true); if (!options?.silent) setRemoteClientError(null); try { const [clients, pending] = await Promise.all([ clientAuth.listClients(), clientAuth.listPendingPairings().catch(() => [] as PendingPairingRecord[]), ]); setRemoteClients(clients); setPendingPairings(pending); } catch (err) { // A silent poll must not surface a transient error over the live list. if (!options?.silent) setRemoteClientError(err instanceof Error ? err.message : String(err)); } finally { if (!options?.silent) setRemoteClientsLoading(false); } }, [clientAuth]); const cancelPendingPairing = React.useCallback(async (id: string) => { if (!clientAuth) return; try { await clientAuth.cancelPairing(id); setPendingPairings((prev) => prev.filter((entry) => entry.id !== id)); await loadRemoteClients({ silent: true }); } catch (err) { setRemoteClientError(err instanceof Error ? err.message : String(err)); } }, [clientAuth, loadRemoteClients]); // Load on mount, then poll while the page is visible so a device that redeems // a pairing link shows up in the list without reopening settings. React.useEffect(() => { if (!clientAuth) return; void loadRemoteClients(); const interval = window.setInterval(() => { if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return; void loadRemoteClients({ silent: true }); }, 5_000); return () => window.clearInterval(interval); }, [clientAuth, loadRemoteClients]); // Available direct transports for the create dialog. The server is authoritative // for LAN reachability (derived from its bind, not the UI origin), so "Local // network" works even when the UI is opened on localhost. Falls back to the // client-side guess if the endpoint is unavailable. const resolveTransportOptions = React.useCallback(async (): Promise<{ localUrl: string | null; lanUrl: string | null; relayAvailable: boolean }> => { if (clientAuth?.getPairingTransports) { try { const transports = await clientAuth.getPairingTransports(); return { localUrl: transports.local, lanUrl: transports.lan, relayAvailable: transports.relayAvailable }; } catch { // fall through to the client-side guess } } const port = getRuntimePort(); const localUrl = port ? `http://127.0.0.1:${port}` : (isLoopbackUrl(window.location.origin) ? window.location.origin : null); let lanUrl: string | null = null; try { const resolved = normalizeHostUrl(await resolvePairingServerUrl()); lanUrl = resolved && !isLoopbackUrl(resolved) ? resolved : null; } catch { // keep null } return { localUrl, lanUrl, relayAvailable: true }; }, [clientAuth]); const openAddDevice = React.useCallback(async () => { setRemoteClientError(null); setPairingUrl(null); setPairingQrDataUrl(null); setPairingCopied(false); setAddDevicePhase('configure'); setAddDeviceFallback(true); setAddDeviceOpen(true); const opts = await resolveTransportOptions(); setTransportOptions(opts); // "Anywhere" (relay, with home-network preference) is the right default for // most people; fall back to narrower options only when relay is unavailable. setAddDeviceTransport(opts.relayAvailable ? 'relay' : opts.lanUrl ? 'lan' : 'local'); }, [resolveTransportOptions]); const createPairingLink = React.useCallback(async () => { if (!clientAuth?.createPairingSession || !transportOptions) return; setRemoteClientError(null); setAddDeviceCreating(true); try { const label = remoteClientLabel.trim() || undefined; // Map the chosen transport (+ fallback) to the per-link candidate request. let serverUrl: string | undefined; let includeRelay: boolean; let includeDirect = true; if (addDeviceTransport === 'local') { serverUrl = transportOptions.localUrl ?? undefined; includeRelay = false; } else if (addDeviceTransport === 'lan') { serverUrl = transportOptions.lanUrl ?? undefined; includeRelay = addDeviceFallback; } else if (addDeviceFallback && transportOptions.lanUrl) { // Relay, but prefer the local network when available: carry both. serverUrl = transportOptions.lanUrl; includeRelay = true; } else { // Relay only. includeDirect = false; includeRelay = true; } const { pairing, server } = await clientAuth.createPairingSession({ label, allowedClientKinds: ['mobile', 'desktop'], serverUrl, includeRelay, includeDirect, }); const payload = buildPairingConnectionPayload({ pairingId: pairing.id, secret: pairing.secret, // The typed name (`label`) is the per-device label shown in THIS server's // device list; it already went to createPairingSession above. The payload // label is what the paired device names its connection by, which must be // the issuing server's name (hostname), not the device's own name. label: server.label, fingerprint: pairing.fingerprint ?? undefined, expiresAt: pairing.expiresAt, candidates: server.candidates as unknown as PairingEndpointCandidate[], }); const encoded = encodePairingConnectionPayload(payload); setPairingUrl(encoded); // Pairing payloads are dense (multiple transport candidates + the relay // E2EE key), so render at high resolution with low error-correction. setPairingQrDataUrl(await QRCode.toDataURL(encoded, { width: 1024, margin: 2, errorCorrectionLevel: 'L' })); setPairingCopied(false); setAddDevicePhase('result'); await loadRemoteClients({ silent: true }); } catch (err) { setRemoteClientError(err instanceof Error ? err.message : String(err)); } finally { setAddDeviceCreating(false); } }, [clientAuth, transportOptions, addDeviceTransport, addDeviceFallback, remoteClientLabel, loadRemoteClients]); const handleCopyPairing = React.useCallback(() => { if (!pairingUrl) return; void copyTextToClipboard(pairingUrl).then((result) => { if (!result.ok) return; setPairingCopied(true); window.setTimeout(() => setPairingCopied(false), 2000); }); }, [pairingUrl]); const revokeRemoteClient = React.useCallback(async (client: RemoteClientRecord) => { if (!clientAuth) return; const isLocalDesktopClient = client.clientKind === 'desktop-local'; setRemoteClientError(null); try { await clientAuth.revokeClient(client.id); if (isLocalDesktopClient && isDesktopShell()) { const config = await desktopHostsGet(); await desktopHostsSet({ hosts: config.hosts, defaultHostId: config.defaultHostId, initialHostChoiceCompleted: config.initialHostChoiceCompleted, localClientToken: null, }); setRemoteClients((clients) => clients.map((entry) => entry.id === client.id ? { ...entry, revokedAt: new Date().toISOString() } : entry)); switchRuntimeEndpoint({ apiBaseUrl: getRuntimeApiBaseUrl(), clientToken: null, runtimeKey: 'local' }); return; } await loadRemoteClients(); } catch (err) { setRemoteClientError(err instanceof Error ? err.message : String(err)); } }, [clientAuth, loadRemoteClients]); const purgeRevokedRemoteClients = React.useCallback(async () => { if (!clientAuth) return; setRemoteClientError(null); try { await clientAuth.purgeRevokedClients(); await loadRemoteClients(); } catch (err) { setRemoteClientError(err instanceof Error ? err.message : String(err)); } }, [clientAuth, loadRemoteClients]); React.useEffect(() => { setDraft(selectedInstance); }, [selectedInstance]); React.useEffect(() => { if (!selectedId) { return; } const interval = window.setInterval(() => { // Skip polling when tab is hidden to reduce background work if (typeof document !== 'undefined' && document.visibilityState !== 'visible') { return; } void refreshStatuses(); }, 2_000); return () => { window.clearInterval(interval); }; }, [refreshStatuses, selectedId]); React.useEffect(() => { // Use requestAnimationFrame for smoother clock updates without setInterval overhead let rafId: number | null = null; let lastTime = Date.now(); const tick = () => { const now = Date.now(); // Update only once per second if (now - lastTime >= 1_000) { setClockMs(now); lastTime = now; } rafId = requestAnimationFrame(tick); }; // Only run when visible if (typeof document === 'undefined' || document.visibilityState === 'visible') { rafId = requestAnimationFrame(tick); } const onVisibility = () => { if (document.visibilityState === 'visible' && rafId === null) { rafId = requestAnimationFrame(tick); } else if (document.visibilityState !== 'visible' && rafId !== null) { cancelAnimationFrame(rafId); rafId = null; } }; document.addEventListener('visibilitychange', onVisibility); return () => { document.removeEventListener('visibilitychange', onVisibility); if (rafId !== null) { cancelAnimationFrame(rafId); } }; }, []); const status = selectedId ? statusesById[selectedId] : null; const statusPhase = status?.phase; const isReady = statusPhase === 'ready'; const isReconnecting = statusPhase === 'degraded'; const isConnecting = isConnectingPhase(statusPhase); const isBusy = isConnecting || isReconnecting; const canDisconnect = isReady || isBusy; const statusAgeMs = status ? Math.max(0, clockMs - status.updatedAtMs) : 0; const reconnectAppearsStuck = isReconnecting && statusAgeMs > 12_000; const hasChanges = React.useMemo(() => { if (!draft || !selectedInstance) return false; return JSON.stringify(draft) !== JSON.stringify(selectedInstance); }, [draft, selectedInstance]); const updateDraft = React.useCallback((updater: (current: DesktopSshInstance) => DesktopSshInstance) => { setDraft((current) => (current ? updater(current) : current)); }, []); const handleSave = React.useCallback(async () => { if (!draft) return; const normalized = normalizeForSave(draft); if (!normalized.sshCommand.trim()) { toast.error(t('settings.remoteInstances.page.toast.sshCommandRequired')); return; } if (normalized.localForward.bindHost === '0.0.0.0') { const allow = window.confirm( t('settings.remoteInstances.page.confirm.bindAllInterfaces'), ); if (!allow) { return; } } if ( normalized.auth.sshPassword?.enabled && normalized.auth.sshPassword.value?.trim() && normalized.auth.sshPassword.store !== 'settings' ) { const store = window.confirm(t('settings.remoteInstances.page.confirm.storeSshPasswordPlaintext')); normalized.auth.sshPassword.store = store ? 'settings' : 'never'; if (!store) { normalized.auth.sshPassword.value = undefined; } } if ( normalized.auth.openchamberPassword?.enabled && normalized.auth.openchamberPassword.value?.trim() && normalized.auth.openchamberPassword.store !== 'settings' ) { const store = window.confirm(t('settings.remoteInstances.page.confirm.storeUiPasswordPlaintext')); normalized.auth.openchamberPassword.store = store ? 'settings' : 'never'; if (!store) { normalized.auth.openchamberPassword.value = undefined; } } try { await upsertInstance(normalized); toast.success(t('settings.remoteInstances.page.toast.instanceSaved')); } catch (error) { toast.error(t('settings.remoteInstances.page.toast.saveFailed'), { description: error instanceof Error ? error.message : String(error), }); } }, [draft, t, upsertInstance]); const createImportedInstance = React.useCallback( async (host: string, destination: string): Promise => { const id = `ssh-${Date.now()}-${Math.random().toString(16).slice(2)}`; try { await createFromCommand(id, `ssh ${destination}`, host); setSelectedId(id); toast.success(t('settings.remoteInstances.page.toast.instanceCreated')); return true; } catch (error) { toast.error(t('settings.remoteInstances.sidebar.toast.createFailed'), { description: error instanceof Error ? error.message : String(error), }); return false; } }, [createFromCommand, setSelectedId, t], ); const closePatternDialog = React.useCallback(() => { if (patternCreating) { return; } setPatternHost(null); setPatternDestination(''); }, [patternCreating]); const handleImportCandidate = React.useCallback( (host: string, pattern: boolean) => { if (pattern) { setPatternHost(host); setPatternDestination(suggestConcreteHost(host)); return; } void createImportedInstance(host, host); }, [createImportedInstance], ); const handlePatternCreate = React.useCallback(async () => { const host = patternHost; const destination = patternDestination.trim(); if (!host) { return; } if (!destination) { toast.error(t('settings.remoteInstances.page.toast.destinationRequired')); return; } setPatternCreating(true); try { const created = await createImportedInstance(host, destination); if (created) { setPatternHost(null); setPatternDestination(''); } } finally { setPatternCreating(false); } }, [createImportedInstance, patternDestination, patternHost, t]); const connectWithPortRecovery = React.useCallback(async () => { if (!selectedInstance) return; try { await connect(selectedInstance.id); return; } catch (error) { if (!isPortInUseError(error)) { throw error; } const allow = window.confirm(t('settings.remoteInstances.sidebar.confirm.localPortInUseRetry')); if (!allow) { throw error; } const nextInstance: DesktopSshInstance = { ...selectedInstance, localForward: { ...selectedInstance.localForward, preferredLocalPort: randomPort(), }, }; await upsertInstance(nextInstance); await connect(nextInstance.id); toast.success(t('settings.remoteInstances.sidebar.toast.retriedWithRandomPort')); } }, [connect, selectedInstance, t, upsertInstance]); const readLogsForInstance = React.useCallback(async (id: string) => { const lines = await desktopSshLogs(id, 600); return lines.map((line) => formatLogLine(line)); }, []); const handleOpenLogs = React.useCallback(async () => { if (!draft) return; setLogDialogOpen(true); setLogDialogLoading(true); setLogDialogError(null); try { const lines = await readLogsForInstance(draft.id); setLogDialogLines(lines); } catch (error) { setLogDialogLines([]); setLogDialogError(error instanceof Error ? error.message : String(error)); } finally { setLogDialogLoading(false); } }, [draft, readLogsForInstance]); React.useEffect(() => { if (!logDialogOpen || !draft) { return; } let disposed = false; const run = async () => { try { const lines = await readLogsForInstance(draft.id); if (!disposed) { setLogDialogLines(lines); setLogDialogError(null); } } catch (error) { if (!disposed) { setLogDialogError(error instanceof Error ? error.message : String(error)); } } }; void run(); const interval = window.setInterval(() => { // Skip polling when tab is hidden if (typeof document !== 'undefined' && document.visibilityState !== 'visible') { return; } void run(); }, 1_000); return () => { disposed = true; window.clearInterval(interval); }; }, [draft, logDialogOpen, readLogsForInstance]); const logLinesText = React.useMemo(() => logDialogLines.join('\n'), [logDialogLines]); const handleCopyAllLogs = React.useCallback(() => { if (!logLinesText.trim()) { toast.error(t('settings.remoteInstances.page.toast.noLogsToCopy')); return; } void copyTextToClipboard(logLinesText).then((result) => { if (result.ok) { toast.success(t('settings.remoteInstances.page.toast.logsCopied')); } }); }, [logLinesText, t]); const handleClearLogs = React.useCallback(async () => { if (!draft) { return; } try { await desktopSshLogsClear(draft.id); setLogDialogLines([]); toast.success(t('settings.remoteInstances.page.toast.logsCleared')); } catch (error) { toast.error(t('settings.remoteInstances.page.toast.clearLogsFailed'), { description: error instanceof Error ? error.message : String(error), }); } }, [draft, t]); const handleOpenCurrentInstance = React.useCallback(async () => { if (!status?.localUrl) { toast.error(t('settings.remoteInstances.page.toast.instanceUrlUnavailable')); return; } const target = status.localUrl.trim(); if (!target) { toast.error(t('settings.remoteInstances.page.toast.instanceUrlUnavailable')); return; } navigateToUrl(target); }, [status?.localUrl, t]); const handlePrimaryConnectionAction = React.useCallback(() => { if (!draft) { return; } setIsPrimaryActionPending(true); const operation = canDisconnect ? disconnect(draft.id) : connectWithPortRecovery(); void operation .catch((error) => { const key = canDisconnect ? (isReady ? 'settings.remoteInstances.page.toast.disconnectFailed' : 'settings.remoteInstances.page.toast.cancelConnectionFailed') : 'settings.remoteInstances.page.toast.connectFailed'; toast.error(t(key), { description: error instanceof Error ? error.message : String(error), }); }) .finally(() => { setIsPrimaryActionPending(false); }); }, [canDisconnect, connectWithPortRecovery, disconnect, draft, isReady, t]); const handleRetryAction = React.useCallback(() => { if (!draft) { return; } if (isConnecting) { return; } setIsRetryPending(true); const operation = isReconnecting ? disconnect(draft.id).then(() => connectWithPortRecovery()) : retry(draft.id); void operation .catch((error) => { toast.error(t('settings.remoteInstances.page.toast.retryFailed'), { description: error instanceof Error ? error.message : String(error), }); }) .finally(() => { setIsRetryPending(false); }); }, [connectWithPortRecovery, disconnect, draft, isConnecting, isReconnecting, retry, t]); const retryButtonLabel = isConnecting ? t('settings.remoteInstances.page.actions.connecting') : isReconnecting ? reconnectAppearsStuck ? t('settings.remoteInstances.page.actions.reconnectNow') : t('settings.remoteInstances.page.actions.reconnecting') : t('settings.remoteInstances.sidebar.actions.retry'); const canRetry = !isPrimaryActionPending && !isRetryPending && (statusPhase === 'error' || statusPhase === 'idle' || !statusPhase || (isReconnecting && reconnectAppearsStuck)) && !isConnecting; const primaryButtonLabel = isReady ? t('settings.remoteInstances.sidebar.actions.disconnect') : canDisconnect ? t('settings.remoteInstances.page.actions.cancel') : t('settings.remoteInstances.sidebar.actions.connect'); if (!draft) { return ( {clientAuth ? (

{t('settings.remoteInstances.clientAuth.title')}

{t('settings.remoteInstances.clientAuth.description')}

{revokedClientCount > 0 ? (
) : null} {remoteClientsLoading && remoteClients.length === 0 && pendingPairings.length === 0 ? (

{t('settings.remoteInstances.clientAuth.state.loading')}

) : remoteClients.length === 0 && pendingPairings.length === 0 ? (

{t('settings.remoteInstances.clientAuth.state.empty')}

) : ( <> {pendingPairings.map((pending) => (

{pending.label || t('settings.remoteInstances.clientAuth.field.labelPlaceholder')}

{pending.usesRelay ? ( {t('settings.remoteInstances.clientAuth.state.viaRelay')} ) : null}

{t('settings.remoteInstances.clientAuth.state.pending')}

))} {remoteClients.map((client) => { const isLocalDesktopClient = client.clientKind === 'desktop-local'; // Live presence: the server refreshes lastUsedAt on every // authenticated request (writes throttled to 60s), so a // device with activity in the last 90s is connected NOW. // The list polls every 5s, keeping this fresh. const lastUsedMs = client.lastUsedAt ? Date.parse(client.lastUsedAt) : Number.NaN; const isOnline = !client.revokedAt && (isLocalDesktopClient || (Number.isFinite(lastUsedMs) && Date.now() - lastUsedMs < 90_000)); const statusText = client.revokedAt ? t('settings.remoteInstances.clientAuth.state.revoked') : isOnline ? (client.lastTransport === 'relay' && !isLocalDesktopClient ? t('settings.remoteInstances.clientAuth.state.connectedRelay') : t('settings.remoteInstances.clientAuth.state.connectedDirect')) : client.lastUsedAt ? t('settings.remoteInstances.clientAuth.lastUsed', { date: client.lastUsedAt }) : t('settings.remoteInstances.clientAuth.neverUsed'); return (

{client.label}

{devicePlatformLabel(client.devicePlatform) ? ( {devicePlatformLabel(client.devicePlatform)} ) : null} {isLocalDesktopClient ? ( {t('settings.remoteInstances.clientAuth.state.thisDevice')} ) : null}

{statusText}

); })} )}
{remoteClientError ?

{remoteClientError}

: null}
) : null} {showInstanceManagement ?

{t('settings.remoteInstances.direct.title')}

{t('settings.remoteInstances.direct.description')}

{t('settings.remoteInstances.direct.note')}

{directLoading ? (

{t('settings.remoteInstances.direct.state.loading')}

) : directHosts.length === 0 ? (

{t('settings.remoteInstances.direct.state.empty')}

) : directHosts.map((host) => (

{redactSensitiveUrl(host.label)}

{directDefaultHostId === host.id ? {t('desktopHostSwitcher.header.default')} : null}

{redactSensitiveUrl(host.apiUrl || host.url)}

))}
{directError ?

{directError}

: null}
: null} {showInstanceManagement ? {t('settings.remoteInstances.direct.actions.add')} {t('settings.remoteInstances.direct.description')}
{ event.preventDefault(); void handleAddDirectHost(); }}> setDirectLabel(event.target.value)} placeholder={t('settings.remoteInstances.direct.field.labelPlaceholder')} disabled={directSaving} /> setDirectUrl(event.target.value)} placeholder={t('settings.remoteInstances.direct.field.urlPlaceholder')} disabled={directSaving} autoFocus /> setDirectToken(event.target.value)} placeholder={t('settings.remoteInstances.direct.field.tokenPlaceholder')} type="password" disabled={directSaving} />

{t('settings.remoteInstances.direct.headers.title')}

{t('settings.remoteInstances.direct.headers.description')}

{directHeaders.map((header) => (
setDirectHeaders((headers) => headers.map((item) => item.id === header.id ? { ...item, name: event.target.value } : item))} placeholder={t('settings.remoteInstances.direct.headers.field.namePlaceholder')} disabled={directSaving} /> setDirectHeaders((headers) => headers.map((item) => item.id === header.id ? { ...item, value: event.target.value } : item))} placeholder={t('settings.remoteInstances.direct.headers.field.valuePlaceholder')} type="password" disabled={directSaving} />
))}
: null} {showInstanceManagement ? { if (!open) setDirectEditingId(null); }}> {t('desktopHostSwitcher.actions.edit')} {t('settings.remoteInstances.direct.description')}
{ event.preventDefault(); void saveDirectHostEdit(); }}> setDirectEditLabel(event.target.value)} placeholder={t('settings.remoteInstances.direct.field.labelPlaceholder')} disabled={directSaving} /> setDirectEditUrl(event.target.value)} placeholder={t('settings.remoteInstances.direct.field.urlPlaceholder')} disabled={directSaving} autoFocus /> setDirectEditToken(event.target.value)} placeholder={t('settings.remoteInstances.direct.field.tokenPlaceholder')} type="password" disabled={directSaving} />

{t('settings.remoteInstances.direct.headers.title')}

{t('settings.remoteInstances.direct.headers.description')}

{directEditHeaders.map((header) => (
setDirectEditHeaders((headers) => headers.map((item) => item.id === header.id ? { ...item, name: event.target.value } : item))} placeholder={t('settings.remoteInstances.direct.headers.field.namePlaceholder')} disabled={directSaving} /> setDirectEditHeaders((headers) => headers.map((item) => item.id === header.id ? { ...item, value: event.target.value } : item))} placeholder={t('settings.remoteInstances.direct.headers.field.valuePlaceholder')} type="password" disabled={directSaving} />
))}
: null} {showInstanceManagement ? {t('settings.remoteInstances.direct.import.action')} {t('settings.remoteInstances.direct.import.description')}
{ event.preventDefault(); void importDirectConnectLink(); }}> setDirectConnectLink(event.target.value)} placeholder={t('settings.remoteInstances.direct.import.placeholder')} disabled={directSaving} autoFocus />
: null} {addDevicePhase === 'result' ? t('settings.remoteInstances.clientAuth.qrDialogTitle') : t('settings.remoteInstances.clientAuth.actions.addDevice')} {/* Configure phase: what this dialog will produce. Result phase: what to do with the QR code that is now on screen. */} {addDevicePhase === 'result' ? t('settings.remoteInstances.clientAuth.qrScanHint') : t('settings.remoteInstances.clientAuth.addDevice.subtitle')} {addDevicePhase === 'configure' ? (
{ event.preventDefault(); void createPairingLink(); }}> setRemoteClientLabel(event.target.value)} placeholder={t('settings.remoteInstances.clientAuth.field.labelPlaceholder')} autoFocus />

{t('settings.remoteInstances.clientAuth.addDevice.transportLabel')}

{/* Ordered by how likely a first-time user is to want each option; "Anywhere" is the default. Every option explains its outcome in plain words — "relay" appears only inside the description. */}
{([ { key: 'relay' as const, label: t('settings.remoteInstances.clientAuth.addDevice.transport.relay'), hint: t('settings.remoteInstances.clientAuth.addDevice.transport.relayHint'), available: Boolean(transportOptions?.relayAvailable) }, { key: 'lan' as const, label: t('settings.remoteInstances.clientAuth.addDevice.transport.lan'), hint: t('settings.remoteInstances.clientAuth.addDevice.transport.lanHint'), available: Boolean(transportOptions?.lanUrl) }, { key: 'local' as const, label: t('settings.remoteInstances.clientAuth.addDevice.transport.local'), hint: t('settings.remoteInstances.clientAuth.addDevice.transport.localHint'), available: Boolean(transportOptions?.localUrl) }, ]).map((option) => { const selected = addDeviceTransport === option.key; return (
{ if (option.available) setAddDeviceTransport(option.key); }} role="presentation" > setAddDeviceTransport(option.key)} ariaLabel={option.label} className="mt-0.5" />

{option.label}

{option.hint}

); })}
{addDeviceTransport === 'lan' ? ( ) : null} {addDeviceTransport === 'relay' && transportOptions?.lanUrl ? ( ) : null}
{remoteClientError ?

{remoteClientError}

: null}
) : (
{pairingQrDataUrl ? (
{t('settings.remoteInstances.clientAuth.qrAlt')}
) : null} {pairingUrl ? (
{pairingUrl}
) : null}
)}
{showInstanceManagement ?

{t('settings.remoteInstances.sidebar.title')}

{t('settings.remoteInstances.sidebar.total', { count: instances.length })}

{isLoading ? (

{t('settings.remoteInstances.page.import.loading')}

) : instances.length === 0 ? (

{t('settings.remoteInstances.page.import.noneFound')}

) : instances.map((instance) => { const instanceStatus = statusesById[instance.id]; const title = instance.nickname?.trim() || instance.sshParsed?.destination || instance.id; const phase = instanceStatus?.phase; const ready = phase === 'ready'; return (

{title}

{t(phaseLabelKey(phase))}{instanceStatus?.localUrl ? ` · ${instanceStatus.localUrl}` : ''}

); })}
: null} {showInstanceManagement ? {t('settings.remoteInstances.sidebar.actions.addSshInstance')} {t('settings.remoteInstances.page.section.instanceDescription')}
{ event.preventDefault(); void createSshInstanceFromDialog(); }}> setSshNameDraft(event.target.value)} placeholder={t('settings.remoteInstances.page.field.nicknamePlaceholder')} disabled={isSaving} /> setSshCommandDraft(event.target.value)} placeholder={t('settings.remoteInstances.page.field.sshCommandPlaceholder')} disabled={isSaving} autoFocus />
: null} {showInstanceManagement ?

{t('settings.remoteInstances.page.import.sectionTitle')}

{isImportsLoading ? (

{t('settings.remoteInstances.page.import.loading')}

) : importCandidates.length === 0 ? (

{t('settings.remoteInstances.page.import.noneFound')}

) : (
{importCandidates.map((candidate) => (
{candidate.host} {candidate.pattern ? ` ${t('settings.remoteInstances.page.import.patternSuffix')}` : ''}
{candidate.sshCommand}
))}
)}
: null} { if (!open) { closePatternDialog(); } }} > {t('settings.remoteInstances.page.patternDialog.title')} {patternHost ? t('settings.remoteInstances.page.patternDialog.descriptionWithHost', { host: patternHost }) : t('settings.remoteInstances.page.patternDialog.description')}
{ event.preventDefault(); handlePatternCreate(); }} > setPatternDestination(event.target.value)} placeholder={t('settings.remoteInstances.page.patternDialog.destinationPlaceholder')} autoFocus />
); } const isManagedMode = draft.remoteOpenchamber.mode === 'managed'; const instanceTitle = draft.nickname?.trim() || draft.sshParsed?.destination || draft.id; return ( { if (!open) setSelectedId(null); }}>

{instanceTitle}

{t(phaseLabelKey(statusPhase))} {status?.localUrl ? {status.localUrl} : null} {reconnectAppearsStuck ? {t('settings.remoteInstances.page.status.reconnectStale')} : null}

{t('settings.remoteInstances.page.section.actions')}

{t('settings.remoteInstances.page.section.actionsDescription')}

{status?.localUrl ? (
{t('settings.remoteInstances.page.status.currentLocalUrl')} {status.localUrl}
) : null}

{t('settings.remoteInstances.page.section.instance')}

{t('settings.remoteInstances.page.section.instanceDescription')}

{t('settings.remoteInstances.page.field.sshCommand')} updateDraft((current) => ({ ...current, sshCommand: event.target.value, })) } placeholder={t('settings.remoteInstances.page.field.sshCommandPlaceholder')} />
{t('settings.remoteInstances.page.field.nickname')} updateDraft((current) => ({ ...current, nickname: event.target.value, })) } placeholder={t('settings.remoteInstances.page.field.nicknamePlaceholder')} />
{t('settings.remoteInstances.page.field.connectionTimeoutSeconds')} { updateDraft((current) => ({ ...current, connectionTimeoutSec: Number.isFinite(next) ? next : current.connectionTimeoutSec, })); }} />

{t('settings.remoteInstances.page.section.remoteServer')}

{t('settings.remoteInstances.page.section.remoteServerDescription')}

{ updateDraft((current) => ({ ...current, remoteOpenchamber: { ...current.remoteOpenchamber, preferredPort: Number.isFinite(next) && next > 0 ? next : undefined, }, })); }} onClear={() => { updateDraft((current) => ({ ...current, remoteOpenchamber: { ...current.remoteOpenchamber, preferredPort: undefined, }, })); }} emptyLabel={t('settings.remoteInstances.page.field.auto')} />
{isManagedMode ? (
) : null} {isManagedMode ? (
updateDraft((current) => ({ ...current, remoteOpenchamber: { ...current.remoteOpenchamber, keepRunning: checked, }, })) } />
) : null}

{t('settings.remoteInstances.page.section.mainTunnel')}

{t('settings.remoteInstances.page.section.mainTunnelDescription')}

{ updateDraft((current) => ({ ...current, localForward: { ...current.localForward, preferredLocalPort: Number.isFinite(next) && next > 0 ? next : undefined, }, })); }} onClear={() => { updateDraft((current) => ({ ...current, localForward: { ...current.localForward, preferredLocalPort: undefined, }, })); }} emptyLabel={t('settings.remoteInstances.page.field.auto')} />

{t('settings.remoteInstances.page.section.authentication')}

{t('settings.remoteInstances.page.section.authenticationDescription')}

{t('settings.remoteInstances.page.field.sshPasswordOptional')} updateDraft((current) => ({ ...current, auth: { ...current.auth, sshPassword: { enabled: event.target.value.trim().length > 0, value: event.target.value, store: current.auth.sshPassword?.store || 'never', }, }, })) } placeholder={t('settings.remoteInstances.page.field.sshPasswordPlaceholder')} />
{t('settings.remoteInstances.page.field.uiPasswordOptional')} updateDraft((current) => ({ ...current, auth: { ...current.auth, openchamberPassword: { enabled: event.target.value.trim().length > 0, value: event.target.value, store: current.auth.openchamberPassword?.store || 'never', }, }, })) } placeholder={t('settings.remoteInstances.page.field.uiPasswordPlaceholder')} />

{t('settings.remoteInstances.page.section.portForwards')}

{t('settings.remoteInstances.page.section.portForwardsDescription')}

{draft.portForwards.length === 0 ? (

{t('settings.remoteInstances.page.empty.noExtraForwards')}

) : null} {draft.portForwards.map((forward, index) => { const updateForward = (updater: (forward: DesktopSshPortForward) => DesktopSshPortForward) => { updateDraft((current) => ({ ...current, portForwards: current.portForwards.map((item, itemIndex) => itemIndex === index ? updater(item) : item, ), })); }; const localLabel = forward.type === 'remote' ? 'Local target' : 'Local listen'; const localHint = forward.type === 'remote' ? 'Local host and port on your machine that receives traffic from remote -R listener.' : 'Local host and port where this forward listens on your machine.'; const remoteLabel = forward.type === 'remote' ? 'Remote listen' : 'Remote target'; const remoteHint = forward.type === 'remote' ? 'Remote host and port where SSH creates the -R listener.' : 'Remote host and port that receives traffic from local -L listener.'; const localEndpoint = formatEndpoint(forward.localHost || 'localhost', forward.localPort); const remoteEndpoint = formatEndpoint(forward.remoteHost || 'localhost', forward.remotePort); const canOpenLocalEndpoint = forward.type === 'local' && typeof forward.localPort === 'number' && forward.localPort > 0; const localEndpointUrl = canOpenLocalEndpoint ? `http://${toBrowserHost(forward.localHost)}:${forward.localPort}` : ''; const isForwardOpen = Boolean(expandedForwards[forward.id]); const typeLabel = forward.type === 'local' ? t('settings.remoteInstances.page.forwardType.local') : forward.type === 'remote' ? t('settings.remoteInstances.page.forwardType.remote') : t('settings.remoteInstances.page.forwardType.dynamic'); return ( { setExpandedForwards((current) => ({ ...current, [forward.id]: open, })); }} className={`${index > 0 ? 'border-t border-[var(--surface-subtle)]' : ''} py-2`} >
{buildForwardLabel(forward)} {typeLabel}
updateForward((item) => ({ ...item, enabled: checked }))} aria-label={t('settings.remoteInstances.page.actions.enableForwardAria')} />

{t(forwardTypeDescriptionKey(forward.type))}

updateForward((item) => ({ ...item, localHost: event.target.value, })) } placeholder={t('settings.remoteInstances.page.field.localHostPlaceholder')} /> : { updateForward((item) => ({ ...item, localPort: Number.isFinite(next) && next > 0 ? next : undefined, })); }} onClear={() => { updateForward((item) => ({ ...item, localPort: undefined, })); }} emptyLabel={t('settings.remoteInstances.page.field.auto')} />
{forward.type !== 'dynamic' ? (
updateForward((item) => ({ ...item, remoteHost: event.target.value, })) } placeholder={t('settings.remoteInstances.page.field.remoteHostPlaceholder')} /> : { updateForward((item) => ({ ...item, remotePort: Number.isFinite(next) && next > 0 ? next : undefined, })); }} onClear={() => { updateForward((item) => ({ ...item, remotePort: undefined, })); }} emptyLabel={t('settings.remoteInstances.page.field.auto')} />
) : null}
{forward.type === 'dynamic' ? ( <> {localEndpoint} {t('settings.remoteInstances.page.preview.localSocks5')} ) : forward.type === 'remote' ? ( <> {remoteEndpoint} {t('settings.remoteInstances.page.preview.remote')} {localEndpoint} {t('settings.remoteInstances.page.preview.local')} ) : ( <> {localEndpoint} {t('settings.remoteInstances.page.preview.local')} {remoteEndpoint} {t('settings.remoteInstances.page.preview.remote')} )}
{canOpenLocalEndpoint ? ( ) : null}
); })}
{status?.localUrl ? ( <> ) : null} {error ?
{error}
: null}
{t('settings.remoteInstances.page.logsDialog.title')} {draft?.nickname?.trim() || draft?.sshParsed?.destination || draft?.id || t('settings.remoteInstances.page.logsDialog.selectedInstanceFallback')}
{logDialogLoading ? (
{t('settings.remoteInstances.page.logsDialog.loading')}
) : logDialogError ? (
{logDialogError}
) : (
              {logDialogLines.length > 0 ? logDialogLines.join('\n') : t('settings.remoteInstances.page.logsDialog.empty')}
            
)}
{ if (!open) { closePatternDialog(); } }} > {t('settings.remoteInstances.page.patternDialog.title')} {patternHost ? t('settings.remoteInstances.page.patternDialog.descriptionWithHost', { host: patternHost }) : t('settings.remoteInstances.page.patternDialog.description')}
{ event.preventDefault(); handlePatternCreate(); }} > setPatternDestination(event.target.value)} placeholder={t('settings.remoteInstances.page.patternDialog.destinationPlaceholder')} autoFocus />
); };