/** * `Settings → Deployment` (ticket #68) — turn IdeA Desktop into a server other * devices can reach, without a command line. * * Pure presentation over {@link useDeployment}; no `invoke()`, and no address is * ever derived here — LAN candidates and the upstream URL come from the backend * preview (`DesktopServerGateway`). * * Two UX invariants this screen exists to protect: * * - **The exposure mode is a decision, not a setting.** It is rendered as radio * *cards* with plain-language consequences, never a technical select, because * choosing wrong (proxy elsewhere, mode "on this computer") fails as a silent * proxy timeout with no IdeA error to read. * - **The authorized-proxy field is not a listen address.** That confusion is * the whole reason this screen is worded the way it is; the help text under * the field says so explicitly. * * The pairing code is runtime-only: shown while running, never rendered into a * persisted field, never mixed with the upstream value. */ import { useState } from "react"; import type { ServerExposureMode } from "@/domain"; import { Button, Field, Input, Panel, cn } from "@/shared"; import { useDeployment } from "./useDeployment"; interface ModeOption { mode: ServerExposureMode; title: string; description: string; } /** The three exposure choices, in increasing order of reach. */ const MODE_OPTIONS: ModeOption[] = [ { mode: "localOnly", title: "This computer only", description: "For using IdeA on this desktop only. Remote devices cannot connect.", }, { mode: "remoteProxyLocal", title: "Remote access, proxy on this computer", description: "Use this when your HTTPS proxy runs on the same machine as IdeA Desktop.", }, { mode: "remoteProxyOtherMachine", title: "Remote access, proxy on another machine", description: "Use this when the HTTPS proxy runs on another machine. IdeA will only accept traffic from that proxy.", }, ]; const STATE_LABEL: Record = { stopped: "Stopped", starting: "Starting…", running: "Running", stopping: "Stopping…", failed: "Failed", }; /** Copy-to-clipboard button; degrades to disabled where the API is absent. */ function CopyButton({ value, label }: { value: string; label: string }) { const [copied, setCopied] = useState(false); const supported = typeof navigator !== "undefined" && Boolean(navigator.clipboard); return ( ); } /** A read-only value the user is meant to copy elsewhere (never editable). */ function ReadOnlyValue({ value, copyLabel }: { value: string; copyLabel: string }) { return (
{value}
); } export function DeploymentSettings() { const vm = useDeployment(); if (!vm.ready || !vm.settings) { return (

Loading…

); } const { settings, status } = vm; const running = status.state === "running"; const remote = settings.mode !== "localOnly"; const otherMachine = settings.mode === "remoteProxyOtherMachine"; const transitioning = status.state === "starting" || status.state === "stopping"; return (
{/* ── Status ────────────────────────────────────────────────────────── */} void vm.stop()} disabled={vm.busy || transitioning} > Stop ) : ( ) } >

{STATE_LABEL[status.state]}

{status.localUrl && (

Local URL: {status.localUrl}

)} {status.publicUrl && (

Public URL: {status.publicUrl}

)} {/* A failure carries the backend's message: it is the correction. */} {status.state === "failed" && status.error && (

{status.error.message}

)} {vm.actionError && (

{vm.actionError}

)}
{/* ── Exposure ──────────────────────────────────────────────────────── */}
{MODE_OPTIONS.map((option) => { const selected = settings.mode === option.mode; return ( ); })}
{({ id }) => ( vm.setPort(Number(e.target.value))} /> )} {remote && ( {({ id, describedBy }) => ( vm.setPublicOrigin(e.target.value)} /> )} )} {otherMachine && ( <> {/* Addresses come from the backend probe — never invented here. */} {({ id, describedBy }) => vm.candidateLanAddresses.length > 0 ? ( ) : ( vm.setLanBindAddress(e.target.value)} /> ) } {({ id, describedBy }) => ( vm.setTrustedProxies(e.target.value)} /> )}

If your proxy is not on this computer, choose this mode. Otherwise the proxy may time out without an IdeA error.

)} {/* Warnings inform; they never block a start. */} {vm.warnings.map((warning) => (

{warning.message}

))} {vm.validationError && (

{vm.validationError}

)}
{/* ── Proxy setup: the upstream to paste, backend-provided ──────────── */} {remote && (

Point your HTTPS reverse proxy at this upstream.

{vm.upstreamUrl ? ( ) : (

Complete the settings above to get the upstream URL.

)}
)} {/* ── Pairing: runtime-only secret, isolated from the upstream ──────── */} {running && status.pairingCode ? (

Temporary code. It disappears when the server stops. Do not save it in configuration files.

) : (

Start the server to generate a pairing code.

)}
); }