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 { serverSettings, setListeningMode } from "../stores/preferences" import { showConfirmDialog } from "../stores/alerts" import { getLogger } from "../lib/logger" import { useI18n } from "../lib/i18n" const log = getLogger("actions") interface RemoteAccessOverlayProps { open: boolean onClose: () => void } export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) { const { t } = useI18n() const [meta, setMeta] = createSignal(null) const [authStatus, setAuthStatus] = createSignal<{ authenticated: boolean; username?: string; passwordUserProvided?: boolean } | null>(null) const [loading, setLoading] = createSignal(false) const [applyingListeningMode, setApplyingListeningMode] = createSignal(false) const [qrCodes, setQrCodes] = createSignal>({}) const [expandedUrl, setExpandedUrl] = createSignal(null) const [error, setError] = createSignal(null) const [passwordFormOpen, setPasswordFormOpen] = createSignal(false) const [passwordValue, setPasswordValue] = createSignal("") const [passwordConfirm, setPasswordConfirm] = createSignal("") const [passwordError, setPasswordError] = createSignal(null) const [savingPassword, setSavingPassword] = createSignal(false) const addresses = createMemo(() => meta()?.addresses ?? []) const currentMode = createMemo(() => meta()?.listeningMode ?? serverSettings().listeningMode) const allowExternalConnections = createMemo(() => currentMode() === "all") const displayAddresses = createMemo(() => { const list = addresses() if (!allowExternalConnections()) { return [] } // Local URL is displayed separately; list only remote-friendly addresses. return list.filter((address) => address.scope !== "loopback") }) const refreshMeta = async () => { setLoading(true) setError(null) setPasswordError(null) try { const [metaResult, authResult] = await Promise.all([serverApi.fetchServerMeta(), serverApi.fetchAuthStatus()]) setMeta(metaResult) setAuthStatus(authResult) } 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) { log.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 } if (applyingListeningMode()) { return } const confirmed = await showConfirmDialog(t("remoteAccess.listeningMode.restartConfirm.message"), { title: allow ? t("remoteAccess.listeningMode.restartConfirm.title.all") : t("remoteAccess.listeningMode.restartConfirm.title.local"), variant: "warning", confirmLabel: t("remoteAccess.listeningMode.restartConfirm.confirmLabel"), cancelLabel: t("remoteAccess.listeningMode.restartConfirm.cancelLabel"), }) if (!confirmed) { // Switch will revert automatically since `checked` is derived from store state return } setApplyingListeningMode(true) setError(null) try { // Important: await the config patch before restart so Electron reads the updated mode from disk. await setListeningMode(targetMode) const restarted = await restartCli() if (!restarted) { setError(t("remoteAccess.restart.errorManual")) } else { setMeta((prev) => (prev ? { ...prev, listeningMode: targetMode } : prev)) } } catch (err) { setError(err instanceof Error ? err.message : String(err)) } finally { setApplyingListeningMode(false) } void refreshMeta() } const handleOpenUrl = (url: string) => { try { window.open(url, "_blank", "noopener,noreferrer") } catch (err) { log.error("Failed to open URL", err) } } const handleSubmitPassword = async () => { setPasswordError(null) const next = passwordValue() const confirm = passwordConfirm() if (next.trim().length < 8) { setPasswordError(t("remoteAccess.password.error.tooShort")) return } if (next !== confirm) { setPasswordError(t("remoteAccess.password.error.mismatch")) return } setSavingPassword(true) try { const result = await serverApi.setServerPassword(next) setAuthStatus({ authenticated: true, username: result.username, passwordUserProvided: result.passwordUserProvided }) setPasswordValue("") setPasswordConfirm("") setPasswordFormOpen(false) } catch (err) { setPasswordError(err instanceof Error ? err.message : String(err)) } finally { setSavingPassword(false) } } return ( { if (!nextOpen) { props.onClose() } }} >

{t("remoteAccess.eyebrow")}

{t("remoteAccess.title")}

{t("remoteAccess.subtitle")}

{t("remoteAccess.sections.listeningMode.label")}

{t("remoteAccess.sections.listeningMode.help")}

{ void handleAllowConnectionsChange(nextChecked) }} disabled={loading() || applyingListeningMode()} > {allowExternalConnections() ? t("remoteAccess.toggle.on") : t("remoteAccess.toggle.off")}
{t("remoteAccess.toggle.title")} {allowExternalConnections() ? t("remoteAccess.toggle.caption.all") : t("remoteAccess.toggle.caption.local")}

{t("remoteAccess.toggle.note")}

{t("remoteAccess.sections.serverPassword.label")}

{t("remoteAccess.sections.serverPassword.help")}

{t("remoteAccess.authStatus.unavailable")}
} >

{t("remoteAccess.username", { username: authStatus()!.username ?? "codenomad" })}

{authStatus()!.passwordUserProvided ? t("remoteAccess.password.status.set") : t("remoteAccess.password.status.unset")}

setPasswordValue(event.currentTarget.value)} placeholder={t("remoteAccess.password.form.placeholder")} />
setPasswordConfirm(event.currentTarget.value)} />
{(message) =>
{message()}
}

{t("remoteAccess.sections.addresses.label")}

{t("remoteAccess.sections.addresses.help")}

{t("remoteAccess.addresses.loading")}
}> {error()}}> 0} fallback={
{t("remoteAccess.addresses.none")}
}>
{(url) => { const value = () => url() const expandedState = () => expandedUrl() === value() const qr = () => qrCodes()[value()] return (

{value()}

{t("remoteAccess.address.scope.loopback")}

) }}
{(address) => { const url = address.remoteUrl const expandedState = () => expandedUrl() === url const qr = () => qrCodes()[url] const scopeLabel = () => address.scope === "external" ? t("remoteAccess.address.scope.network") : address.scope === "loopback" ? t("remoteAccess.address.scope.loopback") : t("remoteAccess.address.scope.internal") return (

{url}

{address.family.toUpperCase()} • {scopeLabel()} • {address.ip}

) }}
) }