feat(frontend): démarrage auto du serveur + erreur port occupé actionnable (#89)

Étend ServerExposureSettings avec autoStart (défaut false, mock aligné sur le
backend). useDeployment expose setAutoStart, qui persiste immédiatement le
draft sans jamais appeler start() ni toucher le comportement de stop(), et ne
met à jour l'état local qu'après confirmation de la sauvegarde (même piste de
validation que start/save). DeploymentSettings ajoute la case à cocher sous
le panneau Serveur, et des actions « Modifier le port »/« Réessayer » quand le
statut échoue avec le code PORT_IN_USE stabilisé côté backend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 18:38:40 +02:00
parent cb2d0c2d44
commit e9b01795d1
6 changed files with 273 additions and 8 deletions

View File

@ -23,7 +23,7 @@
* a dead end.
*/
import { useState } from "react";
import { useRef, useState } from "react";
import type { ServerExposureMode } from "@/domain";
import { Button, Field, Input, Panel, cn } from "@/shared";
@ -105,6 +105,9 @@ function ReadOnlyValue({ value, copyLabel }: { value: string; copyLabel: string
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<HTMLInputElement>(null);
if (!vm.ready || !vm.settings) {
return (
@ -170,17 +173,72 @@ export function DeploymentSettings() {
</p>
)}
{/* A failure carries the backend's message: it is the correction. */}
{/* 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 && (
<p role="alert" className="text-sm text-danger">
{status.error.message}
</p>
status.error.code === "PORT_IN_USE" ? (
<div className="flex flex-col gap-2">
<p role="alert" className="text-sm text-danger">
Le serveur n'a pas démarré automatiquement. {status.error.message}
</p>
<div className="flex gap-2">
<Button
size="sm"
variant="ghost"
onClick={() => {
portInputRef.current?.scrollIntoView?.({
behavior: "smooth",
block: "center",
});
portInputRef.current?.focus();
}}
>
Modifier le port
</Button>
<Button
size="sm"
onClick={() => void vm.start()}
disabled={vm.busy}
>
Réessayer
</Button>
</div>
</div>
) : (
<p role="alert" className="text-sm text-danger">
{status.error.message}
</p>
)
)}
{vm.actionError && (
<p role="alert" className="text-sm text-danger">
{vm.actionError}
</p>
)}
{/* Auto-start at launch (#89) — a persistence-only toggle, never a
start trigger; it never runs the server right now. */}
<label className="flex cursor-pointer items-start gap-2 pt-2">
<input
type="checkbox"
className="mt-0.5"
aria-label="Démarrer le serveur au lancement d'IdeA"
checked={settings.autoStart}
disabled={vm.autoStartBusy}
onChange={(e) => void vm.setAutoStart(e.target.checked)}
/>
<span className="flex flex-col gap-0.5">
<span className="text-sm text-content">
Démarrer le serveur au lancement d'IdeA
</span>
<span className="text-xs text-muted">
IdeA utilisera les réglages réseau enregistrés ci-dessous au
prochain démarrage de l'application desktop.
</span>
</span>
</label>
</div>
</Panel>
@ -225,6 +283,7 @@ export function DeploymentSettings() {
<Field label="Port">
{({ id }) => (
<Input
ref={portInputRef}
id={id}
type="number"
value={String(settings.port)}