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

@ -56,17 +56,29 @@ export interface DeploymentVm {
warnings: DiagnosticWarning[];
/** Why the draft is rejected, as told by the backend. Actionable, not decorative. */
validationError: string | null;
/** Last save/start/stop failure. */
/** Last save/start/stop/auto-start-toggle failure. */
actionError: string | null;
/** True while a start/stop is in flight. */
busy: boolean;
/** True while the auto-start toggle's own save is in flight (#89). */
autoStartBusy: boolean;
setMode: (mode: ServerExposureMode) => void;
setPublicOrigin: (origin: string) => void;
setLanBindAddress: (address: string) => void;
setTrustedProxies: (raw: string) => void;
setPort: (port: number) => void;
/**
* Persists `autoStart` immediately (ticket #89) — never calls `start()`.
* Unlike the other setters, this does not update the draft optimistically:
* the local `settings.autoStart` only flips once the save has actually
* succeeded, so a rejected save (the same validations as `start`/`save`
* apply — e.g. a remote mode still missing its public origin or LAN/proxy)
* leaves the checkbox showing the persisted value, not a lie.
*/
setAutoStart: (enabled: boolean) => Promise<void>;
/** Persists the draft, then starts the server so what runs is what is shown. */
start: () => Promise<void>;
/** Stops the running server. Never touches the persisted `autoStart` flag. */
stop: () => Promise<void>;
}
@ -82,6 +94,7 @@ export function useDeployment(): DeploymentVm {
const [validationError, setValidationError] = useState<string | null>(null);
const [actionError, setActionError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [autoStartBusy, setAutoStartBusy] = useState(false);
// Guards against a stale in-flight preview overwriting a newer one.
const previewSeq = useRef(0);
@ -112,6 +125,7 @@ export function useDeployment(): DeploymentVm {
try {
const preview = await desktopServer.previewExposure({
mode: "localOnly",
autoStart: false,
port: 0,
trustedProxies: [],
});
@ -204,6 +218,29 @@ export function useDeployment(): DeploymentVm {
);
const setPort = useCallback((port: number) => patch({ port }), [patch]);
const setAutoStart = useCallback(
async (enabled: boolean) => {
if (!settings) return;
const next = { ...settings, autoStart: enabled };
setAutoStartBusy(true);
setActionError(null);
try {
// Persist only — never `start()`. Auto-start is applied at the next
// app launch, not now.
await desktopServer.saveExposureSettings(next);
// Update the draft only after the save actually succeeded, so a
// rejected save (e.g. a remote mode still missing its public origin)
// never shows a checkbox state that was not actually persisted.
setSettings(next);
} catch (e) {
setActionError(messageOf(e));
} finally {
setAutoStartBusy(false);
}
},
[desktopServer, settings],
);
const start = useCallback(async () => {
if (!settings) return;
setBusy(true);
@ -242,11 +279,13 @@ export function useDeployment(): DeploymentVm {
validationError,
actionError,
busy,
autoStartBusy,
setMode,
setPublicOrigin,
setLanBindAddress,
setTrustedProxies,
setPort,
setAutoStart,
start,
stop,
};