Dernier état `.ideai/` du sprint serveur embarqué, séparé du code applicatif conformément aux précédents `f965f64` / `7fbaa8e` / `5ee25d1`. - #68 passé en `qa`, pas en `closed` : mergé dans `develop` (`b30c9c7`) et vert, mais RIEN n'a été cliqué dans l'app réelle. « Mergé » ne veut pas dire « validé » — même distinction que pour #69 et son lot 3. - Carnet de #68 : périmètre livré, réserve « aucune validation live », et la dette identifiée (ordre de `preview_settings` qui verrouille la liste des candidats LAN, warning `missingTrustedProxy` inatteignable). - Mémoire projet : note `sandbox-eperm-bind-false-green-web-server`. Elle capitalise le piège qui s'est refermé DEUX FOIS le 2026-07-16 : les sandboxes de DevBackend et QA refusent `TcpListener::bind`, donc tout `cargo test` sur `web-server`/`app-tauri` y rend un vert qui ne prouve rien. La première fois, ce faux vert masquait un vrai bug produit — un serveur embarqué sur port éphémère rejetait toutes les requêtes API en 403. La règle qui en sort : aucun repli EPERM silencieux, et tout vert sur ces crates doit être produit hors sandbox. - Journal des tâches de fond : complétions enregistrées. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.0 KiB
issueRef, version, updatedBy, updatedAt
| issueRef | version | updatedBy | updatedAt | ||||
|---|---|---|---|---|---|---|---|
| #68 | 7 |
|
1784238387151 |
Ticket #68 — Serveur embarqué activable depuis le desktop : carnet de chantier
Statut : livré et mergé dans develop (f965f64) le 2026-07-16, en exécution autonome de nuit.
Reste à faire : la validation live utilisateur. Rien n'a été cliqué dans l'app réelle — voir la réserve plus bas.
Livraison en deux temps
B1 — seam shared-core (mergé plus tôt, b7d38f1 + 328c941). web_server::run_embedded_with_core(config, Arc<BackendCore>). Sans lui, le desktop aurait construit une seconde composition root dans le même processus — deux event bus, deux registres live, deux jeux de stores — et le conflit d'écriture app-data-dir serait réapparu à l'intérieur d'un seul process.
B2/F1/F2 — le reste (49e1d5a backend, 7efa634 frontend, merge b30c9c7).
crates/app-tauri/src/embedded_server.rs: controller de cycle de vie surAppState, commandesembedded_server_start/stop/statusetget/save/preview_server_exposure_settings,FsServerExposureSettingsStore.frontend/src/features/settings/:SettingsView,DeploymentSettings,useDeployment;adapters/desktopServer.ts; portDesktopServerGateway.ProjectsView:showSettings: boolean→ navigation interneAI Profiles/Deployment. Le libellé alternant « Close AI Profiles » a disparu.
Décisions structurantes appliquées
- Pas de
PanelId: "settings". Architect s'est corrigé en cours de route : Settings existe déjà dansProjectsViewcomme surface principale, hors modèleviewPlacement. C'est de la configuration globale persistée, pas une vue projet dockable/détachable. - Persistance dans
<app-data-dir>/deployment/server-exposure.json, paslocalStorage/uiPreferences: falsifier cette config peut exposer le serveur, c'est de la config de sécurité. Écriture atomique,0600posé avant lerename. - Le pairing code n'est jamais persisté — secret runtime, affiché seulement si
running. - Le frontend n'invente jamais une IP :
candidateLanAddressesetupstreamUrlviennent du backend, d'où l'existence depreview. EmbedderSettings/ModelServersPanelnon rapatriés : refonte latérale hors sprint. La liste de sections est le seam où ils s'inséreront.
⚠️ Le brief de Main était FAUX — intercepté par DevFrontend
Main décrivait le mode remoteProxyOtherMachine avec deux champs (origine publique + IP du proxy). validate_settings en exige un troisième : lanBindAddress, et rejette loopback/unspecified. Construit selon le brief, le lot aurait été vert et cassé : chaque save et chaque start en mode 3 auraient échoué, sans qu'aucun test ne le voie.
Corrigé par un select alimenté par candidateLanAddresses — la règle « le frontend n'invente jamais une IP » tient. Leçon consignée dans 7efa634.
Vérification réelle (Main puis Git, tous deux hors sandbox)
npm run typecheck → propre
npx vitest run → 87 files / 789 passed
cargo test -p app-tauri → 43 passed, 1 ignored
cargo test -p web-server → 66 passed
Re-vérifié sur develop après merge : 24 suites Rust, 0 échec.
Invariants vérifiés par exécution, pas par lecture de rapport : run_embedded_with_core est le seul appelant (aucun run_embedded nu dans app-tauri) ; aucune fuite d'EmbeddedServer*/ServerExposure* dans domain ni application ; ordre du store write(tmp) → chmod 0600 → rename ; stop() appelé à la sortie d'app (lib.rs:153) ; features/web n'importe pas la surface Settings (épinglé par test) et le transport web rend UNSUPPORTED_ON_WEB.
Piège d'environnement : app-tauri et web-server ont un sandbox qui bloque TcpListener::bind (EPERM). Le test end_to_end_over_real_loopback est #[ignore] préexistant — lancé hors sandbox avec --ignored, il passe. Garde d'environnement confirmée par exécution, pas faux vert.
RÉSERVE — aucune validation live
Tout est vert, rien n'a été cliqué. Le panneau n'a jamais été ouvert dans l'app réelle : ni le démarrage/arrêt, ni l'affichage du pairing code, ni les trois modes, ni la persistance entre deux lancements. jsdom ne prouve pas le rendu. Une AppImage a été construite pour que l'utilisateur teste — c'est le point de vérité qui manque.
Dette identifiée, non traitée (ne bloque rien)
previewvalide avant de prévisualiser (embedded_server.rs:211) : un brouillon mode 3 ne peut pas être prévisualisé tant qu'il n'a pas lelanBindAddressque la liste des candidats doit justement fournir. Contourné côté UI par une sondelocalOnlytoujours valide. Défaut d'ordonnancement backend.- Code mort : le warning
missingTrustedProxy(embedded_server.rs:452) est inatteignable —validate_settingsrejette lestrustedProxiesvides en mode 3 avant quepreview_settingsne puisse le produire. - Aucun événement de statut : le backend n'émet rien,
onStatusChangedest du polling dans l'adapter Tauri. La forme du port est prête pour un vrai événement, sans toucher l'UI. - Fenêtre umask sur le temporaire du store : le
0600arrive après la création. Aucun secret dans ce fichier (modes, ports, IP), risque faible. UnOpenOptions::mode(0o600)à la création fermerait le sujet. - Piège latent hors #68 : les stubs
unsupported.tspréexistants (WebRemoteGateway,WebWindowGateway) throwent de façon synchrone depuis des méthodes qui rendent des Promise —.then(ok, err)ne les attraperait pas. Sans effet aujourd'hui (les appelantsawait), mais mérite un nettoyage.
Contexte : pourquoi #68 est passé avant #71 et #73
Arbitrage utilisateur, contre la recommandation d'Architect. Ce dernier proposait #72 → #73 → #71 → #68, ce qui reléguait en dernier la fonctionnalité demandée le matin même. L'utilisateur a tranché : #72 ajusté, puis #68. #71 (diagnostics) et #73 (TLS intégré) suivent.