import { Dialog } from "@kobalte/core/dialog" import { Switch } from "@kobalte/core/switch" import { For, Show, createEffect, createMemo, createSignal } from "solid-js" import { toDataURL } from "qrcode" import { ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid" import type { NetworkAddress, ServerMeta } from "../../../server/src/api-types" import { serverApi } from "../lib/api-client" import { restartCli } from "../lib/native/cli" import { preferences, setListeningMode } from "../stores/preferences" import { showConfirmDialog } from "../stores/alerts" interface RemoteAccessOverlayProps { open: boolean onClose: () => void } export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) { const [meta, setMeta] = createSignal(null) const [loading, setLoading] = createSignal(false) const [qrCodes, setQrCodes] = createSignal>({}) const [expandedUrl, setExpandedUrl] = createSignal(null) const [error, setError] = createSignal(null) const addresses = createMemo(() => meta()?.addresses ?? []) const currentMode = createMemo(() => meta()?.listeningMode ?? preferences().listeningMode) const allowExternalConnections = createMemo(() => currentMode() === "all") const refreshMeta = async () => { setLoading(true) setError(null) try { const result = await serverApi.fetchServerMeta() setMeta(result) } catch (err) { setError(err instanceof Error ? err.message : String(err)) } finally { setLoading(false) } } createEffect(() => { if (props.open) { void refreshMeta() } }) const toggleExpanded = async (url: string) => { if (expandedUrl() === url) { setExpandedUrl(null) return } setExpandedUrl(url) if (!qrCodes()[url]) { try { const dataUrl = await toDataURL(url, { margin: 1, scale: 4 }) setQrCodes((prev) => ({ ...prev, [url]: dataUrl })) } catch (err) { console.error("Failed to generate QR code", err) } } } const handleAllowConnectionsChange = async (checked: boolean) => { const allow = Boolean(checked) const targetMode: "local" | "all" = allow ? "all" : "local" if (targetMode === currentMode()) { return } const confirmed = await showConfirmDialog("Restart to apply listening mode? This will stop all running instances.", { title: allow ? "Open to other devices" : "Limit to this device", variant: "warning", confirmLabel: "Restart now", cancelLabel: "Cancel", }) if (!confirmed) { // Switch will revert automatically since `checked` is derived from store state return } setListeningMode(targetMode) const restarted = await restartCli() if (!restarted) { setError("Unable to restart automatically. Please restart the app to apply the change.") } else { setMeta((prev) => (prev ? { ...prev, listeningMode: targetMode } : prev)) } void refreshMeta() } const handleOpenUrl = (url: string) => { try { window.open(url, "_blank", "noopener,noreferrer") } catch (err) { console.error("Failed to open URL", err) } } return ( { if (!nextOpen) { props.onClose() } }} >

Remote handover

Connect to this session remotely

Use the addresses below to open CodeNomad from another device.

Listening mode

Allow or limit remote handovers by binding to all interfaces or just localhost.

{ void handleAllowConnectionsChange(nextChecked) }} > {allowExternalConnections() ? "On" : "Off"}
Allow connections from other IPs {allowExternalConnections() ? "Binding to 0.0.0.0" : "Binding to 127.0.0.1"}

Changing this requires a restart and temporarily stops all active instances. Share the addresses below once the server restarts.

Reachable addresses

Launch or scan from another machine to hand over control.

Loading addresses…
}> {error()}
}> 0} fallback={
No addresses available yet.
}>
{(address) => { const expandedState = () => expandedUrl() === address.url const qr = () => qrCodes()[address.url] return (

{address.url}

{address.family.toUpperCase()} • {address.scope === "external" ? "Network" : address.scope === "loopback" ? "Loopback" : "Internal"} • {address.ip}

) }}
) }