feat(app-tauri): cycle de vie du serveur embarqué et persistance de l'exposition (#68 B2)

Permet d'activer le serveur web depuis l'app desktop — la fonctionnalité
demandée à l'origine du ticket.

- `embedded_server.rs` : `EmbeddedServerController` porté par `AppState`, avec
  les commandes `embedded_server_start` / `stop` / `status` et
  `get` / `save` / `preview_server_exposure_settings`.
- `FsServerExposureSettingsStore` : écrit `<app-data-dir>/deployment/server-exposure.json`.
  C'est de la config de sécurité, pas une préférence d'UI — d'où l'app-data-dir
  global plutôt que `localStorage`. Écriture atomique : temporaire, `0600` posé
  AVANT le `rename`, donc le fichier final n'est jamais brièvement lisible par
  tous.
- Arrêt du serveur câblé sur la sortie de l'app (`lib.rs`), aux côtés des autres
  nettoyages.

Le serveur embarqué appelle `run_embedded_with_core` avec le `BackendCore`
partagé du desktop, jamais `run_embedded` — c'est tout l'intérêt du seam ouvert
par B1 : une seule composition root dans le processus, pas deux.

DETTE CONNUE, consignée au carnet, non bloquante :
- `preview_settings` valide avant de prévisualiser (`embedded_server.rs:211`),
  ce qui verrouille la liste des candidats : un brouillon en mode
  `remoteProxyOtherMachine` ne peut pas être prévisualisé tant qu'il n'a pas le
  `lanBindAddress` que cette liste doit justement fournir. Contourné côté
  frontend par une sonde `localOnly` toujours valide. Défaut d'ordonnancement,
  pas une fatalité.
- Le warning `missingTrustedProxy` (`embedded_server.rs:452`) est du code mort :
  `validate_settings` rejette les `trustedProxies` vides en mode 3 avant que
  `preview_settings` ne puisse le produire.

QA ré-exécutée par Git HORS SANDBOX (DevBackend et QA sont bloqués par EPERM sur
`bind` ; un vert sandboxé ne prouve rien sur ces crates) : `cargo test -p
app-tauri` 43 passed, 1 ignored · `-p web-server` 66 passed, 0 échec.
Le test ignoré `mcp_bridge::tests::end_to_end_over_real_loopback` est
PRÉEXISTANT sur `develop` et non touché par ce lot ; lancé explicitement hors
sandbox avec `--ignored`, il passe. C'est une garde d'environnement, pas un faux
vert — distinction qui nous a déjà coûté deux fois aujourd'hui.

Invariants vérifiés par Git : appelant `run_embedded_with_core` confirmé, aucune
fuite d'`EmbeddedServer*`/`ServerExposure*` dans `domain` ni `application`,
ordre chmod/rename correct, `stop()` bien appelé à la fermeture.

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

View File

@ -61,6 +61,9 @@ use crate::dto::{
UpdateProjectContextRequestDto, UpdateProjectPermissionsRequestDto, UpdateSkillRequestDto,
UpdateTemplateRequestDto, WriteTerminalRequestDto,
};
use crate::embedded_server::{
EmbeddedServerStatusDto, ServerExposurePreviewDto, ServerExposureSettingsDto,
};
use crate::pty::{PtyBridge, PtyChunk};
use crate::state::{AppState, FocusedProjectDto};
use domain::{SkillRef, SkillScope};
@ -83,6 +86,69 @@ pub fn health(
.map_err(ErrorDto::from)
}
/// `get_server_exposure_settings` — read persisted embedded-server exposure settings.
///
/// # Errors
/// Returns an [`ErrorDto`] when persisted settings are invalid.
#[tauri::command]
pub fn get_server_exposure_settings(
state: State<'_, AppState>,
) -> Result<ServerExposureSettingsDto, ErrorDto> {
state.embedded_server.get_settings()
}
/// `save_server_exposure_settings` — validate and persist embedded-server exposure settings.
///
/// # Errors
/// Returns an [`ErrorDto`] when settings are invalid or cannot be written.
#[tauri::command]
pub fn save_server_exposure_settings(
settings: ServerExposureSettingsDto,
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
state.embedded_server.save_settings(settings)
}
/// `preview_server_exposure_settings` — derive LAN candidates and upstream URL.
///
/// # Errors
/// Returns an [`ErrorDto`] when settings are invalid.
#[tauri::command]
pub fn preview_server_exposure_settings(
settings: ServerExposureSettingsDto,
state: State<'_, AppState>,
) -> Result<ServerExposurePreviewDto, ErrorDto> {
state.embedded_server.preview(settings)
}
/// `embedded_server_status` — return the current embedded server status.
#[tauri::command]
pub fn embedded_server_status(state: State<'_, AppState>) -> EmbeddedServerStatusDto {
state.embedded_server.status()
}
/// `embedded_server_start` — start the embedded server with the shared backend core.
///
/// # Errors
/// Returns an [`ErrorDto`] when settings are invalid or the listener cannot start.
#[tauri::command]
pub async fn embedded_server_start(
state: State<'_, AppState>,
) -> Result<EmbeddedServerStatusDto, ErrorDto> {
state.embedded_server.start(state.core()).await
}
/// `embedded_server_stop` — stop the embedded server.
///
/// # Errors
/// Returns an [`ErrorDto`] when stopping the listener fails.
#[tauri::command]
pub async fn embedded_server_stop(
state: State<'_, AppState>,
) -> Result<EmbeddedServerStatusDto, ErrorDto> {
state.embedded_server.stop().await
}
/// `create_project` — create a project from a root: init `.ideai/`, register it.
///
/// # Errors