/** * `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"; import { useI18n } from "@/i18n"; type Tr = (french: string, english: string) => string; interface ModeOption { mode: ServerExposureMode; title: string; description: string; } /** The three exposure choices, in increasing order of reach. */ const modeOptions = (tr: Tr): ModeOption[] => [ { mode: "localOnly", title: tr("Cet ordinateur uniquement", "This computer only"), description: tr("Pour utiliser IdeA uniquement sur cet ordinateur. Les appareils distants ne peuvent pas se connecter.", "Use IdeA only on this computer. Remote devices cannot connect."), }, { mode: "remoteProxyLocal", title: tr("Accès distant, proxy sur cet ordinateur", "Remote access, proxy on this computer"), description: tr("À utiliser quand le proxy HTTPS tourne sur la même machine qu'IdeA Desktop.", "Use when the HTTPS proxy runs on the same machine as IdeA Desktop."), }, { mode: "remoteProxyOtherMachine", title: tr("Accès distant, proxy sur une autre machine", "Remote access, proxy on another machine"), description: tr("À utiliser quand le proxy HTTPS tourne sur une autre machine. IdeA n'acceptera que le trafic provenant de ce proxy.", "Use when the HTTPS proxy runs on another machine. IdeA will only accept traffic from that proxy."), }, ]; const stateLabel = (state: string, tr: Tr): string => ({ stopped: tr("Arrêté", "Stopped"), starting: tr("Démarrage…", "Starting…"), running: tr("En cours d'exécution", "Running"), stopping: tr("Arrêt…", "Stopping…"), failed: tr("Échec", "Failed"), }[state] ?? state); /** Copy-to-clipboard button; degrades to disabled where the API is absent. */ function CopyButton({ value, label }: { value: string; label: string }) { const { tr } = useI18n(); 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}
{tr("Chargement…", "Loading…")}
{stateLabel(status.state, tr)}
{status.localUrl && (
{tr("URL locale", "Local URL")} : {status.localUrl}
{tr("URL publique", "Public URL")} : {status.publicUrl}
{tr("Le serveur n'a pas démarré automatiquement.", "The server did not start automatically.")} {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. */}{tr("Si votre proxy n'est pas sur cet ordinateur, choisissez ce mode. Sinon, le proxy peut expirer sans erreur IdeA.", "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}
)}{tr("Faites pointer votre proxy inverse HTTPS vers cet upstream.", "Point your HTTPS reverse proxy to this upstream.")}
{vm.upstreamUrl ? ({tr("Complétez les paramètres ci-dessus pour obtenir l'URL upstream.", "Complete the settings above to obtain the upstream URL.")}
)}{tr("L'appairage est géré dans", "Pairing is managed in")}{" "} {tr("Paramètres → Appareils", "Settings → Devices")}, {tr("où vous pouvez générer un code et révoquer des appareils.", "where you can generate a code and revoke devices.")}