/** * `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. * * This screen no longer shows a pairing code (#77). A code is not a property of * a running server — it exists only when someone asks for one — so it lives in * the Appareils surface next to the devices it authorises. What is left here is * a signpost: starting the server from this screen and finding no way to pair is * a dead end. */ import { useRef, 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: "Cet ordinateur uniquement", description: "Pour utiliser IdeA uniquement sur cet ordinateur. Les appareils distants ne peuvent pas se connecter.", }, { mode: "remoteProxyLocal", title: "Accès distant, proxy sur cet ordinateur", description: "À utiliser quand le proxy HTTPS tourne sur la même machine qu'IdeA Desktop.", }, { mode: "remoteProxyOtherMachine", title: "Accès distant, proxy sur une autre machine", description: "À utiliser quand le proxy HTTPS tourne sur une autre machine. IdeA n'acceptera que le trafic provenant de ce proxy.", }, ]; const STATE_LABEL: Record = { stopped: "Arrêté", starting: "Démarrage…", running: "En cours d'exécution", stopping: "Arrêt…", failed: "Échec", }; /** 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(); // "Modifier le port" (#89) jumps into Accès réseau — imperative focus is the // simplest correct answer for a cross-panel affordance on one scrolled page. const portInputRef = useRef(null); if (!vm.ready || !vm.settings) { return (

Chargement…

); } 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} > Arrêter ) : ( ) } >

{STATE_LABEL[status.state]}

{status.localUrl && (

URL locale : {status.localUrl}

)} {status.publicUrl && (

URL publique : {status.publicUrl}

)} {/* A failure carries the backend's message: it is the correction. A port conflict (#89 — most often surfaced by auto-start at launch, but shown the same way for a manual start) gets a dedicated, actionable message instead of the generic one. */} {status.state === "failed" && status.error && ( status.error.code === "PORT_IN_USE" ? (

Le serveur n'a pas démarré automatiquement. {status.error.message}

) : (

{status.error.message}

) )} {vm.actionError && (

{vm.actionError}

)} {/* Auto-start at launch (#89) — a persistence-only toggle, never a start trigger; it never runs the server right now. */}
{/* ── Exposure (ticket #78 — "Accès réseau" per UX) ────────────────── */}
{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)} /> )}

Si votre proxy n'est pas sur cet ordinateur, choisissez ce mode. Sinon, le proxy peut expirer sans erreur IdeA.

)} {/* 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 && (

Faites pointer votre proxy inverse HTTPS vers cet upstream.

{vm.upstreamUrl ? ( ) : (

Complétez les paramètres ci-dessus pour obtenir l'URL upstream.

)}
)} {/* ── Pairing moved to its own surface (#77) — leave a signpost ─────── */}

L'appairage est géré dans{" "} Paramètres → Appareils, où vous pouvez générer un code et révoquer des appareils.

); }