feat(frontend): panneau Settings Deployment et gateway serveur desktop (#68 F1+F2)

Donne à l'utilisateur la surface pour activer le serveur depuis l'app.

- `features/settings/` : `SettingsView`, `DeploymentSettings`, `useDeployment`.
- `adapters/desktopServer.ts` + port `DesktopServerGateway` et DTO associés ;
  adapters mock et http/unsupported alignés (le mode web n'expose pas le
  contrôle du serveur qui l'héberge).
- `ProjectsView` : le `showSettings: boolean` devient une navigation interne
  `AI Profiles` / `Deployment`. Le libellé alternant « Close AI Profiles »
  disparaît — Settings existait déjà dans cette vue, la surface évolue au lieu
  d'ajouter un `PanelId`.

CORRECTION D'UN BRIEF FAUX, remontée spontanément par DevFrontend et qui mérite
de survivre : le cadrage décrivait le mode `remoteProxyOtherMachine` avec deux
champs. `validate_settings` en exige un troisième, `lanBindAddress`, et rejette
loopback comme unspecified. Construit selon la spec, chaque save et chaque start
en mode 3 aurait échoué — le lot serait parti vert et cassé. Le panneau expose
donc un select alimenté par `candidateLanAddresses` fourni par le backend :
la règle « le frontend n'invente jamais une IP » tient.

QA ré-exécutée par Git avant merge : `npm run typecheck` exit 0 ·
`npx vitest run` 87 files / 789 passed, 0 échec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 23:43:37 +02:00
parent 49e1d5a157
commit 7efa634f23
16 changed files with 1458 additions and 31 deletions

View File

@ -9,8 +9,15 @@
* browser to replace `pickFolder`), it gets its own lot.
*/
import type { GatewayError, Unsubscribe } from "@/domain";
import type {
EmbeddedServerStatus,
GatewayError,
ServerExposurePreview,
ServerExposureSettings,
Unsubscribe,
} from "@/domain";
import type {
DesktopServerGateway,
FocusedProject,
FocusedProjectGateway,
RemoteGateway,
@ -75,3 +82,40 @@ export class WebRemoteGateway implements RemoteGateway {
return unsupportedOnWeb("Remote (SSH/WSL) connection");
}
}
/**
* Web stub: the embedded server is desktop-only (ticket #68). A web client is
* *served by* this very server, so letting it reconfigure or stop the server
* would let a remote device saw off the branch it sits on. The desktop app owns
* this surface; every call fails explicitly rather than reaching for Tauri.
*/
export class WebDesktopServerGateway implements DesktopServerGateway {
// `async` on purpose: these must *reject* rather than throw synchronously, so
// callers using `.then(ok, err)` (not just `await`) still see the failure.
async getExposureSettings(): Promise<ServerExposureSettings> {
return unsupportedOnWeb("Embedded server settings");
}
async saveExposureSettings(_settings: ServerExposureSettings): Promise<void> {
return unsupportedOnWeb("Embedded server settings");
}
async previewExposure(
_settings: ServerExposureSettings,
): Promise<ServerExposurePreview> {
return unsupportedOnWeb("Embedded server settings");
}
async status(): Promise<EmbeddedServerStatus> {
return unsupportedOnWeb("Embedded server status");
}
async start(): Promise<EmbeddedServerStatus> {
return unsupportedOnWeb("Starting the embedded server");
}
async stop(): Promise<EmbeddedServerStatus> {
return unsupportedOnWeb("Stopping the embedded server");
}
onStatusChanged(
_handler: (status: EmbeddedServerStatus) => void,
): Promise<Unsubscribe> {
// Never fires on web; a no-op unsubscribe keeps callers' teardown honest.
return Promise.resolve(() => {});
}
}