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

@ -117,6 +117,97 @@ export interface ModelServerCommandPreview {
display: string;
}
/**
* How the embedded web server is exposed (ticket #68, mirror of the backend
* `ServerExposureMode`).
*
* - `localOnly` — binds loopback, no remote access.
* - `remoteProxyLocal` — binds loopback; an HTTPS reverse proxy runs on *this*
* machine and reaches IdeA over loopback.
* - `remoteProxyOtherMachine` — binds a concrete LAN address so a proxy on
* *another* host can reach it; that proxy must be declared in
* `trustedProxies`.
*/
export type ServerExposureMode =
| "localOnly"
| "remoteProxyLocal"
| "remoteProxyOtherMachine";
/**
* Persisted embedded-server exposure settings (mirror of the backend
* `ServerExposureSettingsDto`).
*
* The backend validates every field on save/start; the UI never derives network
* facts (LAN addresses, upstream URL) from these — it asks `previewExposure`.
*/
export interface ServerExposureSettings {
mode: ServerExposureMode;
/** TCP port to bind. `0` asks the OS for an ephemeral port. */
port: number;
/** Public HTTPS origin, required by both remote modes. */
publicOrigin?: string;
/**
* Peers allowed to contact IdeA, as IPs or CIDRs. **Not** a listen address.
* Required (non-empty) by `remoteProxyOtherMachine`.
*/
trustedProxies: string[];
/**
* Concrete LAN address to bind, required by `remoteProxyOtherMachine`. Must
* come from {@link ServerExposurePreview.candidateLanAddresses} — the UI is
* never allowed to invent one.
*/
lanBindAddress?: string;
}
/** A non-fatal diagnostic from the exposure preview (never blocks a start). */
export interface DiagnosticWarning {
/** Stable warning code (e.g. `missingTrustedProxy`). */
code: string;
/** Human-readable, actionable message. */
message: string;
}
/**
* Backend-derived preview of a draft exposure config (mirror of
* `ServerExposurePreviewDto`). The backend is the sole authority on LAN
* addresses and the proxy upstream URL.
*/
export interface ServerExposurePreview {
/** LAN addresses the backend discovered on this host. */
candidateLanAddresses: string[];
/** URL the reverse proxy should target; absent in `localOnly`. */
upstreamUrl?: string;
/** Non-fatal diagnostics to surface alongside the form. */
warnings: DiagnosticWarning[];
}
/** Embedded-server lifecycle state (mirror of `EmbeddedServerStatusStateDto`). */
export type EmbeddedServerState =
| "stopped"
| "starting"
| "running"
| "stopping"
| "failed";
/**
* Embedded-server status (mirror of `EmbeddedServerStatusDto`). `pairingCode`
* is a runtime-only secret: it exists while the server runs, is never
* persisted, and must never be written into a settings field.
*/
export interface EmbeddedServerStatus {
state: EmbeddedServerState;
/** Local URL, when running. */
localUrl?: string;
/** Public URL, when a remote mode is configured. */
publicUrl?: string;
/** Upstream URL to hand to the reverse proxy. */
upstreamUrl?: string;
/** Runtime pairing code — present only while running. */
pairingCode?: string;
/** Last failure, when `state` is `failed`. */
error?: GatewayError;
}
/** A domain event relayed from the backend (tagged union on `type`). */
export type DomainEvent =
| { type: "projectCreated"; projectId: string }