diff --git a/coherence_taches.md b/coherence_taches.md
new file mode 100644
index 0000000..879b705
--- /dev/null
+++ b/coherence_taches.md
@@ -0,0 +1,122 @@
+# Revue de cohérence — tâches system_update
+
+> **But** : vérifier que les tâches 1.9 à 8 s'enchaînent proprement vers l'objectif final : une webapp serveur `system_update` exploitable via Docker Compose, avec API extensible et clients futurs.
+
+---
+
+## 1. Verdict global
+
+Les tâches sont globalement cohérentes.
+
+Il n'y a pas de contradiction bloquante détectée entre :
+
+- moteur templates/scripts ;
+- frontend web ;
+- backend/API ;
+- BDD ;
+- Hermes/MCP ;
+- optimisation/nettoyage ;
+- app locale future.
+
+Les recouvrements repérés sont normaux : certains sujets sont transverses et doivent apparaître dans plusieurs tâches, mais avec responsabilités différentes.
+
+---
+
+## 2. Flux de développement recommandé
+
+Ordre logique :
+
+```text
+1. Validation tâche 1.9 → schéma BDD cible
+2. Validation tâche 2 → templates, APT, Docker, contrats JSON
+3. Validation tâche 4 → scripts post-install/hardware/profils
+4. Validation tâche 5 → backend/API/jobs/storage
+5. Validation tâche 3 → frontend web/tuiles/paramètres/layout
+6. Validation tâche 6 → Hermes/MCP/skills
+7. Validation tâche 7 → optimisation/nettoyage/sécurité/découverte
+8. Validation tâche 8 → app Rust/GNOME future
+```
+
+Pourquoi cet ordre :
+
+- la BDD et les contrats JSON structurent tout ;
+- les scripts/templates produisent les données ;
+- le backend stocke, orchestre et expose ;
+- le frontend consomme ces contrats ;
+- Hermes/MCP et optimisations s'appuient sur le backend ;
+- l'app Rust/GNOME reste une évolution future via API commune.
+
+---
+
+## 3. Recouvrements acceptés
+
+### Métriques simples
+
+- `tache4.md` : définit le script SSH `machine_metrics_simple`.
+- `tache5.md` : définit stockage/API/snapshot.
+- `tache7.md` : définit affichage footer, optimisation et usage observabilité.
+- `tache1.9.md` : définit tables `machine_metrics_latest`.
+
+Ce n'est pas un doublon : chaque tâche couvre une couche différente.
+
+### Logs, rapports et messages importants
+
+- `tache5.md` : stockage backend, API, rétention.
+- `tache6.md` : accès Hermes/MCP.
+- `tache7.md` : nettoyage/rétention.
+- `tache1.9.md` : tables.
+
+Ce recouvrement est nécessaire.
+
+### Paramètres frontend
+
+- `tache3.md` : UX paramètres.
+- `tache1.9.md` : stockage `app_settings`, `user_preferences`, `machine_ui_state`.
+- `tache5.md` : API `/api/settings`.
+
+Ce découpage est cohérent.
+
+### App locale Rust/GNOME
+
+- `tache8.md` : client natif futur.
+- `tache5.md` : API commune/capabilities.
+- `tache1.9.md` : table `api_clients`.
+
+Ce n'est pas un chantier immédiat ; il doit rester futur.
+
+---
+
+## 4. Points corrigés pendant la revue
+
+- `tache2.md` et `validation_tache2.md` parlaient encore de 7 questions d'investigation alors que la tâche en contient 8 après ajout des profils machine. Corrigé.
+
+---
+
+## 5. Objectif final Docker Compose
+
+Objectif final confirmé :
+
+```text
+Une webapp serveur system_update déployable via Docker Compose :
+- backend API ;
+- frontend web servi par le backend ou reverse proxy ;
+- SQLite persisté en volume ;
+- reports/logs persistés en volume ;
+- configuration via variables d'environnement ;
+- secrets master key hors image ;
+- accès réseau vers machines SSH ;
+- option future reverse proxy/TLS.
+```
+
+Cet objectif doit rester un critère transversal dans les validations, surtout tâches 5, 7 et le plan d'implémentation final.
+
+---
+
+## 6. Risques à surveiller
+
+- Ne pas implémenter toutes les tâches en un seul jalon : trop grand.
+- Garder les actions dangereuses validées UI, même si Hermes ou l'app Rust les demande.
+- Ne pas exposer les credentials SSH/sudo/API dans logs, UI, Hermes, MCP ou clients locaux.
+- Garder SQLite au MVP, mais écrire le schéma pour migrer vers PostgreSQL.
+- Garder les scripts critiques versionnés sur disque, pas uniquement en BDD.
+- Ne pas confondre terminal live d'exécution, vrai terminal SSH interactif et conversation Hermes.
diff --git a/consigne_icon.md b/consigne_icon.md
new file mode 100644
index 0000000..f0b254d
--- /dev/null
+++ b/consigne_icon.md
@@ -0,0 +1,295 @@
+# Consigne icônes — system_update
+
+> **Type** : brief de création d'icônes SVG et assets applicatifs.
+> **Langue** : français.
+> **But** : transmettre à un agent spécialisé les contraintes de création d'icônes pour la webapp `system_update`.
+
+---
+
+## 1. Contexte
+
+`system_update` est une application d'administration système agentless SSH :
+
+- suivi de machines Debian, Ubuntu, Proxmox, Raspberry Pi OS ;
+- update/analyse APT ;
+- Docker Compose ;
+- scripts post-install ;
+- métriques simples ;
+- logs/rapports ;
+- discussion Hermes ;
+- terminal SSH.
+
+Le design system est **Gruvbox seventies** :
+
+- rétro-industriel ;
+- console de monitoring ;
+- SCADA / terminal années 70 ;
+- fond brun/gris usé ;
+- accent orange brûlé ;
+- UI technique, dense, lisible.
+
+Lire aussi :
+
+- `design_system/consigne_design_system.md`
+- `design_system/tokens/tokens.css`
+- `design_system/tokens/tokens.gnome.css`
+- `design_system/tokens/tokens.json`
+- `tache3.md`
+
+---
+
+## 2. Règles absolues
+
+- Ne pas utiliser d'emoji.
+- on peut utiliser des logos officiels de distributions, Docker, Proxmox, Raspberry Pi, NVIDIA, etc.
+- on peut utiliser des mascotte.
+- Ne pas créer d'illustration complexe.
+- Icônes lisibles en `16px`, `20px`, `24px`, `32px`.
+- SVG monochrome ou bichrome maximum.
+- Les couleurs doivent être pilotables par CSS : `currentColor`, variables CSS ou classes.
+- Pas de hex en dur dans les SVG finaux, sauf si un export bitmap final est explicitement demandé.
+- Stroke régulier, formes simples, angles légèrement industriels.
+- Éviter les arrondis excessifs.
+- Les icônes UI courantes doivent d'abord utiliser Font Awesome via `Icon`; créer un SVG custom seulement pour les concepts spécifiques.
+
+---
+
+## 3. Assets applicatifs à créer
+
+### Favicon principal
+
+Fichier cible recommandé :
+
+```text
+client/public/favicon.svg
+```
+
+Concept :
+
+- petit terminal ou serveur ;
+- LED de statut ;
+- flèche d'update ;
+- grille machine ou stack discret.
+
+Contraintes :
+
+- lisible à `16x16` ;
+- pas de texte ;
+- silhouette reconnaissable ;
+- version dark/light compatible.
+
+### Fallback navigateur
+
+```text
+client/public/favicon.ico
+```
+
+Exporter depuis le SVG en tailles :
+
+- `16x16`
+- `32x32`
+- `48x48`
+
+### Icônes smartphone / PWA
+
+```text
+client/public/apple-touch-icon.png
+client/public/icon-192.png
+client/public/icon-512.png
+client/public/maskable-512.png
+```
+
+Contraintes :
+
+- fond plein compatible thème Gruvbox ;
+- symbole centré ;
+- marge de sécurité pour icône maskable ;
+- lisible sur fond clair et sombre ;
+- pas de détails fins.
+
+---
+
+## 4. Direction visuelle
+
+Formes recommandées :
+
+- serveur rack compact ;
+- terminal carré ;
+- grille 2x2 de machines ;
+- LED ronde ;
+- flèche circulaire d'update ;
+- ligne de terminal ;
+- stack de conteneurs ;
+- puce CPU ;
+- disque ;
+- bouclier sécurité.
+
+Palette conceptuelle :
+
+- fond : utiliser les tokens `--bg-2`, `--bg-3` ;
+- trait principal : `--ink-1` ou `currentColor` ;
+- accent : `--accent` ;
+- statut ok : `--ok` ;
+- warning : `--warn` ;
+- erreur : `--err`.
+
+Le SVG doit rester utilisable en `currentColor`. Les variantes colorées ne doivent être utilisées que pour favicon/app icon, pas pour toutes les icônes UI.
+
+---
+
+## 5. Liste d'icônes nécessaires
+
+### Navigation et layout
+
+| Alias | Usage | Source recommandée |
+|---|---|---|
+| `app-logo` | logo app, favicon | SVG custom |
+| `machines` | onglet machines | Font Awesome ou SVG custom |
+| `hermes` | volet discussion agent | SVG custom si identité locale nécessaire |
+| `settings` | paramètres | Font Awesome |
+| `terminal` | volet terminal | Font Awesome |
+| `logs` | logs bruts | Font Awesome |
+| `report` | rapport Markdown | Font Awesome |
+| `copy` | copier message/commande | Font Awesome |
+| `fullscreen` | terminal plein écran | Font Awesome |
+| `collapse` | réduire volet | Font Awesome |
+
+### Actions système
+
+| Alias | Usage | Source recommandée |
+|---|---|---|
+| `refresh` | analyse/refresh | Font Awesome |
+| `analyze` | update + analyse | Font Awesome |
+| `upgrade` | upgrade | Font Awesome |
+| `full-upgrade` | full/dist-upgrade | SVG custom optionnel |
+| `reboot` | reboot | Font Awesome |
+| `reboot-verified` | reboot vérifié | SVG custom optionnel |
+| `stop` | arrêter action | Font Awesome |
+| `dry-run` | simulation | Font Awesome |
+| `approve` | validation action | Font Awesome |
+| `reject` | refus action | Font Awesome |
+
+### Type de machine
+
+| Alias | Usage | Source recommandée |
+|---|---|---|
+| `server` | machine générique | Font Awesome |
+| `vm` | machine virtuelle | SVG custom optionnel |
+| `physical-host` | machine physique | SVG custom optionnel |
+| `proxmox-host` | hôte hyperviseur générique | SVG custom, sans logo Proxmox |
+| `container` | LXC/container | Font Awesome ou SVG custom |
+| `raspberry-pi` | Raspberry Pi générique | SVG custom sans logo officiel |
+| `workstation` | poste/serveur GPU | Font Awesome |
+
+### Hardware et métriques
+
+| Alias | Usage | Source recommandée |
+|---|---|---|
+| `cpu` | CPU/load | Font Awesome existant |
+| `memory` | RAM | Font Awesome existant |
+| `disk` | disque/df | Font Awesome existant |
+| `network` | réseau | Font Awesome existant |
+| `gpu` | GPU | SVG custom optionnel |
+| `temperature` | température | Font Awesome |
+| `smart-disk` | SMART disk | SVG custom optionnel |
+| `benchmark` | benchmark | Font Awesome |
+| `machine-probe` | détection hardware | SVG custom optionnel |
+
+### APT, Docker, scripts
+
+| Alias | Usage | Source recommandée |
+|---|---|---|
+| `package` | paquet APT | Font Awesome |
+| `repository` | dépôt APT | Font Awesome |
+| `firmware` | firmware | SVG custom optionnel |
+| `driver` | driver | SVG custom optionnel |
+| `docker` | Docker installé/absent | SVG custom ou Font Awesome si disponible |
+| `compose-stack` | stack Compose | SVG custom recommandé |
+| `container-image` | image Docker | SVG custom optionnel |
+| `prune` | nettoyage images | SVG custom optionnel |
+| `script` | script install | Font Awesome |
+| `profile` | profil post-install | Font Awesome |
+
+### Sécurité et états
+
+| Alias | Usage | Source recommandée |
+|---|---|---|
+| `ok` | succès | Font Awesome |
+| `warning` | warning | Font Awesome |
+| `error` | erreur | Font Awesome |
+| `locked` | action verrouillée | Font Awesome |
+| `secret` | secret masqué | Font Awesome |
+| `key` | clé SSH/API | Font Awesome |
+| `shield` | sécurité | Font Awesome |
+| `disconnected` | machine/Hermes déconnecté | Font Awesome |
+| `running` | action en cours | Font Awesome |
+
+---
+
+## 6. Icônes SVG custom prioritaires
+
+Priorité haute :
+
+1. `app-logo`
+2. `compose-stack`
+3. `machine-probe`
+4. `reboot-verified`
+
+Priorité moyenne :
+
+1. `proxmox-host`
+2. `physical-host`
+3. `vm`
+4. `gpu`
+5. `firmware`
+6. `driver`
+
+Priorité basse :
+
+1. `smart-disk`
+2. `prune`
+3. `container-image`
+4. `raspberry-pi`
+
+---
+
+## 7. Format attendu des SVG
+
+Recommandation :
+
+```xml
+
+```
+
+Contraintes :
+
+- `viewBox="0 0 24 24"` pour icônes UI ;
+- `viewBox="0 0 512 512"` possible pour logo/app icon source ;
+- `stroke-width` entre `1.6` et `2`;
+- pas de filtre SVG, pas de blur ;
+- pas de texte vectorisé ;
+- pas de dépendance externe ;
+- fichiers nommés en kebab-case.
+
+---
+
+## 8. Validation visuelle
+
+Chaque icône doit être vérifiée :
+
+- en dark theme ;
+- en light theme ;
+- en `16px`, `20px`, `24px`, `32px` ;
+- sur fond `--bg-2` et `--bg-3` ;
+- en état normal, warning, error si applicable ;
+- avec le composant `IconButton` et tooltip.
+
+Critères d'acceptation :
+
+- silhouette compréhensible sans label à `20px` ;
+- pas de confusion entre `refresh`, `upgrade`, `reboot` ;
+- pas de confusion entre `vm`, `physical-host`, `proxmox-host`, `container` ;
+- pas de dépendance à une marque externe ;
+- rendu cohérent avec le design system Gruvbox seventies.
diff --git a/docs/design/tache2/00-synthese.md b/docs/design/tache2/00-synthese.md
new file mode 100644
index 0000000..9ce58c4
--- /dev/null
+++ b/docs/design/tache2/00-synthese.md
@@ -0,0 +1,91 @@
+# Tâche 2 — Moteur de templates de mise à jour : synthèse de design
+
+> **Type** : document de design / spec. Aucune implémentation. Langue : français.
+> **Périmètre** : design du moteur de templates (APT + Docker + scripts custom) et des contrats de données associés, prêt à passer en `writing-plans`.
+> **Statut** : design figé proposé, à valider contre `validation_tache2.md` (gate obligatoire avant tout code).
+
+---
+
+## 1. Objet de la mission
+
+Concevoir — sans coder — le **moteur de templates de mise à jour complet** et les **contrats JSON** associés, couvrant cinq axes :
+
+- **Axe A** — Templates APT complets et OS-aware (update/analyse, upgrade, full/dist-upgrade, clean, autoremove, reboot-check, reboot vérifié), profils OS + type machine, proxy apt-cacher-ng.
+- **Axe B** — Capture des mises à jour *prévues* (snapshot) et *appliquées* (diff réel avant/après), consommables par Hermes via déduplication + réduction déterministe.
+- **Axe C** — Taxonomie des erreurs APT/dpkg/Docker + stratégie de remédiation.
+- **Axe D** — Docker Compose : scan, inspect, pull-check, apply, prune, down, par SSH, avec racines déclarées + détection labels.
+- **Axe E** — Scripts personnalisés (post-install, installation de paquets) avec garde-fous, manifestes et champs dynamiques.
+
+La logique métier vit dans des **templates shell versionnés sur disque** (esprit `nas-ops`), rendus en Mustache et poussés en SSH (`server/ssh/client.ts`). Le backend orchestre, parse en **JSON canonique**, archive logs + rapports. Hermes analyse les JSON réduits, n'exécute jamais de SSH, ne reçoit jamais de secret.
+
+---
+
+## 2. Cartographie des livrables
+
+| Fichier | Contenu | Axe / Livrable §4 |
+|---|---|---|
+| `00-synthese.md` (ce fichier) | Vue d'ensemble, décisions clés, couverture du gate | tous |
+| `10-templates-apt.md` | Inventaire + pseudo-shell des templates APT, sémantique, marqueurs | A, §4.1, §4.2 |
+| `20-docker.md` | Inventaire + pseudo-shell Docker Compose, flux, sécurité prune/down | D, §4.1, §4.2 |
+| `30-scripts-custom.md` | Modèle des profils post-install, manifestes, champs dynamiques, garde-fous | E, §4.1, §4.7 |
+| `40-contrats-json.md` | Schémas JSON canoniques étendus + types TS rétro-compatibles + déduplication/réduction | B, §4.3 |
+| `50-erreurs.md` | Taxonomie des erreurs APT/dpkg/Docker/réseau + codes + remédiation | C, §4.4 |
+| `60-profils-os-machine.md` | Modèle profils OS + type machine + overrides + proxy APT + détection | A, §4.5, §4.6 |
+| `70-securite.md` | Frontière Hermes/MCP, actions destructives, validations, surface MCP | §4.8 |
+| `80-sous-jalons.md` | Découpage en sous-jalons priorisé, prêt pour `writing-plans` | §4.9 |
+| `90-questions-investigation.md` | Les 8 questions §3 tranchées (MVP / alternatives / risques) | §3 |
+| `99-couverture-gate.md` | Auto-évaluation case par case de `validation_tache2.md` | gate |
+
+---
+
+## 3. Décisions structurantes (résumé)
+
+Détail et justifications dans `90-questions-investigation.md`. Synthèse :
+
+1. **Parsing : hybride, parsing-TS dominant (MVP).** On conserve l'approche actuelle (marqueurs `===SU:XXX===` + parsing TS dans `server/services/`), enrichie par des **données structurées en TSV/clé=valeur produites côté shell** (ex. `dpkg-query -W -f=...`, `docker ... --format json`) là où le format est déjà stable et documenté. Pas de génération de gros JSON imbriqué dans le shell. Rétro-compatible avec le jalon 1.
+2. **Profils OS = fichiers de templates par profil + héritage par convention de dossier.** Arborescence `templates//.sh.tpl` avec un profil `base` et des overrides par OS résolus par ordre de priorité. Le moteur de rendu choisit le template le plus spécifique disponible (fallback vers `base`).
+3. **Type machine = choix manuel à l'ajout + action `machine_probe` de correction.** L'opérateur choisit `os_family` et `machine_kind` au formulaire ; une sonde non destructive propose des corrections (jamais appliquées automatiquement sans validation).
+4. **Diff avant/après = snapshot dpkg autour de chaque action APT réelle.** `dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n'` avant et après ; le backend calcule le diff. L'exit code APT ne suffit jamais à déclarer un succès.
+5. **Extensions de `shared/types.ts` : tout en champs optionnels.** Élargissement des unions (`OsFamily`, `ActionType`, `AptProxyMode`), ajout de blocs optionnels `apt` (détaillé), `docker`, `custom`/`postInstall`, `reboot`, `errors` sur `UpdateSnapshot` / `ExecutionResult`. Un snapshot/exécution du jalon 1 reste strictement valide.
+6. **Opérations longues : `nohup` + fichier exit-code généralisé pour les actions applicatives longues, mais pas pour le refresh.** Reboot vérifié = mécanisme dédié (boot_id avant/après, reconnexion, délai adaptatif). Le refresh/analyse reste synchrone et court.
+7. **Sécurité prune/down/scripts : barrière de validation côté webapp + `action_requests`.** Hermes propose, ne déclenche jamais. Secrets jamais lus ni renvoyés (registry creds, sudo, tokens). Erreurs nettoyées avant UI/MCP.
+8. **Surface MCP minimale, en lecture + déclenchement d'actions déjà autorisées.** Réutilise les outils v1 du rapport (`list_machines`, `get_machine_snapshot`, `get_machine_execution`, `list_templates`, `preview_template`, `run_refresh`, `run_action`, `search_reports`) ; aucune nouvelle primitive d'exécution SSH exposée.
+
+---
+
+## 4. Principes invariants respectés
+
+- **Convention templates** : tous les templates émettent des marqueurs `===SU:XXX===` ; `LC_ALL=C` ; `DEBIAN_FRONTEND=noninteractive` pour APT ; exécution sous `sudo -S` via `runScriptSudo` (base64) ; marqueur de sortie `===SU:EXIT=N===`.
+- **Réduction déterministe avant LLM** : le réducteur (`aptReduce.ts`, à étendre en `reduceLines.ts`) ne garde que les lignes utiles APT et Docker ; le log brut complet est archivé séparément (`raw_artifacts` / `rawLogPath`).
+- **Déduplication** : APT par `os_family + package + from + to + origin` ; Docker par `image + fromDigest + toDigest` (fallback `image + fromImageId + toImageId`).
+- **Templates versionnés sur disque** : éditables depuis le front mais sauvegardés comme ressources de projet (revues Git), versionnés via `install_recipe_versions` pour les scripts custom.
+- **Backend orchestre, shell porte la logique métier, JSON canonique = langage commun** frontend / MCP / Hermes.
+- **Réutilisation de l'existant** : pas de nouveau mécanisme d'exécution SSH ; on s'appuie sur `runScriptSudo` / `runPlain` et sur la table `executions` + WebSocket terminal.
+
+---
+
+## 5. Alignement avec `tache1.9.md` (schéma BDD cible)
+
+Les contrats JSON et tables dérivées de ce design se rangent dans les tables prévues :
+
+- Snapshot APT/Docker → `snapshots(kind, payload_json, important_json)` + tables dérivées `apt_planned_packages`, `docker_compose_stacks`, `docker_stack_services`.
+- Résultats d'exécution → `executions(result_json, important_json)` + `apt_applied_packages`, `docker_image_events`, `apt_errors`.
+- Scripts custom → `install_profiles`, `install_recipes`, `install_recipe_versions`, `machine_profile_state`, `script_variables_presets`.
+- Messages importants extraits → `important_messages`.
+- Config Docker par machine → `docker_settings`, `docker_compose_roots`.
+- Profils OS / type machine → colonnes `machines.os_family / machine_kind / virtualization / hardware_profile` + `machine_hardware` (sonde).
+
+Aucune table nouvelle n'est requise par la tâche 2 ; le design réutilise la cible `tache1.9.md`.
+
+---
+
+## 6. Ce qui reste hors périmètre (suggestions, pas exécuté)
+
+- Catalogue détaillé des scripts post-install → renvoyé à **tâche 4** (ce design pose le mécanisme moteur + les manifestes attendus).
+- API/jobs/route d'action, file de jobs persistante → **tâche 5**.
+- Affichage UI fin des snapshots/actions → **tâche 3**.
+- Skill Hermes et analyse → **tâche 6**.
+- Politique de rétention/purge des logs → **tâche 7**.
+- Découverte réseau de machines → hors tâche 2.
+
+Ces points sont mentionnés comme contexte d'emboîtement mais ne sont pas conçus en détail ici.
diff --git a/docs/design/tache2/10-templates-apt.md b/docs/design/tache2/10-templates-apt.md
new file mode 100644
index 0000000..2b09fa0
--- /dev/null
+++ b/docs/design/tache2/10-templates-apt.md
@@ -0,0 +1,202 @@
+# 10 — Templates APT : inventaire, sémantique et pseudo-shell
+
+> Axe A + livrables §4.1 et §4.2. Cohérent avec `templates/apt/check.sh.tpl`, `full-upgrade.sh.tpl`, `reboot.sh.tpl` existants, la convention `===SU:XXX===`, `LC_ALL=C`, `DEBIAN_FRONTEND=noninteractive`, exécution sous `sudo -S` (`server/ssh/client.ts`).
+
+---
+
+## 1. Sémantique APT clarifiée (manpage `apt-get`)
+
+| Commande | Effet | Peut supprimer ? | Peut installer du nouveau ? |
+|---|---|---|---|
+| `apt-get update` | Resynchronise les index de paquets. Ne modifie aucun paquet installé. | non | non |
+| `apt-get -s upgrade` | **Simulation** : installe les nouvelles versions des paquets installés **sans jamais supprimer** ni installer de nouveaux paquets. Les paquets dont l'upgrade exigerait une suppression/installation restent *held back*. | non | non |
+| `apt-get -s dist-upgrade` / `full-upgrade` | **Simulation** : gère intelligemment les changements de dépendances, peut **installer de nouveaux paquets et en supprimer** pour satisfaire les dépendances. | oui | oui |
+| `apt-get autoremove` | Retire les dépendances automatiquement installées et devenues inutiles. | oui | non |
+| `apt-get clean` | Vide le cache local `/var/cache/apt/archives`. N'affecte pas l'état des paquets. | non | non |
+
+> `apt full-upgrade` (commande `apt`) ≡ `apt-get dist-upgrade` (commande `apt-get`). **On utilise toujours `apt-get` en script** (non interactif, stable), jamais `apt` (UI humaine). L'UI parle d'« full-upgrade » comme alias convivial ; la commande système est `apt-get dist-upgrade`.
+
+Lignes documentées parsées : `Inst [] ( [])`, `Conf `, `Remv `. Le log brut complet reste archivé ; seules ces lignes + `E:`/`W:`/`dpkg:`/`reboot-required` alimentent Hermes.
+
+Sources : `apt-get` https://manpages.debian.org/apt-get · `dpkg` https://manpages.debian.org/dpkg · `dpkg-query` https://manpages.debian.org/dpkg-query · `apt-listchanges` https://manpages.debian.org/bookworm/apt-listchanges/apt-listchanges.1.en.html · `needrestart` https://manpages.debian.org/bookworm/needrestart/needrestart.1.en.html
+
+---
+
+## 2. Inventaire des templates APT
+
+| Template | Action (`ActionType`) | Rôle | Type | OS ciblés | Destructif ? | Marqueurs |
+|---|---|---|---|---|---|---|
+| `apt/update-analyze.sh.tpl` | `apt_update_analyze` | Refresh index + simulation `upgrade` et `dist-upgrade` + reboot-check. **Tâche de fond, non destructif.** Remplace/étend l'actuel `check.sh.tpl`. | snapshot | tous | non | `===SU:APT_UPDATE===`, `===SU:APT_SIM_UPGRADE===`, `===SU:APT_SIM_DISTUPGRADE===`, `===SU:APT_HELD===`, `===SU:REBOOT===`, `===SU:EXIT=N===` |
+| `apt/upgrade.sh.tpl` | `apt_upgrade` | Applique l'upgrade simple (sans suppression volontaire). Snapshot dpkg avant/après. | action | tous | oui (modif paquets) | `===SU:DPKG_BEFORE===`, `===SU:APT_UPGRADE===`, `===SU:DPKG_AFTER===`, `===SU:REBOOT===`, `===SU:EXIT=N===` |
+| `apt/full-upgrade.sh.tpl` | `apt_full_upgrade` | Applique `apt-get dist-upgrade`. **Conserve l'existant (jalon 1) en l'enrichissant du diff dpkg.** | action | tous | oui (peut supprimer) | idem upgrade + `===SU:APT_FULLUPGRADE===` |
+| `apt/autoremove.sh.tpl` | `apt_autoremove` | Retire les dépendances inutiles, avec simulation préalable affichée. | action | tous | oui (supprime) | `===SU:APT_SIM_AUTOREMOVE===`, `===SU:DPKG_BEFORE===`, `===SU:APT_AUTOREMOVE===`, `===SU:DPKG_AFTER===`, `===SU:EXIT=N===` |
+| `apt/clean.sh.tpl` | `apt_clean` | Vide le cache des paquets téléchargés. Action séparée, peu risquée. | action | tous | non (cache only) | `===SU:APT_CLEAN===`, `===SU:EXIT=N===` |
+| `apt/reboot-check.sh.tpl` | (intégré au refresh) | Vérifie `/run/reboot-required`, `/var/run/reboot-required`, liste `reboot-required.pkgs`, état `needrestart -b`. | snapshot | tous | non | `===SU:REBOOT===` |
+| `apt/reboot.sh.tpl` | `reboot_verified` | Lit `boot_id` avant, planifie le reboot, émet un marqueur parsable avant coupure SSH. Conserve l'existant en ajoutant `boot_id`. | action | tous | oui (reboot) | `===SU:BOOT_ID_BEFORE===`, `===SU:REBOOT_NOW===` |
+
+> **Compatibilité jalon 1** : `apt_full_upgrade` et `reboot` restent des actions valides. `check.sh.tpl` n'est pas supprimé ; `update-analyze.sh.tpl` est son successeur enrichi (le refresh peut basculer dessus sans casser le parsing existant — voir §6 ci-dessous).
+
+---
+
+## 3. Variables de rendu (Mustache)
+
+Étend `TemplateVars` (extension proposée, voir `40-contrats-json.md`). Variables utilisées par les templates APT :
+
+```text
+aptProxy string|null proxy apt-cacher-ng injecté à l'exécution (mode runtime)
+osProfile string debian | ubuntu | proxmox | raspbian
+machineKind string physical | vm | proxmox_host | lxc | raspberry_pi | workstation
+confValues bool true => --force-confdef --force-confold (défaut)
+inactivityTimeout int secondes; 0 = désactivé (défaut 600)
+```
+
+Les sections Mustache `{{#aptProxy}}…{{/aptProxy}}` restent identiques à l'existant.
+
+---
+
+## 4. Pseudo-shell des templates clés
+
+### 4.1 `apt/update-analyze.sh.tpl` (refresh + analyse, non destructif)
+
+```sh
+#!/bin/sh
+# Refresh index + simulations upgrade/dist-upgrade + reboot-check.
+# Exécuté entier sous sudo par la couche SSH. Non destructif.
+export LC_ALL=C
+export DEBIAN_FRONTEND=noninteractive
+{{#aptProxy}}export http_proxy="{{aptProxy}}"; export https_proxy="{{aptProxy}}"
+{{/aptProxy}}
+
+echo "===SU:APT_UPDATE==="
+apt-get update -qq 2>&1
+UPD=$?
+
+echo "===SU:APT_SIM_UPGRADE==="
+apt-get -s -y upgrade 2>&1
+
+echo "===SU:APT_SIM_DISTUPGRADE==="
+apt-get -s -y dist-upgrade 2>&1
+
+echo "===SU:APT_HELD==="
+# Paquets retenus (held back) : présents en dist-upgrade mais pas en upgrade.
+apt-mark showhold 2>/dev/null
+
+echo "===SU:REBOOT==="
+if [ -f /run/reboot-required ] || [ -f /var/run/reboot-required ]; then
+ echo "REBOOT_REQUIRED=1"
+ [ -f /var/run/reboot-required.pkgs ] && sed 's/^/PKG=/' /var/run/reboot-required.pkgs
+else
+ echo "REBOOT_REQUIRED=0"
+fi
+# needrestart en mode batch/list si présent (jamais interactif).
+command -v needrestart >/dev/null 2>&1 && needrestart -b 2>/dev/null | grep -E '^NEEDRESTART-(KSTA|SVC)' || true
+
+echo "===SU:EXIT=${UPD}==="
+```
+
+Le backend parse :
+- section `APT_SIM_UPGRADE` → liste `upgrade` (paquets `Inst`),
+- section `APT_SIM_DISTUPGRADE` → liste `dist-upgrade` (inclut `Inst` nouveaux + `Remv` suppressions),
+- `held` = `APT_HELD` (et/ou paquets présents en dist-upgrade absents d'upgrade),
+- `REBOOT_REQUIRED` + `PKG=` → reboot requis + paquets concernés.
+
+Statut snapshot APT : `ok` si rien ; `updates_available` si `Inst` non vides ; `warning` si dist-upgrade implique des `Remv` ou des `held` ; `error` si `APT_UPDATE` échoue (dépôt injoignable, clé GPG…).
+
+### 4.2 `apt/full-upgrade.sh.tpl` (application, avec diff dpkg)
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+export DEBIAN_FRONTEND=noninteractive
+{{#aptProxy}}export http_proxy="{{aptProxy}}"; export https_proxy="{{aptProxy}}"
+{{/aptProxy}}
+
+echo "===SU:DPKG_BEFORE==="
+dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
+
+echo "===SU:APT_FULLUPGRADE==="
+apt-get -y \
+ -o Dpkg::Options::=--force-confdef \
+ -o Dpkg::Options::=--force-confold \
+ dist-upgrade 2>&1
+CODE=$?
+
+echo "===SU:DPKG_AFTER==="
+dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
+
+echo "===SU:REBOOT==="
+if [ -f /run/reboot-required ] || [ -f /var/run/reboot-required ]; then echo "REBOOT_REQUIRED=1"; else echo "REBOOT_REQUIRED=0"; fi
+
+echo "===SU:EXIT=${CODE}==="
+```
+
+Le backend compare `DPKG_BEFORE` et `DPKG_AFTER` (clé = `package+arch`) → `installed` / `upgraded` / `removed` / `unchanged`, versions finales réelles. **L'exit code seul ne suffit pas** : un paquet annoncé en simulation mais resté inchangé est signalé comme anomalie.
+
+> Politique non interactive justifiée : `--force-confdef`+`--force-confold` conservent les fichiers de config locaux quand dpkg ne peut pas trancher, pour ne pas écraser une configuration distante. Les prompts (conffile, debconf, apt-listchanges, needrestart) sont traités comme **risques de blocage à détecter** (timeout d'inactivité), pas comme dialogues à exposer. Voir `50-erreurs.md` (`human_interaction_required`).
+
+### 4.3 `apt/autoremove.sh.tpl`
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+export DEBIAN_FRONTEND=noninteractive
+echo "===SU:APT_SIM_AUTOREMOVE==="
+apt-get -s -y autoremove 2>&1 # prévisualisation des Remv
+echo "===SU:DPKG_BEFORE==="
+dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
+echo "===SU:APT_AUTOREMOVE==="
+apt-get -y autoremove 2>&1
+CODE=$?
+echo "===SU:DPKG_AFTER==="
+dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
+echo "===SU:EXIT=${CODE}==="
+```
+
+Confirmation UI explicite requise (action qui supprime des paquets).
+
+### 4.4 `apt/clean.sh.tpl`
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+echo "===SU:APT_CLEAN==="
+BEFORE=$(du -sb /var/cache/apt/archives 2>/dev/null | awk '{print $1}')
+apt-get clean 2>&1
+AFTER=$(du -sb /var/cache/apt/archives 2>/dev/null | awk '{print $1}')
+echo "FREED_BYTES=$((BEFORE - AFTER))"
+echo "===SU:EXIT=0==="
+```
+
+### 4.5 `apt/reboot.sh.tpl` (reboot vérifié — capture boot_id)
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+echo "===SU:BOOT_ID_BEFORE==="
+cat /proc/sys/kernel/random/boot_id 2>/dev/null
+echo "===SU:REBOOT_NOW==="
+# Reboot différé pour laisser le canal SSH se fermer proprement.
+nohup sh -c 'sleep 2; reboot' >/dev/null 2>&1 &
+echo "reboot planifié"
+```
+
+Le **backend** orchestre la vérification (hors template) : il a lu `boot_id` *avant*, attend la coupure SSH, retente la connexion (délai adaptatif), relit `boot_id` via `runPlain` (`cat /proc/sys/kernel/random/boot_id`). Reboot `ok` seulement si la machine revient ET `boot_id` a changé. Statuts d'échec : `reboot_command_failed`, `ssh_never_went_down`, `machine_did_not_return`, `boot_id_unchanged`, `timeout`. Champs résultat : `beforeBootId`, `afterBootId`, `requestedAt`, `sshWentDownAt`, `sshCameBackAt`, `waitedSeconds`, `status`, `errors`. Délai adaptatif par machine : `lastRebootDurationSeconds` → `nextRecommendedWaitSeconds` (avec marge). Voir `40-contrats-json.md` (`RebootResult`).
+
+---
+
+## 5. Spécificités par profil OS (détail dans `60-profils-os-machine.md`)
+
+- **Debian** : avant de proposer firmware/drivers propriétaires, vérifier la présence des composants `contrib`, `non-free`, `non-free-firmware` (template `apt/check-components.sh.tpl`, lecture seule). Pas d'activation automatique.
+- **Ubuntu** : `ubuntu-drivers devices` (lecture) pour proposer des drivers (NVIDIA/GPU) — proposition uniquement, jamais installé par défaut.
+- **Proxmox** : profil dédié. Vérifier dépôts PVE (`pve-no-subscription` vs `enterprise`), meta-package `proxmox-ve`, kernel PVE, Ceph éventuel, puis `apt-get dist-upgrade`. Ne **jamais** traiter comme une Debian générique.
+- **Raspberry Pi OS** : profil dédié. Attention firmware/kernel, **vérifier l'espace disque avant upgrade**, utiliser `full-upgrade`.
+
+Le template le plus spécifique disponible sous `templates//.sh.tpl` est choisi, sinon fallback `templates/apt/.sh.tpl` (profil `base`).
+
+---
+
+## 6. Compatibilité et migration (non-régression jalon 1)
+
+- L'actuel `check.sh.tpl` produit `===SU:UPDATE===` / `===SU:SIMULATE===` / `===SU:REBOOT===` / `===SU:END===`. Le nouveau `update-analyze.sh.tpl` ajoute des sections **sans supprimer** la sémantique : le parsing actuel (`extractSection(raw, "===SU:SIMULATE===", "===SU:REBOOT===")`) peut être conservé en parallèle pendant la migration.
+- **Recommandation MVP** : introduire `update-analyze.sh.tpl` comme nouveau template et faire pointer le refresh dessus dans un sous-jalon dédié, en gardant `check.sh.tpl` jusqu'à bascule validée. Aucune rupture du flux prouvé en prod.
+- `full-upgrade.sh.tpl` et `reboot.sh.tpl` existants restent fonctionnels ; on **ajoute** les sections `DPKG_BEFORE/AFTER` et `BOOT_ID_BEFORE` (extension, pas remplacement de marqueurs).
diff --git a/docs/design/tache2/20-docker.md b/docs/design/tache2/20-docker.md
new file mode 100644
index 0000000..6ff3ac6
--- /dev/null
+++ b/docs/design/tache2/20-docker.md
@@ -0,0 +1,211 @@
+# 20 — Docker Compose : inventaire, flux et pseudo-shell
+
+> Axe D + livrables §4.1/§4.2. Conçu pour cocher chaque case de `validation_tache2.md §6` (« Focus Docker Compose »). Gestion **par SSH sur la machine cible** via `server/ssh/client.ts` et templates versionnés ; pas de moteur parallèle.
+
+---
+
+## 1. Méthode retenue (MVP)
+
+- **Gestion par SSH** sur la machine cible, réutilisant `runScriptSudo` / `runPlain` et la table `executions`, le WebSocket terminal, `rawLogPath`, `reportPath`, statut `ok|warning|error`. **Pas de second système de jobs Docker.**
+- Variante `docker context` over SSH : **citée comme alternative opérateur**, pas le moteur MVP.
+- **Découverte des stacks** depuis des **racines déclarées par machine** (`composeRoots`, ex. `/opt/stacks`, `/srv/docker`), scan limité en profondeur (`composeScanDepth`, défaut 4), puis validation UI.
+- **Détection par labels Compose** (`com.docker.compose.project`, `com.docker.compose.service`, et si présent `com.docker.compose.project.working_dir`) = **complément** pour retrouver les stacks actifs, pas l'unique source de vérité.
+- Cycle de vie d'un stack : `candidate` (juste détecté) → `enabled` (validé par l'utilisateur) → actions autorisées. `pull`, `up`, `down`, `prune` **uniquement sur un stack `enabled`/validé**.
+
+Sources citées : `docker compose pull` https://docs.docker.com/reference/cli/docker/compose/pull/ · `up` https://docs.docker.com/reference/cli/docker/compose/up/ · `config` https://docs.docker.com/reference/cli/docker/compose/config/ · `ps` https://docs.docker.com/reference/cli/docker/compose/ps/ · `images` https://docs.docker.com/reference/cli/docker/compose/images/ · `down` https://docs.docker.com/reference/cli/docker/compose/down/ · `image inspect` https://docs.docker.com/reference/cli/docker/image/inspect/ · `image prune` https://docs.docker.com/reference/cli/docker/image/prune/
+
+---
+
+## 2. Inventaire des templates Docker
+
+| Template | Action (`ActionType`) | Rôle | Effet disque | Destructif ? | Validation UI |
+|---|---|---|---|---|---|
+| `docker/scan-compose.sh.tpl` | `docker_scan` | Scanne les racines déclarées, trouve les fichiers compose, valide chaque candidat. **Passif.** | non | non | non |
+| `docker/inspect-compose.sh.tpl` | `docker_inspect_current` | État actuel sans changement : config images, ps, images, inspect. **Passif.** | non | non | non |
+| `docker/pull-check.sh.tpl` | `docker_pull_check` | `docker compose pull` (télécharge sans démarrer), compare ID/digest/labels avant-après. **Écrit sur le disque Docker** (pas un scan pur). | oui (cache images) | non (pas applicatif) | non (mais non passif) |
+| `docker/apply-compose.sh.tpl` | `docker_compose_apply` | `docker compose up -d --remove-orphans` après validation. Recapture ps/images/inspect. | oui | oui (recrée conteneurs) | **oui, explicite** |
+| `docker/prune-images.sh.tpl` | `docker_prune_images` | `docker image prune -f` (safe) ; mode agressif `-a -f --filter "until=168h"`. | oui | oui (agressif) | **oui pour agressif** |
+| `docker/down-compose.sh.tpl` | `docker_compose_down` | `docker compose down`. **Action séparée et destructive**, hors chemin de mise à jour normal. | oui | oui | **oui, forte** |
+
+> `--volumes` et `--rmi` sur `down` : **interdits au MVP** (ou protégés par une validation forte distincte). Le chemin de mise à jour normal n'utilise jamais `down` : `up -d` recrée les conteneurs quand l'image ou la config change, en préservant les volumes montés.
+
+Tous les templates : `LC_ALL=C`, marqueurs `===SU:DOCKER_*===`, sortie parsable, log brut archivé.
+
+---
+
+## 3. Flux de mise à jour Docker (formalisé)
+
+1. `docker_scan` — découverte des stacks candidats (racines déclarées + labels actifs).
+2. `docker_inspect_current` — état actuel des services, conteneurs, images.
+3. `docker_pull_check` — téléchargement des images candidates **sans démarrage de conteneurs**.
+4. Comparaison déterministe — image ref, image ID, repo digest, labels OCI (`org.opencontainers.image.version`, `revision`, `source`, `created`) si présents.
+5. Proposition UI/Hermes — liste des stacks/services avec update dispo, erreurs de pull, inconnues.
+6. `docker_compose_apply` après validation utilisateur — `docker compose up -d --remove-orphans`.
+7. Vérification après application — conteneurs recréés, état `running/exited`, health si dispo, erreurs.
+8. `docker_prune_images` après succès ou action séparée — images supprimées, espace récupéré, erreurs.
+
+**Points clés validés** :
+- `docker compose pull` télécharge mais **ne démarre pas** les conteneurs → bon pré-check applicatif, pas un scan sans effet.
+- `docker compose up -d` recrée les conteneurs quand l'image/config a changé, **en préservant les volumes** → `down` inutile pour une MAJ normale.
+- `docker image prune -f` = images *dangling* (sûr) ; `docker image prune -a` = **toutes** les images non référencées par un conteneur (destructif).
+
+---
+
+## 4. Pseudo-shell des templates
+
+### 4.1 `docker/scan-compose.sh.tpl`
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+echo "===SU:DOCKER_SCAN==="
+# {{composeRoots}} rendu en liste shell-safe par le backend (une racine par ligne).
+ROOTS="{{composeRoots}}"
+DEPTH="{{composeScanDepth}}"
+for root in $ROOTS; do
+ [ -d "$root" ] || continue
+ find "$root" -maxdepth "$DEPTH" -type f \
+ \( -name 'compose.yaml' -o -name 'compose.yml' \
+ -o -name 'docker-compose.yaml' -o -name 'docker-compose.yml' \) \
+ -not -path '*/.git/*' -not -path '*/node_modules/*' \
+ -not -path '*/backup/*' -not -path '*/old/*' -not -path '*/archive/*' \
+ 2>/dev/null | while IFS= read -r f; do
+ dir=$(dirname "$f")
+ # Valide le candidat ; n'applique rien.
+ if docker compose -f "$f" config --quiet >/dev/null 2>&1; then
+ echo "STACK_OK\tdir=$dir\tfile=$f"
+ else
+ echo "STACK_INVALID\tdir=$dir\tfile=$f"
+ fi
+ done
+done
+echo "===SU:DOCKER_LABELS==="
+# Complément : stacks actifs détectés par labels.
+docker ps --format '{{ "{{.ID}}" }}' 2>/dev/null | while read -r id; do
+ proj=$(docker inspect --format '{{ "{{index .Config.Labels \"com.docker.compose.project\"}}" }}' "$id" 2>/dev/null)
+ wd=$(docker inspect --format '{{ "{{index .Config.Labels \"com.docker.compose.project.working_dir\"}}" }}' "$id" 2>/dev/null)
+ [ -n "$proj" ] && echo "ACTIVE\tproject=$proj\tworking_dir=$wd"
+done
+echo "===SU:EXIT=0==="
+```
+
+> Note de rendu : les `{{ }}` Docker Go-template sont échappés ici pour ne pas être interprétés par Mustache (le moteur de rendu réel utilisera des délimiteurs Mustache personnalisés ou un échappement, à fixer en implémentation). Seules `composeRoots`/`composeScanDepth` sont des variables Mustache.
+
+### 4.2 `docker/inspect-compose.sh.tpl`
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+cd "{{stackDir}}" || { echo "===SU:DOCKER_ERR===\ncompose_not_found"; echo "===SU:EXIT=2==="; exit 2; }
+echo "===SU:DOCKER_CONFIG_IMAGES==="
+docker compose config --images 2>&1
+echo "===SU:DOCKER_PS==="
+docker compose ps --format json 2>&1
+echo "===SU:DOCKER_IMAGES==="
+docker compose images --format json 2>&1
+echo "===SU:DOCKER_INSPECT==="
+# Pour chaque image utilisée : Id, RepoDigests, labels OCI.
+docker compose config --images 2>/dev/null | while IFS= read -r img; do
+ docker image inspect "$img" \
+ --format '{{ "IMG\t{{.Id}}\t{{join .RepoDigests \",\"}}\t{{index .Config.Labels \"org.opencontainers.image.version\"}}\t{{index .Config.Labels \"org.opencontainers.image.source\"}}" }}' 2>/dev/null \
+ || echo "IMG_MISSING\t$img"
+done
+echo "===SU:EXIT=0==="
+```
+
+### 4.3 `docker/pull-check.sh.tpl`
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+cd "{{stackDir}}" || { echo "compose_not_found"; echo "===SU:EXIT=2==="; exit 2; }
+echo "===SU:DOCKER_INSPECT_BEFORE==="
+docker compose config --images 2>/dev/null | while IFS= read -r img; do
+ id=$(docker image inspect "$img" --format '{{ "{{.Id}}" }}' 2>/dev/null || echo "")
+ dg=$(docker image inspect "$img" --format '{{ "{{join .RepoDigests \",\"}}" }}' 2>/dev/null || echo "")
+ echo "BEFORE\t$img\t$id\t$dg"
+done
+echo "===SU:DOCKER_PULL==="
+# Télécharge les images candidates SANS démarrer de conteneurs.
+docker compose pull --policy always --ignore-buildable 2>&1
+CODE=$?
+echo "===SU:DOCKER_INSPECT_AFTER==="
+docker compose config --images 2>/dev/null | while IFS= read -r img; do
+ id=$(docker image inspect "$img" --format '{{ "{{.Id}}" }}' 2>/dev/null || echo "")
+ dg=$(docker image inspect "$img" --format '{{ "{{join .RepoDigests \",\"}}" }}' 2>/dev/null || echo "")
+ ver=$(docker image inspect "$img" --format '{{ "{{index .Config.Labels \"org.opencontainers.image.version\"}}" }}' 2>/dev/null || echo "")
+ echo "AFTER\t$img\t$id\t$dg\t$ver"
+done
+echo "===SU:EXIT=${CODE}==="
+```
+
+Backend : compare `BEFORE`/`AFTER` par `image ref`. Si `id`/`digest` change → `updates_available`. Erreurs `pull_failed`/`registry_auth_failed` nettoyées (jamais d'URL/token sensible vers UI/MCP).
+
+### 4.4 `docker/apply-compose.sh.tpl`
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+cd "{{stackDir}}" || { echo "compose_not_found"; echo "===SU:EXIT=2==="; exit 2; }
+echo "===SU:DOCKER_APPLY==="
+docker compose up -d --remove-orphans 2>&1
+CODE=$?
+echo "===SU:DOCKER_PS_AFTER==="
+docker compose ps --format json 2>&1
+echo "===SU:DOCKER_INSPECT_AFTER==="
+docker compose config --images 2>/dev/null | while IFS= read -r img; do
+ docker image inspect "$img" --format '{{ "IMG\t{{.Id}}\t{{join .RepoDigests \",\"}}" }}' 2>/dev/null || echo "IMG_MISSING\t$img"
+done
+echo "===SU:EXIT=${CODE}==="
+```
+
+### 4.5 `docker/prune-images.sh.tpl`
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+echo "===SU:DOCKER_PRUNE==="
+{{#aggressive}}
+# Mode agressif : nécessite validation UI explicite distincte.
+docker image prune -a -f --filter "until=168h" 2>&1
+{{/aggressive}}
+{{^aggressive}}
+# Mode sûr par défaut : dangling images uniquement.
+docker image prune -f 2>&1
+{{/aggressive}}
+CODE=$?
+echo "===SU:EXIT=${CODE}==="
+```
+
+Le backend parse `Total reclaimed space` et `deleted` pour `bytesReclaimed` et la liste d'images supprimées.
+
+### 4.6 `docker/down-compose.sh.tpl`
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+cd "{{stackDir}}" || { echo "compose_not_found"; echo "===SU:EXIT=2==="; exit 2; }
+echo "===SU:DOCKER_DOWN==="
+# --volumes et --rmi INTERDITS au MVP. down simple uniquement.
+docker compose down 2>&1
+CODE=$?
+echo "===SU:EXIT=${CODE}==="
+```
+
+---
+
+## 5. Réduction Hermes (Docker)
+
+Seules ces lignes (+ le JSON canonique) sont transmises : `Pulling`, `Digest`, `Status`, `Downloaded newer image`, `Recreating`, `Started`, `Error`, `deleted`, `Total reclaimed space`. Le log brut complet reste archivé (`raw_artifacts` / `rawLogPath`).
+
+---
+
+## 6. Insertion dans la webapp existante
+
+- **Config machine** : nouveaux champs `dockerEnabled`, `composeRoots[]`, `composeScanDepth`, `composeStacks[]` — **optionnels** ou dans un endpoint dédié ; `MachineView` n'est pas cassé (les champs sont ajoutés en option). Stockés dans `docker_settings` / `docker_compose_roots` / `docker_compose_stacks` (`tache1.9.md`).
+- **Refresh/snapshot** : le refresh machine peut produire un snapshot combiné `apt` + `docker`, **ou** un refresh Docker séparé pour éviter de lancer `docker_pull_check` automatiquement (recommandé : pull-check séparé car non-passif).
+- **Actions** : extension progressive de `ActionType` (`docker_scan`, `docker_pull_check`, `docker_compose_apply`, `docker_prune_images`, `docker_compose_down`) avec filtrage d'autorisation conservé sur `POST /api/machines/:id/actions`.
+- **Executions/rapports** : réutilisation de la table `executions`, du WebSocket terminal, de `rawLogPath`/`reportPath`/statut. Pas de second système.
+- **UI machine** : compteur Docker séparé du compteur APT (ex. stacks avec updates) ; vue détail par stack/service ; boutons d'action validés (`Pull/check`, `Appliquer`, `Prune`, `Down`).
+- **Validation utilisateur** : `docker_compose_apply`, `docker_prune_images` agressif et `docker_compose_down` passent par une confirmation UI explicite (via `action_requests`). Hermes propose, ne déclenche jamais.
+- **Secrets** : credentials registry (`~/.docker/config.json`, helpers, tokens) **jamais lus ni renvoyés** ; erreurs nettoyées si elles exposent une URL sensible (voir `70-securite.md`).
diff --git a/docs/design/tache2/30-scripts-custom.md b/docs/design/tache2/30-scripts-custom.md
new file mode 100644
index 0000000..2a460f3
--- /dev/null
+++ b/docs/design/tache2/30-scripts-custom.md
@@ -0,0 +1,171 @@
+# 30 — Scripts personnalisés / post-install : modèle moteur
+
+> Axe E + livrables §4.1/§4.7. Conçu pour cocher `validation_tache2.md §8` (« scripts post-install et profils personnalisés »). **Le catalogue détaillé est renvoyé à la tâche 4** ; ce document pose le **mécanisme moteur**, les **manifestes**, les **champs dynamiques** et les **garde-fous**.
+
+---
+
+## 1. Principe UX et absence d'interactivité SSH
+
+- **Interdiction stricte des questions interactives au milieu d'un script SSH.** Toute question nécessaire devient un **champ de formulaire** dans la webapp avant exécution.
+- Les profils post-install sont **cochables** ; cocher un profil déplie ses champs obligatoires.
+- Chaque profil fournit un **manifeste** : `id`, `label`, `description`, `fields`, valeurs par défaut, validations, prévisualisation, `risk`, `requiresConfirmation`.
+- Le bouton d'exécution reste **désactivé** tant que les champs requis des profils cochés ne sont pas valides.
+- La webapp propose une **preview du template rendu** avant exécution (`preview_template`), avec **masquage des secrets** et signalement des changements réseau/reboot.
+- Les scripts s'exécutent en **mode non interactif** ; s'ils détectent une décision non fournie, ils **échouent avec une erreur structurée** au lieu de bloquer.
+
+Stockage : `install_profiles` (catalogue + `manifest_json`), `install_recipes`/`install_recipe_versions` (scripts versionnés + sha256), `machine_profile_state` (état par machine, variables **non sensibles**), `script_variables_presets` (préréglages réutilisables). Cf. `tache1.9.md §9`.
+
+---
+
+## 2. Profils post-install attendus (composables)
+
+| Profil | Rôle | Risque | Confirmation |
+|---|---|---|---|
+| `bootstrap_root` | Première prépa après DHCP/`su -` : installe `sudo`, `resolvconf`, `ca-certificates`, `curl` ; ajoute l'opérateur au groupe `sudo` ; vérifie `sudo`. | low | non |
+| `identity_network` | Hostname, domaine/search `.home`, `/etc/hosts`, IP statique dans `/etc/network/interfaces`. | network_change | **oui** |
+| `base_tools` | Outils de base **sans vim** : `nano`, `less`, `bash-completion`, `tmux`, `screen`, `htop`, `iotop`, `ncdu`, `tree`, `rsync`, `unzip`, `zip`, `tar`. | low | non |
+| `network_tools` | `iproute2`, `iputils-ping`, `dnsutils`, `traceroute`, `net-tools`(opt), `tcpdump`, `nmap`, `mtr-tiny`, `lsof`, `netcat-openbsd`. | low | non |
+| `dev_git` | `git`, `curl`, `wget`, `jq`, `yq`, `gnupg`, `lsb-release` ; `build-essential` optionnel. | low | non |
+| `sharing` | `samba`, `nfs-kernel-server`, `avahi-daemon`, `libnss-mdns`, configurables séparément. | medium | oui |
+| `docker_official` | Docker Engine depuis le dépôt officiel Debian : `docker-ce`, `docker-ce-cli`, `containerd.io`, `docker-buildx-plugin`, `docker-compose-plugin` ; ajout user au groupe `docker` ; dossier Compose dans le home ; reboot/reconnexion si nécessaire. | medium | oui |
+| `vm_guest_tools` | `qemu-guest-agent` ou `open-vm-tools` selon hyperviseur choisi. | low | non |
+| Optionnels (non installés par défaut) | `security_basic`, `backup_tools`, `monitoring`, `mail_notify`, `time_sync`, `storage_tools`. | variable | selon profil |
+
+> Les scripts hardware/drivers/benchmark ne sont **jamais installés par défaut** et exigent validation (voir `60-profils-os-machine.md`).
+
+---
+
+## 3. Champs dynamiques générés
+
+- **`identity_network`** : `newHostname`, `domain`/`search`, `interfaceName`, `staticAddress` (CIDR, ex. `10.0.x.y/22`), `gateway` (défaut `10.0.0.1`), `dnsNameservers` (défaut `10.0.0.1`, `10.0.0.10`), `reconnectHost`.
+- **`docker_official`** : `dockerUser` (ex. `gilles`), `dockerHomeDir`/`composeRoot` (ex. `/home/gilles/docker`), `installComposePlugin`, `rebootAfterInstall`.
+- **`sharing`** : choix séparé Samba/NFS/mDNS, noms de partages, chemins autorisés, utilisateurs/groupes si nécessaire.
+- **`vm_guest_tools`** : type d'hyperviseur / paquet cible.
+
+Les champs peuvent être **préremplis** depuis la machine (`machine.name`, IP DHCP, interface primaire détectée par `machine_probe`, utilisateur SSH), mais restent **modifiables** avant validation.
+
+### Exemple de manifeste (attendu dans la spec)
+
+```json
+{
+ "id": "identity_network",
+ "label": "Hostname + IP statique",
+ "requiresConfirmation": true,
+ "risk": "network_change",
+ "fields": [
+ { "name": "newHostname", "type": "hostname", "required": true },
+ { "name": "domain", "type": "string", "required": true, "default": "home" },
+ { "name": "interfaceName", "type": "select", "required": true, "defaultFrom": "detected.primaryInterface" },
+ { "name": "staticAddress", "type": "ipv4_cidr", "required": true },
+ { "name": "gateway", "type": "ipv4", "required": true, "default": "10.0.0.1" },
+ { "name": "dnsNameservers", "type": "ipv4_list", "required": true, "default": ["10.0.0.1", "10.0.0.10"] },
+ { "name": "reconnectHost", "type": "ipv4", "required": true, "defaultFrom": "staticAddress.ip" }
+ ]
+}
+```
+
+Types de champ proposés : `string`, `hostname`, `ipv4`, `ipv4_cidr`, `ipv4_list`, `select`, `bool`, `int`, `path`, `secret` (jamais sérialisé en clair, jamais envoyé à Hermes/MCP). `defaultFrom` référence une valeur détectée par `machine_probe`.
+
+---
+
+## 4. Templates custom attendus (pseudo-shell)
+
+Tous : `LC_ALL=C`, `DEBIAN_FRONTEND=noninteractive`, marqueurs `===SU:CUSTOM_*===`, `===SU:EXIT=N===`, sortie parsable, log brut archivé. Échec contrôlé si décision manquante.
+
+### 4.1 `custom/bootstrap-root.sh.tpl`
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+export DEBIAN_FRONTEND=noninteractive
+echo "===SU:CUSTOM_BOOTSTRAP==="
+apt-get update -qq 2>&1
+apt-get install -y sudo resolvconf ca-certificates curl 2>&1
+CODE=$?
+# Ajoute l'opérateur au groupe sudo (variable de formulaire, non secret).
+usermod -aG sudo "{{operatorUser}}" 2>&1 || echo "WARN usermod"
+# Vérifie sudo
+su - "{{operatorUser}}" -c 'sudo -n true' 2>&1 && echo "SUDO_OK" || echo "SUDO_CHECK_PENDING"
+echo "===SU:EXIT=${CODE}==="
+```
+
+### 4.2 `custom/identity-network.sh.tpl`
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+echo "===SU:CUSTOM_IDENTITY==="
+# Sauvegardes avant modification.
+cp -a /etc/hosts "/etc/hosts.su.bak.$(date +%s)" 2>/dev/null
+cp -a /etc/network/interfaces "/etc/network/interfaces.su.bak.$(date +%s)" 2>/dev/null
+OLD_IP="{{dhcpEndpoint}}"
+echo "OLD_ENDPOINT=${OLD_IP}"
+hostnamectl set-hostname "{{newHostname}}" 2>&1 || echo "hostname_failed"
+# Réécrit /etc/network/interfaces pour {{interfaceName}} en statique {{staticAddress}}.
+# (rendu détaillé en tâche 4 ; échoue proprement si interface absente)
+echo "NEW_ENDPOINT={{reconnectHost}}"
+echo "RECONNECT_REQUIRED=1"
+echo "===SU:EXIT=0==="
+```
+
+Le script **ne coupe jamais la connexion sans stratégie de reconnexion planifiée par la webapp**. Si reboot requis → mécanisme `reboot_verified`. Après application, la webapp vérifie la reconnexion sur la nouvelle IP/hostname et met à jour la machine si le retour est confirmé. Erreurs distinguées : `network_config_invalid`, `interface_not_found`, `dns_config_failed`, `reconnect_failed`, `hostname_failed`, `sudo_setup_failed`.
+
+### 4.3 `custom/install-package-groups.sh.tpl`
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+export DEBIAN_FRONTEND=noninteractive
+echo "===SU:CUSTOM_PKGGROUPS==="
+# {{packages}} rendu comme liste shell-safe par le backend (jamais de vim par défaut).
+apt-get update -qq 2>&1
+apt-get install -y {{packages}} 2>&1
+CODE=$?
+echo "===SU:EXIT=${CODE}==="
+```
+
+### 4.4 `custom/docker-official-debian.sh.tpl`
+
+Suit la doc officielle Docker Debian (https://docs.docker.com/engine/install/debian/, https://docs.docker.com/compose/install/linux/) : clé GPG dans `/etc/apt/keyrings`, `docker.sources`, puis paquets.
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+export DEBIAN_FRONTEND=noninteractive
+echo "===SU:CUSTOM_DOCKER==="
+apt-get update -qq 2>&1
+apt-get install -y ca-certificates curl 2>&1
+install -m 0755 -d /etc/apt/keyrings
+curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc 2>&1
+chmod a+r /etc/apt/keyrings/docker.asc
+# docker.sources écrit selon codename détecté (non secret).
+apt-get update -qq 2>&1
+apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 2>&1
+CODE=$?
+usermod -aG docker "{{dockerUser}}" 2>&1 || echo "WARN docker group"
+mkdir -p "{{composeRoot}}" 2>&1
+echo "DOCKER_GROUP_RELOGIN_REQUIRED=1"
+{{#rebootAfterInstall}}echo "REBOOT_REQUESTED=1"{{/rebootAfterInstall}}
+echo "===SU:EXIT=${CODE}==="
+```
+
+### 4.5 `custom/sharing.sh.tpl` et `4.6 custom/vm-guest-tools.sh.tpl`
+
+Installent Samba/NFS/mDNS selon les choix (sans config dangereuse par défaut) / l'agent invité choisi. Mêmes conventions de marqueurs et d'échec contrôlé. Détail des configs renvoyé à la tâche 4.
+
+Sources citées : Docker Debian https://docs.docker.com/engine/install/debian/ · Compose plugin https://docs.docker.com/compose/install/linux/ · Debian network https://wiki.debian.org/NetworkConfiguration · Debian Handbook https://www.debian.org/doc/manuals/debian-handbook/sect.network-config · resolvconf https://packages.debian.org/stable/net/resolvconf
+
+---
+
+## 5. JSON canonique post-install
+
+`ExecutionResult` reçoit un bloc optionnel `postInstall` (voir `40-contrats-json.md`) listant : profils exécutés, variables **non sensibles** utilisées, fichiers modifiés, paquets installés, services activés/démarrés, reboots demandés, erreurs. Secrets/tokens **jamais** inclus (variables sérialisées, logs UI, rapports, MCP). Les changements réseau/Docker sont marqués dans le rapport Markdown avec les prochaines actions attendues (reconnexion, logout/login groupe Docker, reboot).
+
+---
+
+## 6. Insertion dans la webapp existante
+
+- Même mécanique que les autres actions : templates versionnés, preview, exécution SSH (`runScriptSudo`), WebSocket terminal, `executions`, rapport Markdown, log brut.
+- Valeurs réutilisables conservées dans `script_variables_presets` (scope `global`/`machine`/`profile`) ; état par machine dans `machine_profile_state`. Le provisioning peut être un assistant ponctuel ou stocké par machine.
+- Hermes peut proposer des profils ou expliquer un échec, mais ne reçoit que le JSON réduit et **ne déclenche jamais** les actions à risque sans validation webapp.
+- Découpage en sous-jalons indépendants : bootstrap/sudo, identité+réseau, paquets de base, Docker officiel, partage réseau, outils VM/monitoring (voir `80-sous-jalons.md`).
diff --git a/docs/design/tache2/40-contrats-json.md b/docs/design/tache2/40-contrats-json.md
new file mode 100644
index 0000000..6756478
--- /dev/null
+++ b/docs/design/tache2/40-contrats-json.md
@@ -0,0 +1,311 @@
+# 40 — Contrats JSON canoniques étendus + types TypeScript
+
+> Axe B + livrable §4.3. Tranche la question §3.5 (extensions de `shared/types.ts`). **Tous les ajouts sont rétro-compatibles** : champs optionnels, unions élargies. Un `UpdateSnapshot`/`ExecutionResult` du jalon 1 reste strictement valide.
+
+---
+
+## 1. Principe de rétro-compatibilité
+
+État actuel (`shared/types.ts`) :
+
+```ts
+export type OsFamily = "debian" | "ubuntu" | "unknown";
+export type AptProxyMode = "direct" | "runtime";
+export type ActionType = "apt_full_upgrade" | "reboot";
+// UpdateSnapshot.apt: { enabled, count, rebootRequired, packages: AptPackage[] }
+// ExecutionResult: { ... action: ActionType, status, rebootRequiredAfterRun, importantLogLines, rawLogRef, reportRef }
+```
+
+Règles d'extension :
+1. **Élargir les unions** (`OsFamily`, `AptProxyMode`, `ActionType`) — additif, aucun retrait.
+2. **Ajouter des blocs optionnels** (`docker?`, `errors?`, `apt` détaillé optionnel, `reboot?`, `postInstall?`) — un payload sans ces blocs reste valide.
+3. **Ne jamais retirer ni renommer** un champ existant. Le jalon 1 émet `apt: { enabled, count, rebootRequired, packages }` ; on **ajoute** des champs optionnels à côté.
+4. Versionner via `schemaVersion?: number` (aligné `snapshots.schema_version` / `executions.schema_version` de `tache1.9.md`). Absence ⇒ version 1.
+
+---
+
+## 2. Extensions des unions
+
+```ts
+// Élargissement additif. Le jalon 1 ("debian"|"ubuntu"|"unknown") reste valide.
+export type OsFamily = "debian" | "ubuntu" | "proxmox" | "raspbian" | "unknown";
+
+export type MachineKind =
+ | "physical" | "vm" | "proxmox_host" | "lxc"
+ | "raspberry_pi" | "workstation" | "unknown";
+
+// "persistent" ajouté (écriture dans /etc/apt/apt.conf.d/).
+export type AptProxyMode = "direct" | "runtime" | "persistent";
+
+export type ActionType =
+ // jalon 1 (conservés tels quels)
+ | "apt_full_upgrade" | "reboot"
+ // APT
+ | "apt_update_analyze" | "apt_upgrade" | "apt_dist_upgrade"
+ | "apt_autoremove" | "apt_clean" | "reboot_verified"
+ // Docker
+ | "docker_scan" | "docker_inspect_current" | "docker_pull_check"
+ | "docker_compose_apply" | "docker_prune_images" | "docker_compose_down"
+ // probe + custom
+ | "machine_probe" | "post_install";
+
+export type SnapshotStatus = "ok" | "updates_available" | "warning" | "error";
+export type ExecutionStatus = "ok" | "warning" | "error"; // inchangé
+```
+
+> `reboot` (jalon 1) et `reboot_verified` coexistent : `reboot_verified` ajoute la vérification boot_id ; le code jalon 1 continue d'émettre `reboot`.
+
+---
+
+## 3. Snapshot canonique étendu (`UpdateSnapshot`)
+
+```ts
+export interface AptPackage {
+ name: string;
+ currentVersion: string | null;
+ targetVersion: string;
+ origin: string | null;
+ // Ajouts optionnels (rétro-compatibles) :
+ arch?: string;
+ operation?: "upgrade" | "install" | "remove" | "hold";
+ severityHint?: "normal" | "security";
+}
+
+export interface AptSnapshotDetail {
+ enabled: boolean;
+ count: number;
+ rebootRequired: boolean;
+ packages: AptPackage[];
+ // Ajouts optionnels :
+ status?: SnapshotStatus; // ok | updates_available | warning | error
+ upgradeCount?: number; // simulation `upgrade`
+ distUpgradeCount?: number; // simulation `dist-upgrade`
+ installed?: AptPackage[]; // nouveaux paquets (dist-upgrade)
+ removed?: AptPackage[]; // suppressions prévues (=> status warning)
+ held?: string[]; // paquets retenus (=> status warning)
+ rebootPkgs?: string[]; // depuis reboot-required.pkgs
+}
+
+export interface DockerSnapshotService {
+ serviceName: string;
+ image: string; // image ref (ex. jellyfin/jellyfin:latest)
+ currentImageId?: string | null;
+ currentDigest?: string | null;
+ candidateImageId?: string | null; // après pull-check
+ candidateDigest?: string | null;
+ currentVersion?: string | null; // label OCI org.opencontainers.image.version
+ candidateVersion?: string | null;
+ sourceUrl?: string | null; // label OCI source
+ status?: "up_to_date" | "updates_available" | "warning" | "error";
+}
+
+export interface DockerSnapshotStack {
+ name: string;
+ workingDir: string;
+ composeFiles: string[];
+ projectName?: string | null;
+ status: "candidate" | "enabled" | "ignored" | "error";
+ detectedBy?: "root_scan" | "label" | "manual";
+ services: DockerSnapshotService[];
+}
+
+export interface DockerSnapshot {
+ enabled: boolean;
+ installed: boolean;
+ count: number; // services avec update dispo
+ declaredRoots?: string[];
+ stacks: DockerSnapshotStack[];
+ status?: SnapshotStatus;
+}
+
+export interface SnapshotError {
+ source: "apt" | "docker" | "post_install" | "ssh" | "system";
+ kind: string; // voir 50-erreurs.md (taxonomie)
+ severity: "info" | "warning" | "error";
+ message: string; // nettoyé, jamais de secret
+ remediation?: string;
+ importantLines?: string[];
+}
+
+export interface UpdateSnapshot {
+ machineId: string;
+ hostname: string;
+ os: { family: OsFamily; version: string };
+ checkedAt: string; // ISO 8601
+ status: MachineStatus;
+ apt: AptSnapshotDetail; // bloc jalon 1 conservé, champs additifs optionnels
+ // Ajouts optionnels (rétro-compatibles) :
+ schemaVersion?: number;
+ kind?: "apt_update_analyze" | "docker_scan" | "reboot_check" | "combined";
+ machineKind?: MachineKind;
+ docker?: DockerSnapshot;
+ errors?: SnapshotError[];
+ rawHints?: { logImportantLines: string[] };
+}
+```
+
+> Le bloc `apt` reste **requis** (présent au jalon 1) ; seuls ses champs *additifs* sont optionnels. `docker`, `errors`, `machineKind`, `kind`, `schemaVersion` sont **optionnels** → un snapshot jalon 1 (sans eux) reste valide.
+
+### Bloc Docker minimal exigé par la validation (couverture §6)
+
+Le bloc snapshot Docker contient au minimum : stacks déclarés (`declaredRoots`), stacks candidats (`stacks[].status="candidate"`), services (`stacks[].services[]`), image ref actuelle (`image`), image ID actuelle (`currentImageId`), digest actuel si dispo (`currentDigest`), labels de version si dispo (`currentVersion`), image ID/digest candidat après pull (`candidateImageId`/`candidateDigest`), statut `up_to_date|updates_available|warning|error`. ✔
+
+---
+
+## 4. Résultat d'exécution étendu (`ExecutionResult`)
+
+```ts
+export interface AptChange {
+ name: string;
+ arch?: string;
+ fromVersion: string | null;
+ toVersion: string | null;
+ operation: "upgraded" | "installed" | "removed" | "unchanged";
+ origin?: string | null;
+}
+
+export interface AptExecutionResult {
+ planned: AptPackage[]; // ce qui était prévu (simulation pré-action)
+ applied: AptChange[]; // diff dpkg réel before/after
+ installed: AptChange[];
+ removed: AptChange[];
+ held: string[];
+ errors?: SnapshotError[];
+ rebootRequiredAfterRun: boolean;
+}
+
+export interface DockerImageChange {
+ stack: string;
+ serviceName?: string;
+ imageRef?: string;
+ fromImageId?: string | null;
+ toImageId?: string | null;
+ fromDigest?: string | null;
+ toDigest?: string | null;
+ operation: "pulled" | "recreated" | "pruned";
+}
+
+export interface DockerExecutionResult {
+ pull?: { changes: DockerImageChange[]; errors?: SnapshotError[] };
+ up?: { recreated: string[]; running: string[]; exited: string[]; errors?: SnapshotError[] };
+ prune?: { imagesDeleted: string[]; bytesReclaimed: number; errors?: SnapshotError[] };
+ errors?: SnapshotError[];
+}
+
+export interface RebootResult {
+ beforeBootId: string | null;
+ afterBootId: string | null;
+ requestedAt: string;
+ sshWentDownAt: string | null;
+ sshCameBackAt: string | null;
+ waitedSeconds: number;
+ status: "ok" | "reboot_command_failed" | "ssh_never_went_down"
+ | "machine_did_not_return" | "boot_id_unchanged" | "timeout";
+ lastRebootDurationSeconds?: number;
+ nextRecommendedWaitSeconds?: number;
+ errors?: SnapshotError[];
+}
+
+export interface PostInstallResult {
+ profilesRun: string[];
+ variablesUsed: Record; // NON sensible uniquement
+ filesModified: string[];
+ packagesInstalled: string[];
+ servicesEnabled: string[];
+ rebootsRequested: boolean;
+ networkChange?: {
+ oldEndpoint: string | null;
+ newEndpoint: string | null;
+ reconnectHost: string | null;
+ };
+ errors?: SnapshotError[];
+}
+
+export interface ExecutionResult {
+ executionId: string;
+ machineId: string;
+ startedAt: string;
+ finishedAt: string;
+ mode: "manual" | "scheduled" | "hermes_requested"; // élargi (jalon 1 = "manual")
+ action: ActionType;
+ status: ExecutionStatus;
+ rebootRequiredAfterRun: boolean;
+ importantLogLines: string[];
+ rawLogRef: string;
+ reportRef: string;
+ // Ajouts optionnels (rétro-compatibles) :
+ schemaVersion?: number;
+ apt?: AptExecutionResult;
+ docker?: DockerExecutionResult;
+ reboot?: RebootResult;
+ postInstall?: PostInstallResult;
+ errors?: SnapshotError[];
+}
+```
+
+> `mode` était `"manual"` (littéral) au jalon 1. L'élargir en union `"manual" | "scheduled" | "hermes_requested"` reste compatible (le jalon 1 émet toujours `"manual"`). Tous les nouveaux blocs (`apt`, `docker`, `reboot`, `postInstall`, `errors`) sont optionnels → une exécution jalon 1 reste valide.
+
+---
+
+## 5. Extension de `TemplateVars` (rendu Mustache)
+
+```ts
+export interface TemplateVars {
+ aptProxy?: string | null; // existant
+ // Ajouts (tous optionnels) :
+ osProfile?: OsFamily;
+ machineKind?: MachineKind;
+ confValues?: boolean;
+ inactivityTimeout?: number;
+ // Docker :
+ composeRoots?: string; // liste rendue shell-safe par le backend
+ composeScanDepth?: number;
+ stackDir?: string;
+ aggressive?: boolean; // prune agressif
+ // Custom :
+ operatorUser?: string;
+ packages?: string; // liste shell-safe
+ newHostname?: string;
+ interfaceName?: string;
+ staticAddress?: string;
+ reconnectHost?: string;
+ dockerUser?: string;
+ composeRoot?: string;
+ rebootAfterInstall?: boolean;
+ // ... champs de profil custom (typés au cas par cas en tâche 4)
+}
+```
+
+---
+
+## 6. Déduplication (empreinte fonctionnelle)
+
+- **APT** : `dedupKey = os_family + "|" + package + "|" + from + "|" + to + "|" + origin`. Permet à Hermes de mutualiser une même mise à jour vue sur plusieurs machines (une seule recherche web, un seul résumé). Stocké dans `apt_planned_packages.dedup_key` / `apt_applied_packages.dedup_key`.
+- **Docker** : `dedupKey = image + "|" + fromDigest + "|" + toDigest` ; fallback `image + "|" + fromImageId + "|" + toImageId` quand le digest manque.
+
+Le calcul de `dedupKey` se fait **côté backend TS** (déterministe), pas dans le shell.
+
+---
+
+## 7. Réduction déterministe avant Hermes/MCP
+
+Le réducteur actuel (`server/templates/aptReduce.ts`) garde les lignes : `Inst `, `Conf `, `Remv `, `Err `, `E:`, `W:`, `dpkg:`, `reboot-required`/`REBOOT_REQUIRED`. **Extension proposée** (renommage suggéré `reduceLines.ts`, additif, sans casser `reduceAptLines`) ajoutant les préfixes Docker : `Pulling`, `Digest`, `Status`, `Downloaded newer image`, `Recreating`, `Started`, `Error`, `deleted`, `Total reclaimed space`.
+
+Ce que Hermes reçoit : **JSON canonique réduit** (`important_json`) + lignes importantes (`importantLogLines`). **Jamais** le log brut complet (archivé dans `raw_artifacts`/`rawLogPath`), jamais de secret.
+
+---
+
+## 8. Mapping vers `tache1.9.md` (tables dérivées)
+
+| Bloc JSON | Table dérivée | Colonnes clés |
+|---|---|---|
+| `apt.packages` / `installed` / `removed` / `held` (simulation) | `apt_planned_packages` | `mode`, `operation`, `current_version`, `target_version`, `origin`, `dedup_key` |
+| `apt.applied` (diff dpkg) | `apt_applied_packages` | `from_version`, `to_version`, `operation`, `dedup_key` |
+| `errors[]` source apt | `apt_errors` | `kind`, `severity`, `message`, `important_lines_json`, `remediation` |
+| `docker.stacks[]` | `docker_compose_stacks` + `docker_stack_services` | `status`, `detected_by`, `current_image_id`, `candidate_digest`, … |
+| `docker.pull/up/prune changes` | `docker_image_events` | `from_image_id`, `to_image_id`, `operation`, `bytes_reclaimed` |
+| lignes importantes/notices | `important_messages` | `source`, `category`, `package_name`, `message` |
+| payload complet snapshot | `snapshots.payload_json` + `important_json` | `kind`, `schema_version`, `status` |
+| payload complet exécution | `executions.result_json` + `important_json` | `error_kind`, `error_message`, `exit_code` |
+
+Le JSON complet reste la vérité canonique (archivé) ; les tables dérivées servent recherche/filtres/dédup/badges (conforme à la règle structurante `tache1.9.md §2`).
diff --git a/docs/design/tache2/50-erreurs.md b/docs/design/tache2/50-erreurs.md
new file mode 100644
index 0000000..72c2179
--- /dev/null
+++ b/docs/design/tache2/50-erreurs.md
@@ -0,0 +1,77 @@
+# 50 — Taxonomie des erreurs + stratégie de remédiation
+
+> Axe C + livrable §4.4. Codes d'erreur normalisés alignés sur `SnapshotError.kind` (`40-contrats-json.md`) et `apt_errors.kind` (`tache1.9.md`).
+
+---
+
+## 1. Principes
+
+- **L'exit code ne suffit jamais** à déclarer un succès (APT comme Docker). On corrèle exit code + sections parsées + diff réel + lignes importantes.
+- **Statut normalisé** : `ok` | `warning` | `error`. `warning` = succès partiel ou effet à surveiller (suppressions de paquets, held back, orphans removed, conteneur unhealthy).
+- **Capture des lignes pertinentes** : seules les lignes d'erreur utiles (`E:`, `W:`, `dpkg:`, `Error`, etc.) sont remontées ; le log brut complet reste archivé.
+- **Pas d'auto-réparation dangereuse non validée** : on **propose** une remédiation, on ne l'exécute pas automatiquement (`dpkg --configure -a`, `rm /var/lib/dpkg/lock`, etc. exigent validation explicite).
+- **Nettoyage des secrets** : toute ligne d'erreur exposant une URL d'auth, un token ou un chemin de credential est nettoyée avant UI/MCP (voir `70-securite.md`).
+
+---
+
+## 2. Taxonomie APT / dpkg
+
+| `kind` | Détection (lignes/exit) | Sévérité | Remédiation proposée (non auto) |
+|---|---|---|---|
+| `apt_lock_busy` | `Could not get lock /var/lib/dpkg/lock`, `E: Unable to acquire the dpkg frontend lock` | error | Attendre la fin d'un apt/unattended-upgrades concurrent ; relancer. |
+| `dpkg_interrupted` | `dpkg was interrupted`, `dpkg --configure -a` | error | Proposer `dpkg --configure -a` **avec validation explicite**. |
+| `repo_unreachable` | `Failed to fetch`, `Could not resolve`, `Connection timed out`, `E: Some index files failed to download` | error | Vérifier réseau/proxy apt-cacher-ng/dépôt ; retenter. |
+| `gpg_key_error` | `NO_PUBKEY`, `EXPKEYSIG`, `signatures couldn't be verified` | error | Vérifier/mettre à jour la clé du dépôt ; ne pas désactiver la vérif. |
+| `package_conflict` | `Depends:`, `but it is not going to be installed`, `held broken packages` | warning/error | Examiner le conflit ; éventuel `dist-upgrade` ; validation. |
+| `disk_space_low` | `E: You don't have enough free space`, `No space left on device` | error | Libérer de l'espace (`apt-get clean`), vérifier `/`, `/var`, `/boot`. |
+| `packages_held` | `apt-mark showhold` non vide / présents en dist-upgrade absents d'upgrade | warning | Information : paquets retenus ; nécessite dist-upgrade ou hold explicite. |
+| `packages_removed` | `Remv ` en simulation/diff | warning | Suppression de paquets ⇒ confirmation UI obligatoire. |
+| `human_interaction_required` | timeout d'inactivité atteint, prompt conffile/debconf/needrestart/apt-listchanges détecté | error | Reprise manuelle ; ne pas forcer ; lignes importantes fournies. |
+| `kernel_partial_config` | `needrestart` signale services à redémarrer, `reboot-required` | warning | Planifier `reboot_verified`. |
+| `apt_unknown_error` | exit ≠ 0 sans motif identifié | error | Consulter le log brut archivé. |
+
+### Gestion des interactions humaines (couverture §7)
+
+- Upgrades réels : `DEBIAN_FRONTEND=noninteractive`, `apt-get -y`, `-o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold`.
+- Justification politique par défaut : **conserver les fichiers de config locaux** quand dpkg ne peut pas trancher (ne pas écraser une config distante).
+- Prompts potentiels (conffile, debconf, apt-listchanges, needrestart, service restart, maintainer script) = **risques de blocage à détecter**, jamais des dialogues exposés dans le terminal.
+- **Timeout d'inactivité** (`inactivityTimeout`, défaut 600 s) et/ou **timeout global** → classer l'exécution en erreur contrôlée `human_interaction_required` si une action reste bloquée. Le backend (couche SSH) détecte l'absence de nouvelles données et coupe proprement, en marquant l'exécution.
+
+---
+
+## 3. Taxonomie Docker (alignée §6 de la validation)
+
+| `kind` | Détection | Sévérité | Remédiation |
+|---|---|---|---|
+| `docker_not_installed` | `docker: command not found`, exit 127 | error | Proposer profil `docker_official`. |
+| `compose_not_found` | dossier/fichier compose absent, `no configuration file provided` | error | Vérifier `composeRoots`/chemin du stack. |
+| `compose_config_invalid` | `docker compose config --quiet` exit ≠ 0 | error | Corriger le fichier compose ; stack reste `candidate`. |
+| `registry_auth_failed` | `unauthorized`, `denied`, `pull access denied` | error | Vérifier l'auth registry **sur la machine** (jamais lire les creds) ; message nettoyé. |
+| `pull_failed` | `Error response from daemon`, `manifest unknown`, timeout | warning/error | Vérifier réseau/tag/registry ; retenter. |
+| `image_inspect_failed` | `No such image`, exit ≠ 0 sur inspect | warning | Image absente localement ; relancer pull-check. |
+| `up_failed` | `docker compose up` exit ≠ 0 | error | Examiner logs du service ; conserver l'ancien conteneur. |
+| `container_unhealthy` | health `unhealthy` après up | warning | Surveiller ; proposer rollback manuel. |
+| `prune_failed` | `docker image prune` exit ≠ 0 | warning | Vérifier l'état du daemon. |
+| `disk_space_low` | `no space left on device` pendant pull/up | error | Prune sûr, libérer de l'espace. |
+
+---
+
+## 4. Taxonomie réseau / post-install (couverture §8)
+
+| `kind` | Sévérité | Remédiation |
+|---|---|---|
+| `network_config_invalid` | error | Restaurer la sauvegarde `/etc/network/interfaces.su.bak.*`. |
+| `interface_not_found` | error | Vérifier `interfaceName` (sonde `machine_probe`). |
+| `dns_config_failed` | warning | Vérifier `resolvconf`/`dnsNameservers`. |
+| `reconnect_failed` | error | La webapp retente sur `reconnectHost` ; rollback si échec. |
+| `hostname_failed` | error | Vérifier droits / `hostnamectl`. |
+| `sudo_setup_failed` | error | Reprendre `bootstrap_root` depuis un contexte root. |
+
+---
+
+## 5. Robustesse, idempotence, reprise
+
+- **Idempotence** : les templates de détection (`update-analyze`, `docker_scan`, `inspect`, `pull-check`) sont rejouables sans effet de bord applicatif (pull-check écrit dans le cache images mais ne démarre rien — rejouable).
+- **Opérations longues** : voir `90-questions-investigation.md` Q6. MVP : `nohup` + fichier exit-code pour les actions applicatives longues (full-upgrade, docker apply), reboot vérifié via boot_id ; refresh reste synchrone court.
+- **Reprise** : une exécution coupée laisse un état lisible (sections déjà émises + exit-code sur disque côté machine). Le backend peut relire l'état au lieu de tout relancer.
+- **Verrous** : `machine_locks` (`tache1.9.md`) empêche deux actions concurrentes destructives sur une même machine (`apt`, `docker`, `reboot`, `exclusive`).
diff --git a/docs/design/tache2/60-profils-os-machine.md b/docs/design/tache2/60-profils-os-machine.md
new file mode 100644
index 0000000..dbaefee
--- /dev/null
+++ b/docs/design/tache2/60-profils-os-machine.md
@@ -0,0 +1,150 @@
+# 60 — Profils OS, type machine, overrides et proxy APT
+
+> Axe A + livrables §4.5/§4.6. Tranche §3.2 (structure profils OS) et §3.3 (profils machine). Couvre la grille §7 (« Profils OS et type de machine »).
+
+---
+
+## 1. Deux dimensions distinctes
+
+- **`os_family`** : `debian` | `ubuntu` | `proxmox` | `raspbian` | `unknown` (quelle distro / quel jeu de dépôts et de commandes).
+- **`machine_kind`** : `physical` | `vm` | `proxmox_host` | `lxc` | `raspberry_pi` | `workstation` | `unknown` (quel matériel / quels scripts pertinents : firmware, drivers, guest tools…).
+
+Les deux sont **orthogonaux** : une Debian peut être physique, VM ou LXC ; un Raspberry Pi OS implique presque toujours `raspberry_pi`. Choisis **manuellement à l'ajout**, corrigeables par `machine_probe`. Stockés dans `machines.os_family` / `machine_kind` / `virtualization` / `hardware_profile` (`tache1.9.md §5`).
+
+---
+
+## 2. Arborescence des templates et héritage (décision §3.2)
+
+**Convention de dossier + fallback `base`.** Le moteur de rendu choisit le template **le plus spécifique disponible**, sinon retombe sur le profil générique.
+
+```text
+templates/
+├── apt/ # profil "base" (Debian/Ubuntu générique — jalon 1)
+│ ├── update-analyze.sh.tpl
+│ ├── upgrade.sh.tpl
+│ ├── full-upgrade.sh.tpl
+│ ├── autoremove.sh.tpl
+│ ├── clean.sh.tpl
+│ ├── reboot-check.sh.tpl
+│ └── reboot.sh.tpl
+├── proxmox/ # overrides Proxmox (dépôts PVE, kernel, Ceph)
+│ ├── update-analyze.sh.tpl
+│ └── full-upgrade.sh.tpl
+├── raspbian/ # overrides Raspberry Pi OS (firmware, espace disque)
+│ ├── update-analyze.sh.tpl
+│ └── full-upgrade.sh.tpl
+├── docker/
+│ └── *.sh.tpl
+└── custom/
+ └── *.sh.tpl
+```
+
+Résolution (pseudo-code backend) :
+
+```text
+resolveTemplate(action, osFamily):
+ candidate = templates//.sh.tpl
+ if exists(candidate): return candidate
+ return templates/apt/.sh.tpl # fallback base
+```
+
+**Pourquoi pas un héritage par fragments/includes Mustache ?** Plus simple à auditer en Git (un fichier = un script complet, lisible et testable). Inconvénient : duplication partielle entre profils — accepté au MVP (peu de profils). Alternative notée en §3.2 de `90-questions-investigation.md`.
+
+> **Non-régression jalon 1** : Debian/Ubuntu n'ont pas de dossier dédié ⇒ ils tombent sur `templates/apt/*` (comportement actuel inchangé). Le mécanisme de résolution est **additif**.
+
+---
+
+## 3. Overrides par machine
+
+Au-delà du profil OS, chaque machine peut surcharger :
+
+- `aptProxyMode` / `aptProxyUrl` (déjà présent au jalon 1) ;
+- des variables de contexte (`composeRoots`, `inactivityTimeout`, etc.) ;
+- l'activation de templates (`templates activables` côté formulaire) ;
+- des presets de variables custom (`script_variables_presets` scope `machine`).
+
+Priorité de résolution des variables : **override machine** > **défaut profil OS** > **défaut global**. Aucun override ne peut introduire un secret dans un template (les secrets restent côté `machine_credentials`, injectés uniquement via `runScriptSudo` stdin).
+
+---
+
+## 4. Spécificités par profil OS
+
+### Debian
+- `apt-get update` + `dist-upgrade` standard.
+- Avant firmware/drivers propriétaires : vérifier `contrib`, `non-free`, `non-free-firmware` dans les sources (lecture seule, template `check-components`). Proposition uniquement.
+- Source : https://www.debian.org/releases/bookworm/amd64/ch02s02.en.html
+
+### Ubuntu
+- Idem Debian + `ubuntu-drivers devices` (lecture) pour proposer des drivers (NVIDIA/GPU), surtout sur `machine_kind` physique/workstation/gpu. Jamais installé par défaut.
+
+### Proxmox (profil dédié, jamais Debian générique)
+- Contrôler les dépôts PVE : `pve-no-subscription` vs `enterprise` (sinon `apt update` échoue sur le dépôt entreprise sans abonnement).
+- Meta-package `proxmox-ve`, kernel PVE, Ceph éventuel.
+- `apt-get update` puis `apt-get dist-upgrade`.
+- Source : https://pve.proxmox.com/wiki/System_Software_Updates
+
+### Raspberry Pi OS (profil dédié)
+- Attention firmware/kernel (`rpi-update` **non** utilisé par défaut — risqué).
+- **Vérifier l'espace disque avant upgrade** (carte SD souvent petite).
+- Utiliser `full-upgrade`.
+- Source : https://www.raspberrypi.com/documentation/usage/terminal/
+
+---
+
+## 5. Influence du type machine sur les scripts proposés
+
+| `machine_kind` | Scripts pertinents | À éviter |
+|---|---|---|
+| `physical` | détection hardware, firmware (`fwupd`), SMART/disques, sensors, drivers GPU, benchmark | guest tools |
+| `vm` | guest tools (`qemu-guest-agent` ou `open-vm-tools`) | drivers GPU/firmware (sauf passthrough) |
+| `proxmox_host` | profil Proxmox dédié (dépôts PVE, kernel, Ceph) | traitement Debian générique |
+| `lxc` | minimal (pas de kernel/firmware propre au conteneur) | firmware, drivers, reboot kernel |
+| `raspberry_pi` | profil RPi (firmware/kernel prudent, espace disque) | drivers GPU desktop |
+| `workstation` / GPU server | drivers GPU (`ubuntu-drivers`/`nvidia`), benchmark | — |
+
+> Les scripts hardware/drivers/benchmark ne sont **jamais installés par défaut** et **exigent validation explicite** (couverture §7).
+
+---
+
+## 6. Détection / correction : `machine_probe` (décision §3.3)
+
+Action non destructive (lecture seule), proposée à l'ajout et relançable. Sources lues :
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+echo "===SU:PROBE_OS==="
+cat /etc/os-release 2>/dev/null
+echo "===SU:PROBE_ARCH==="
+uname -m
+dpkg --print-architecture 2>/dev/null
+echo "===SU:PROBE_VIRT==="
+systemd-detect-virt 2>/dev/null || echo "none"
+echo "===SU:PROBE_PROXMOX==="
+[ -d /etc/pve ] && echo "PROXMOX=1" || echo "PROXMOX=0"
+echo "===SU:PROBE_RPI==="
+grep -qi raspberry /proc/cpuinfo 2>/dev/null && echo "RPI=1" || echo "RPI=0"
+echo "===SU:PROBE_GPU==="
+command -v lspci >/dev/null 2>&1 && lspci 2>/dev/null | grep -Ei 'vga|3d|display' || echo "no-lspci"
+echo "===SU:PROBE_NET==="
+ip -o -4 addr show 2>/dev/null | awk '{print $2, $4}'
+echo "===SU:EXIT=0==="
+```
+
+Le backend propose une **correction** de `os_family`/`machine_kind`/`virtualization`/interface primaire, **jamais appliquée automatiquement** sans validation utilisateur (règle de correction : l'opérateur garde le dernier mot). Résultats persistés dans `machine_hardware` + colonnes `machines`.
+
+**MVP retenu** : choix manuel à l'ajout (Debian/Ubuntu/Proxmox VE/Raspberry Pi OS/autre Linux ; VM/physique/Proxmox/LXC/RPi/GPU-workstation) + `machine_probe` pour proposer des corrections. Alternative (détection 100 % automatique) jugée trop fragile (cas limites : conteneurs imbriqués, distros dérivées) — voir `90-questions-investigation.md` Q3.
+
+---
+
+## 7. Proxy APT (apt-cacher-ng) — trois modes
+
+`AptProxyMode = "direct" | "runtime" | "persistent"` (le `persistent` est l'ajout tâche 2).
+
+| Mode | Mécanisme | Quand |
+|---|---|---|
+| `direct` | aucun proxy | défaut |
+| `runtime` | `export http_proxy/https_proxy` dans le script (sections Mustache `{{#aptProxy}}`) — **comportement jalon 1, conservé** | proxy temporaire pour une exécution |
+| `persistent` | écrire `Acquire::http::Proxy "";` dans `/etc/apt/apt.conf.d/01proxy` (action dédiée, idempotente, sauvegarde de l'existant) | proxy permanent de la machine |
+
+Le mode `persistent` est une **action explicite** (écriture sur disque) avec preview ; il n'est pas appliqué silencieusement. `runtime` reste injecté au rendu comme aujourd'hui.
diff --git a/docs/design/tache2/70-securite.md b/docs/design/tache2/70-securite.md
new file mode 100644
index 0000000..121f66c
--- /dev/null
+++ b/docs/design/tache2/70-securite.md
@@ -0,0 +1,70 @@
+# 70 — Note de sécurité : frontière Hermes/MCP et actions destructives
+
+> Livrable §4.8. Tranche §3.7 (sécurité prune/scripts) et §3.8 (surface MCP). Aligné `CLAUDE.md` (sécurité non négociable) et `validation_tache2.md §3`/§6/§7/§8.
+
+---
+
+## 1. Ce qui ne doit JAMAIS atteindre Hermes / MCP / un prompt LLM
+
+- Mots de passe SSH, **sudo password**, passphrases de clés, clés privées.
+- Tokens / credentials de registry Docker (`~/.docker/config.json`, credential helpers, tokens d'auth).
+- Toute variable de champ de type `secret` d'un profil post-install.
+- URLs contenant des identifiants (`https://user:pass@…`), en-têtes d'auth, chaînes de connexion.
+- Le **log brut complet** (archivé séparément, jamais inliné dans un prompt).
+
+Mécanismes :
+- Les secrets vivent uniquement dans `machine_credentials` (chiffrés au repos), injectés **uniquement** via `runScriptSudo` sur **stdin** (`sudo -S -p ''`) — jamais dans le corps du script, jamais en argument, jamais loggés.
+- Le JSON canonique métier ne contient **aucun** champ secret (cf. `PostInstallResult.variablesUsed` = non sensible uniquement).
+- **Nettoyage des erreurs** avant UI/MCP : un filtre déterministe masque les motifs sensibles (`https?://[^/@\s]+:[^/@\s]+@`, `Authorization:`, chemins `*/config.json`, `token=…`) dans les lignes d'erreur Docker/APT avant affichage et avant réduction Hermes.
+
+---
+
+## 2. Actions destructives → validation explicite côté webapp
+
+Toute action modifiant l'état de la machine de façon non triviale passe par une **confirmation UI explicite** et est tracée comme `action_request` (`tache1.9.md §10`).
+
+| Action | Risque | Validation |
+|---|---|---|
+| `apt_dist_upgrade` / `apt_full_upgrade` | peut supprimer des paquets | confirmation explicite si `removed`/`held` |
+| `apt_autoremove` | supprime des paquets | confirmation explicite |
+| `reboot` / `reboot_verified` | redémarrage | confirmation explicite |
+| `docker_compose_apply` | recrée les conteneurs | confirmation explicite |
+| `docker_prune_images` (agressif `-a`) | supprime des images non dangling | confirmation explicite distincte |
+| `docker_compose_down` | arrête le stack | confirmation forte ; `--volumes`/`--rmi` interdits au MVP |
+| `post_install:identity_network` | change le réseau / coupe la connexion | preview obligatoire + sauvegarde fichiers + stratégie de reconnexion planifiée |
+| `apt_proxy persistent` | écrit dans `/etc/apt/apt.conf.d` | preview |
+
+Règle d'or : **Hermes peut recommander/proposer, mais ne déclenche jamais directement** une action à risque. Le déclenchement passe par l'opérateur via l'UI (ou un `action_request` approuvé).
+
+Actions sûres sans validation : `apt_update_analyze`, `docker_scan`, `docker_inspect_current`, `machine_probe`, `apt_clean`. `docker_pull_check` est **non passif** (écrit dans le cache images) mais non applicatif : pas de validation destructive, mais isolé du chemin de scan pur.
+
+---
+
+## 3. Surface MCP minimale (décision §3.8)
+
+On **réutilise la surface v1** du rapport / `CLAUDE.md`, sans nouvelle primitive d'exécution SSH :
+
+| Outil MCP | Rôle | Renvoie un secret ? |
+|---|---|---|
+| `list_machines` | liste les machines (vue publique, sans secret) | non |
+| `get_machine_snapshot` | dernier snapshot réduit (APT + Docker) | non |
+| `get_machine_execution` | résultat d'exécution réduit + réfs logs/rapport | non |
+| `list_templates` | liste des templates disponibles | non |
+| `preview_template` | rendu d'un template avec **masquage des secrets** | non |
+| `run_refresh` | déclenche un refresh/analyse (action sûre) | non |
+| `run_action` | déclenche une action **déjà autorisée** ; les actions destructives exigent une validation webapp préalable | non |
+| `search_reports` | recherche dans les rapports archivés | non |
+
+Principes :
+- Le **MCP est une façade** de l'API métier : aucune logique SSH dedans, aucun secret.
+- Aucun nouvel outil pour Docker/post-install : les nouvelles capacités passent par `run_action(actionType, params)` avec le **filtrage d'autorisation** de la route `POST /api/machines/:id/actions`. Surface stable et petite = agents fiables.
+- `run_action` sur une action destructive non encore validée → renvoie un `action_request` en attente, **pas** une exécution.
+- Audit : tout appel MCP est journalisé (`mcp_audit_log`, `tache1.9.md §11`) avec `request_json` réduit (sans secret).
+
+---
+
+## 4. Traçabilité
+
+- Chaque exécution : log brut archivé (`raw_artifacts`/`rawLogPath`, `redacted=1`), rapport Markdown (`reports`), `important_json` réduit.
+- Les changements réseau/Docker sont explicitement marqués dans le rapport avec les prochaines actions attendues (reconnexion, relogin groupe docker, reboot).
+- Les secrets ne figurent ni dans les rapports, ni dans les logs UI, ni dans le MCP.
diff --git a/docs/design/tache2/80-sous-jalons.md b/docs/design/tache2/80-sous-jalons.md
new file mode 100644
index 0000000..92eae3c
--- /dev/null
+++ b/docs/design/tache2/80-sous-jalons.md
@@ -0,0 +1,89 @@
+# 80 — Découpage en sous-jalons implémentables
+
+> Livrable §4.9. Chaque sous-jalon = un cycle spec → plan → implémentation, indépendamment testable, sans casser le jalon 1. Priorisé. Prêt pour `writing-plans`.
+
+---
+
+## Ordre recommandé et dépendances
+
+```text
+SJ-0 (socle types/réduction) ──► SJ-1 (APT update/analyse) ──► SJ-2 (APT upgrade + diff)
+ │
+ ▼
+ SJ-3 (reboot vérifié)
+SJ-4 (Docker scan/inspect) ──► SJ-5 (Docker pull-check) ──► SJ-6 (Docker apply/prune/down)
+SJ-7 (profils OS Proxmox/RPi) [transversal, après SJ-1]
+SJ-8 (post-install bootstrap/identité) [après SJ-0]
+SJ-9 (post-install Docker officiel / partages / VM tools) [après SJ-8]
+```
+
+---
+
+## SJ-0 — Socle : types étendus + réduction + résolution de profil
+
+- **Contenu** : étendre `shared/types.ts` (unions + blocs optionnels, cf. `40-contrats-json.md`), étendre le réducteur (`reduceLines.ts` ajoutant les préfixes Docker), ajouter le mécanisme `resolveTemplate(action, osFamily)` avec fallback `base`, ajouter `schemaVersion`.
+- **Testable** : tests unitaires de réduction (APT + Docker), tests de résolution de template, validation qu'un snapshot/exécution jalon 1 reste typé valide.
+- **Risque** : faible (additif). **Priorité : 1 (prérequis de tout le reste).**
+
+## SJ-1 — APT update/analyse (snapshot enrichi)
+
+- **Contenu** : `apt/update-analyze.sh.tpl` (update + simulations upgrade/dist-upgrade + held + reboot-check), parsing des sections, `AptSnapshotDetail` enrichi, statut `ok|updates_available|warning|error`. Bascule du refresh dessus (en gardant `check.sh.tpl` jusqu'à validation).
+- **Testable** : fixtures de sortie APT → snapshot ; non-régression du refresh jalon 1.
+- **Risque** : faible-moyen (toucher le refresh). **Priorité : 2.**
+
+## SJ-2 — APT upgrade / full-upgrade / autoremove / clean + diff dpkg réel
+
+- **Contenu** : templates `upgrade`, `full-upgrade` (enrichi diff), `autoremove`, `clean` ; capture `DPKG_BEFORE/AFTER` ; calcul du diff réel (`AptExecutionResult`) ; timeout d'inactivité + `human_interaction_required` ; confirmations UI pour suppressions.
+- **Testable** : fixtures dpkg before/after → diff ; détection des suppressions/held.
+- **Risque** : moyen (actions destructives). **Priorité : 3.**
+
+## SJ-3 — Reboot vérifié (boot_id + délai adaptatif)
+
+- **Contenu** : `apt/reboot.sh.tpl` (capture boot_id) + orchestration backend (attente coupure, reconnexion, relecture boot_id), `RebootResult`, délai adaptatif par machine. Conserve l'action `reboot` jalon 1.
+- **Testable** : simulation des états (`boot_id_unchanged`, `machine_did_not_return`, `timeout`).
+- **Risque** : moyen. **Priorité : 4.**
+
+## SJ-4 — Docker scan + inspect (passifs)
+
+- **Contenu** : `docker/scan-compose.sh.tpl`, `docker/inspect-compose.sh.tpl` ; config machine `dockerEnabled`/`composeRoots`/`composeScanDepth` ; cycle `candidate`/`enabled` ; tables `docker_*`. Détection labels en complément.
+- **Testable** : fixtures de scan → liste de stacks ; validation `compose config --quiet`.
+- **Risque** : faible (passif). **Priorité : 5.**
+
+## SJ-5 — Docker pull-check + comparaison
+
+- **Contenu** : `docker/pull-check.sh.tpl` ; comparaison déterministe ID/digest/labels OCI ; `DockerSnapshot`/services ; dédup Docker ; refresh Docker séparé (non auto).
+- **Testable** : fixtures before/after pull → updates détectées ; nettoyage secrets registry.
+- **Risque** : faible-moyen. **Priorité : 6.**
+
+## SJ-6 — Docker apply + prune + down
+
+- **Contenu** : `apply-compose`, `prune-images` (safe/agressif), `down-compose` ; `DockerExecutionResult` ; validations UI explicites ; `docker_image_events`.
+- **Testable** : fixtures up/prune → conteneurs recréés / bytes reclaimed.
+- **Risque** : moyen-élevé (destructif). **Priorité : 7.**
+
+## SJ-7 — Profils OS Proxmox + Raspberry Pi (+ proxy persistent)
+
+- **Contenu** : dossiers `templates/proxmox/`, `templates/raspbian/` (update-analyze, full-upgrade) ; mode `AptProxyMode="persistent"` ; `machine_probe`.
+- **Testable** : résolution de template par OS ; sonde → propositions de correction.
+- **Risque** : faible (additif, fallback base préservé). **Priorité : 8 (transversal).**
+
+## SJ-8 — Post-install : bootstrap + identité/réseau
+
+- **Contenu** : moteur de profils (manifestes, champs dynamiques, preview, validations), `custom/bootstrap-root.sh.tpl`, `custom/identity-network.sh.tpl` ; `install_profiles`/`install_recipes` ; stratégie reconnexion ; `PostInstallResult`.
+- **Testable** : rendu de manifeste → formulaire ; preview masquant les secrets ; échec contrôlé si champ manquant.
+- **Risque** : moyen (réseau). **Priorité : 9.**
+
+## SJ-9 — Post-install : paquets de base + Docker officiel + partages + VM tools
+
+- **Contenu** : `install-package-groups`, `docker-official-debian`, `sharing`, `vm-guest-tools` ; presets de variables ; renvoi du catalogue détaillé à la tâche 4.
+- **Testable** : installation de groupes ; idempotence.
+- **Risque** : faible-moyen. **Priorité : 10.**
+
+---
+
+## Notes de séquencement
+
+- **SJ-0 est bloquant** pour tous les autres (types + réduction + résolution).
+- APT (SJ-1→3) et Docker (SJ-4→6) sont **indépendants** : peuvent être menés en parallèle après SJ-0.
+- Chaque sous-jalon livre un logiciel testable et ne casse pas les flux jalon 1 (`refresh`, `apt_full_upgrade`, `reboot`) grâce aux extensions additives.
+- Les actions destructives n'arrivent qu'après le socle de validation UI (`action_requests`), conformément à `70-securite.md`.
diff --git a/docs/design/tache2/90-questions-investigation.md b/docs/design/tache2/90-questions-investigation.md
new file mode 100644
index 0000000..c764808
--- /dev/null
+++ b/docs/design/tache2/90-questions-investigation.md
@@ -0,0 +1,72 @@
+# 90 — Les 8 questions d'investigation (§3) tranchées
+
+> Chaque question : **MVP recommandé / alternatives / risques**. Décisions autonomes argumentées, cohérentes avec l'existant et `tache1.9.md`.
+
+---
+
+## Q1 — JSON-in-shell vs parsing-in-TS
+
+**MVP recommandé : hybride à dominante parsing-TS.** On conserve la convention actuelle (marqueurs `===SU:XXX===` + parsing dans `server/services/`). On enrichit avec des **données semi-structurées produites par le shell uniquement quand le format est déjà stable et documenté** : `dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n'` (TSV), `docker compose ps/images --format json`, `docker image inspect --format '...'`. Pas de construction de gros JSON imbriqué à la main dans le shell.
+
+- **Pourquoi** : (a) cohérence avec le jalon 1 (déjà en prod, parsing TS testé) ; (b) testabilité — les fixtures de sortie shell + tests TS sont faciles à maintenir ; (c) robustesse multi-OS — éviter le JSON bricolé en Bash (échappement fragile, comme on le voit dans `nas-ops` avec la concaténation manuelle de chaînes JSON) ; (d) on tire parti des formats JSON **natifs et documentés** de Docker sans les réinventer.
+- **Alternatives** : (1) tout-JSON-in-shell façon `nas-ops` — rejeté (échappement fragile, dur à tester, risque de casser sur des noms/versions exotiques) ; (2) tout-parsing-TS sur sortie brute uniquement — rejeté pour Docker où `--format json` est plus sûr que parser du texte libre.
+- **Risques** : double convention (TSV/clé=valeur + sections) à documenter clairement ; mitigé par un parseur central par section. Format `docker ... --format json` varie selon version de Compose — pin de la commande + fallback texte.
+
+## Q2 — Structure des profils OS
+
+**MVP recommandé : un fichier de template complet par profil, dans un dossier par OS, avec fallback `base`** (`templates//.sh.tpl` → sinon `templates/apt/.sh.tpl`). Résolution par convention de chemin (cf. `60-profils-os-machine.md §2`).
+
+- **Pourquoi** : lisibilité et audit Git (un script = un fichier complet, testable isolément) ; n'invalide pas Debian/Ubuntu (pas de dossier dédié ⇒ fallback `apt/`, comportement jalon 1 intact).
+- **Alternatives** : (1) héritage par fragments/partials Mustache (DRY) — plus complexe à auditer, reporté ; (2) une matrice de variables dans un seul template géant avec `{{#proxmox}}…` — rejeté (templates illisibles, logique métier noyée dans le rendu).
+- **Risques** : duplication partielle entre profils. Accepté au MVP (peu de profils) ; refactor en partials possible plus tard si la duplication devient coûteuse.
+
+## Q3 — Structure des profils machine
+
+**MVP recommandé : choix manuel à l'ajout (`os_family` + `machine_kind`) + action `machine_probe` non destructive proposant des corrections** (jamais appliquées sans validation). Sources : `/etc/os-release`, `uname -m`/`dpkg --print-architecture`, `systemd-detect-virt`, présence `/etc/pve`, `/proc/cpuinfo` (RPi), `lspci` (GPU), `ip addr` (interface).
+
+- **Pourquoi** : la détection auto seule est fragile (conteneurs imbriqués, distros dérivées, VM mal taguées). Le couple « défaut manuel + sonde de correction » est robuste et garde l'opérateur maître.
+- **Alternatives** : (1) détection 100 % auto — rejetée (cas limites) ; (2) manuel sans sonde — perte de confort et risque d'erreur de profil.
+- **Risques** : utilisateur choisit mal le profil ⇒ `machine_probe` le signale ; correction nécessite validation. Persisté dans `machines` + `machine_hardware`.
+
+## Q4 — Capture avant/après (diff réel)
+
+**MVP recommandé : snapshot dpkg complet avant ET après chaque action APT réelle** via `dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n'`, diff calculé côté backend (clé `package+arch`). L'exit code APT ne suffit jamais.
+
+- **Pourquoi** : `dpkg-query` reflète l'état **réel** installé (pas l'intention APT). Détecte les écarts entre simulation et réalité (paquet annoncé non installé, held back effectif).
+- **Alternatives** : (1) parser uniquement la sortie `apt-get` (`Setting up …`) — moins fiable, dépend du locale et du verbeux ; (2) historique `/var/log/dpkg.log` — parsing daté fragile. Les deux gardés comme signaux secondaires, pas comme source de vérité.
+- **Risques** : sur de très gros parcs de paquets, deux `dpkg-query` ajoutent quelques secondes — négligeable. Diff côté TS = testable par fixtures.
+
+## Q5 — Contrats JSON (extensions exactes)
+
+**MVP recommandé : extensions additives détaillées dans `40-contrats-json.md`** — unions élargies (`OsFamily`, `AptProxyMode`, `ActionType`, `MachineKind`), blocs optionnels `docker`/`errors`/`reboot`/`postInstall` + champs additifs sur `apt`, `schemaVersion?`. Types TS fournis. Un payload jalon 1 reste valide.
+
+- **Pourquoi** : rétro-compatibilité stricte exigée par le gate (§3 de la validation). Champs optionnels + unions additives = zéro rupture.
+- **Alternatives** : versionner par type séparé (`UpdateSnapshotV2`) — rejeté (duplication, migration lourde) au profit de `schemaVersion` sur un type unique.
+- **Risques** : un type unique grossit ; mitigé par découpe en sous-interfaces (`AptSnapshotDetail`, `DockerSnapshot`, etc.).
+
+## Q6 — Idempotence & opérations longues
+
+**MVP recommandé : différencier selon la durée et le risque.**
+- **Refresh/analyse, scan, inspect** : synchrones et courts (comme le jalon 1).
+- **Actions applicatives longues** (`full-upgrade`, `docker apply`) : généraliser l'exécution détachée `nohup` + **fichier exit-code sur la machine** (survit à une coupure SSH), comme prévu au jalon 1 et inspiré de `linux-update-dashboard`. Le backend peut relire l'état/exit-code à la reconnexion plutôt que tout relancer.
+- **Reboot** : mécanisme dédié `reboot_verified` (boot_id avant/après + reconnexion + délai adaptatif).
+- **Idempotence** : les détections sont rejouables sans effet de bord applicatif ; `pull-check` écrit dans le cache images mais ne démarre rien (rejouable). `machine_locks` évite la concurrence destructive.
+
+- **Alternatives** : (1) tout synchrone — rejeté (un upgrade long meurt avec la session SSH) ; (2) file de jobs persistante dès le MVP — utile mais relève de la **tâche 5** ; ici on pose le mécanisme `nohup`+exit-code et on renvoie l'orchestration job à la tâche 5.
+- **Risques** : suivi de progression d'une opération détachée = tailing du fichier de sortie distant + WebSocket ; à câbler proprement en implémentation (réutilise `outputHub`).
+
+## Q7 — Sécurité Docker `prune` / scripts custom
+
+**MVP recommandé : barrière de validation côté webapp (`action_requests`) pour toute action destructive + nettoyage déterministe des secrets dans les erreurs.** `docker_prune_images -a`, `docker_compose_down`, `docker_compose_apply`, suppressions APT, `reboot`, `identity_network` ⇒ confirmation explicite. Hermes propose, ne déclenche jamais. Credentials registry/sudo/tokens jamais lus ni renvoyés (cf. `70-securite.md`).
+
+- **Pourquoi** : respecte `CLAUDE.md` (actions destructives validées, aucun secret vers LLM). La barrière unique (`action_requests`) centralise l'autorisation côté API.
+- **Alternatives** : validation par template (flag `requiresConfirmation` dans le manifeste) — complémentaire, pas suffisant seul ; la décision d'autorisation reste côté API/route.
+- **Risques** : un script custom pourrait logger un secret ⇒ règle « pas de secret dans le corps du script » + filtre de nettoyage des lignes avant UI/MCP + revue Git des templates.
+
+## Q8 — Surface MCP
+
+**MVP recommandé : conserver la surface v1 (8 outils), sans nouvelle primitive d'exécution SSH.** Les nouvelles capacités (Docker, post-install, APT détaillé) passent par `run_action(actionType, params)` filtré côté route, pas par de nouveaux outils. `preview_template` masque les secrets. `run_action` sur action destructive non validée renvoie un `action_request` en attente. Audit via `mcp_audit_log`.
+
+- **Pourquoi** : surface petite = agents fiables et auditables ; le MCP reste une **façade** sans logique SSH ni secret.
+- **Alternatives** : exposer un outil par action (`docker_apply`, `apt_upgrade`…) — rejeté (explosion de surface, duplication d'autorisation).
+- **Risques** : `run_action` générique doit valider strictement `actionType`/`params` côté API (liste blanche) ; sinon risque d'action non prévue. Mitigé par la table d'autorisation et `action_requests`.
diff --git a/docs/design/tache2/99-couverture-gate.md b/docs/design/tache2/99-couverture-gate.md
new file mode 100644
index 0000000..bd9edd8
--- /dev/null
+++ b/docs/design/tache2/99-couverture-gate.md
@@ -0,0 +1,182 @@
+# 99 — Auto-évaluation de couverture du gate `validation_tache2.md`
+
+> Relecture case par case. ✅ = couvert ; ⚠️ = couvert avec réserve / hors périmètre design (à confirmer en implémentation). Légende des renvois : fichiers de `docs/design/tache2/`.
+
+---
+
+## §1 Discipline & périmètre
+
+| Case | État | Renvoi |
+|---|---|---|
+| Aucun code de production modifié (server/client/shared/templates/configs) | ✅ | Seuls `docs/design/tache2/**` + section clôture `tache2.md` créés. À vérifier par `git status`. |
+| Jalon 1 et jalon 2 intacts | ✅ | Aucun fichier de jalon touché. |
+| Aucun autre chantier hors périmètre | ✅ | Hors-scope listés comme suggestions (`00-synthese.md §6`). |
+| Dépôts de référence non copiés | ✅ | `nas-ops`/`linux-update-dashboard` cités en inspiration, pseudo-shell réécrit. |
+
+## §2 Complétude — Axes
+
+| Axe | État | Renvoi |
+|---|---|---|
+| A — Templates APT + sémantique + profils OS + proxy | ✅ | `10-templates-apt.md`, `60-profils-os-machine.md` |
+| B — Capture prévu/appliqué consommable Hermes | ✅ | `40-contrats-json.md` (snapshot/diff/dédup/réduction) |
+| C — Taxonomie erreurs + remédiation | ✅ | `50-erreurs.md` |
+| D — Docker scan/pull/up/down/prune + détection + JSON | ✅ | `20-docker.md` |
+| E — Scripts personnalisés + overrides + garde-fous | ✅ | `30-scripts-custom.md` |
+
+## §2 Complétude — Livrables §4
+
+| Livrable | État | Renvoi |
+|---|---|---|
+| Inventaire des templates | ✅ | `10` §2, `20` §2, `30` §2/§4 |
+| Contenu proposé (pseudo-shell, `===SU:XXX===`) | ✅ | `10` §4, `20` §4, `30` §4 |
+| Schémas JSON canoniques étendus | ✅ | `40` |
+| Taxonomie des erreurs | ✅ | `50` |
+| Modèle profils OS + overrides | ✅ | `60` |
+| Modèle profils machine | ✅ | `60` §5 |
+| Modèle scripts personnalisés | ✅ | `30` |
+| Note de sécurité | ✅ | `70` |
+| Découpage en sous-jalons priorisé | ✅ | `80` |
+
+## §2 — 8 questions d'investigation
+
+| | État | Renvoi |
+|---|---|---|
+| Q1–Q8 tranchées (MVP/alternatives/risques) | ✅ | `90-questions-investigation.md` |
+
+## §3 Cohérence & intégration
+
+| Case | État | Renvoi |
+|---|---|---|
+| Types JSON compatibles `shared/types.ts`, rétro-compatibles | ✅ | `40` §1–§4 (champs optionnels, payload jalon 1 valide) |
+| Convention templates (`===SU:`, `LC_ALL=C`, `sudo -S`, parsable) | ✅ | `10`/`20`/`30` |
+| Parsing (JSON-in-shell vs TS) explicite et justifié | ✅ | `90` Q1, `40` §7 |
+| Couche SSH réutilisée (`server/ssh/client.ts`) | ✅ | `00` §4, `90` Q6 |
+| Frontière Hermes/MCP, réduction déterministe | ✅ | `70`, `40` §7 |
+| Sécurité actions destructives + pas de secret | ✅ | `70` §1/§2 |
+| Profils OS n'invalident pas Debian/Ubuntu prod | ✅ | `60` §2 (fallback `base`) |
+| Sous-jalons indépendamment implémentables | ✅ | `80` |
+
+## §4 Non-régression
+
+| Case | État | Note |
+|---|---|---|
+| `pnpm check`/`test`/`build` verts | ⚠️ | Hors périmètre design (aucun code touché) ; à exécuter par l'orchestrateur. Aucune modification de code n'a été faite. |
+| Flux jalon 1 inchangés | ✅ | Extensions additives uniquement ; templates jalon 1 non modifiés. |
+
+## §6 Focus Docker Compose
+
+| Case | État | Renvoi |
+|---|---|---|
+| Gestion par SSH, réutilise couche existante, `docker context` = alternative | ✅ | `20` §1 |
+| Stacks depuis racines déclarées `composeRoots`, scan limité, validation UI | ✅ | `20` §1/§4.1 |
+| Détection labels en complément | ✅ | `20` §1/§4.1 |
+| Stack détecté = `candidate`, actions sur `enabled` seulement | ✅ | `20` §1 |
+| `scan-compose.sh.tpl` (fichiers compose, ignore .git/node_modules/backup/old/archive, `config --quiet`) | ✅ | `20` §4.1 |
+| `inspect-compose.sh.tpl` (`config --images`, `ps --format json`, `images --format json`, `image inspect`) | ✅ | `20` §4.2 |
+| `pull-check.sh.tpl` (`pull --policy always --ignore-buildable`, compare ID/digest/labels, non passif) | ✅ | `20` §4.3, §1 tableau |
+| `apply-compose.sh.tpl` (`up -d --remove-orphans`, recapture) | ✅ | `20` §4.4 |
+| `prune-images.sh.tpl` (safe `-f`, agressif `-a -f --filter until=168h` validé) | ✅ | `20` §4.5 |
+| `down-compose.sh.tpl` (séparé/destructif, `--volumes`/`--rmi` interdits) | ✅ | `20` §4.6 |
+| Flux 1→8 formalisé | ✅ | `20` §3 |
+| `pull` télécharge sans démarrer | ✅ | `20` §3 |
+| `up -d` recrée si changement, préserve volumes, `down` inutile | ✅ | `20` §3 |
+| `prune -f` vs `-a` (destructif) | ✅ | `20` §2/§3 |
+| Sources Docker citées | ✅ | `20` §1 |
+| Snapshot Docker rétrocompatible (bloc optionnel) | ✅ | `40` §3 |
+| Bloc snapshot Docker (stacks/services/ID/digest/labels/candidat/statut) | ✅ | `40` §3 (« bloc Docker minimal ») |
+| `ExecutionResult.docker` (pull/up/prune/erreurs/recréés/supprimés/octets) | ✅ | `40` §4 |
+| Erreurs Docker structurées (10 codes) | ✅ | `50` §3 |
+| Réduction Hermes (lignes Docker) + log brut archivé | ✅ | `20` §5, `40` §7 |
+| Config machine (`dockerEnabled`/`composeRoots`/`composeScanDepth`/`composeStacks[]`) sans casser `MachineView` | ✅ | `20` §6, `40` §5 |
+| Refresh combiné apt+docker ou Docker séparé | ✅ | `20` §6 |
+| `ActionType` étendu (docker_*) + filtrage autorisation | ✅ | `40` §2, `20` §6 |
+| Réutilise `executions`/WS/`rawLogPath`/`reportPath`/statut | ✅ | `20` §6 |
+| UI compteur Docker séparé + détail + boutons validés | ✅ | `20` §6 |
+| Validation UI apply/prune agressif/down ; Hermes ne déclenche pas | ✅ | `20` §6, `70` §2 |
+| Secrets registry jamais lus ; erreurs nettoyées | ✅ | `20` §6, `70` §1 |
+
+## §7 Focus APT/reboot
+
+| Case | État | Renvoi |
+|---|---|---|
+| `apt_update_analyze` distinct des upgrades destructifs | ✅ | `10` §2, `40` §2 |
+| update + `-s upgrade` + `-s dist-upgrade` | ✅ | `10` §4.1 |
+| Snapshot liste paquets prévus (nom/cur/cible/origine/arch) | ✅ | `10` §4.1, `40` §3 |
+| Distingue upgrade vs full/dist (maj/install/remove/held) | ✅ | `10` §4.1, `40` §3 |
+| Simulations parsées via `Inst`/`Conf`/`Remv`, log brut archivé | ✅ | `10` §1/§4.1 |
+| Statut `ok/updates_available/warning/error`, warning si remove/held | ✅ | `10` §4.1, `40` §3 |
+| Sources APT citées | ✅ | `10` §1 |
+| Distingue `os_family` et `machine_kind` à l'ajout | ✅ | `60` §1/§6 |
+| Choix manuel OS (Debian/Ubuntu/Proxmox/RPi/autre) | ✅ | `60` §6 |
+| Choix manuel type (VM/physique/Proxmox/LXC/RPi/GPU-workstation) | ✅ | `60` §6 |
+| `machine_probe` détecte/corrige | ✅ | `60` §6 |
+| Scripts dépendent du couple OS/type | ✅ | `60` §5 |
+| Debian firmware vérifie contrib/non-free/non-free-firmware | ✅ | `60` §4 |
+| Proxmox = profil dédié | ✅ | `60` §2/§4 |
+| Scripts hardware/drivers/benchmark jamais par défaut, validation | ✅ | `60` §5 |
+| Templates APT attendus (update-analyze/upgrade/full-upgrade/autoremove/clean/reboot-check/reboot) | ✅ | `10` §2/§4 |
+| Politique non interactive (`noninteractive`, `-y`, confdef/confold) | ✅ | `10` §4.2, `50` §2 |
+| Justification confdef/confold | ✅ | `10` §4.2, `50` §2 |
+| Prompts traités comme risques de blocage | ✅ | `50` §2 |
+| Timeout inactivité/global → erreur contrôlée | ✅ | `50` §2 |
+| `human_interaction_required` prévu | ✅ | `50` §2 |
+| Pas seulement exit code | ✅ | `50` §1, `10` §4.2 |
+| dpkg-query before/after | ✅ | `10` §4.2 |
+| Diff backend (maj/install/remove/inchangé/versions/anomalies) | ✅ | `40` §4, `90` Q4 |
+| `ExecutionResult.apt` (planned/applied/installed/removed/held/errors/reboot) | ✅ | `40` §4 |
+| Rapport MD résume diff + réf log | ✅ | `70` §4, `40` §8 |
+| Reboot vérifié (boot_id avant/après, attente, reconnexion) | ✅ | `10` §4.5, `40` §4 |
+| Reboot ok si revient ET boot_id changé | ✅ | `10` §4.5 |
+| `RebootResult` (beforeBootId…status/errors) | ✅ | `40` §4 |
+| Délai adaptatif par machine | ✅ | `10` §4.5, `40` §4 |
+| Statuts d'échec reboot distingués | ✅ | `40` §4 (`RebootResult.status`) |
+| Reboot = action validée ; Hermes ne déclenche pas | ✅ | `70` §2 |
+| `apt_update_analyze` alimente snapshot + tuile | ✅ | `10` §6, `20` §6 |
+| Actions via même route + table `executions` | ✅ | `20` §6, `10` §6 |
+| UI avant exécution (paquets/suppressions/held/reboot/risque) | ✅ | `70` §2, `40` §3 (renvoi tâche 3 pour le rendu) |
+| UI après exécution (réussite/diff/reboot/rapport/log) | ✅ | `70` §4 (renvoi tâche 3) |
+| Confirmation UI pour dist/full/autoremove/reboot | ✅ | `70` §2 |
+| Nouveaux champs/actions rétrocompatibles | ✅ | `40` §1/§2 |
+
+## §8 Focus post-install
+
+| Case | État | Renvoi |
+|---|---|---|
+| Interdit questions interactives SSH → champs formulaire | ✅ | `30` §1 |
+| Profils cochables dépliant leurs champs | ✅ | `30` §1/§3 |
+| Manifeste (`id`/`label`/`description`/`fields`/défauts/validations/preview/risk/confirmations) | ✅ | `30` §1/§3 |
+| Bouton désactivé si champs invalides | ✅ | `30` §1 |
+| Preview avec masquage secrets + signalement réseau/reboot | ✅ | `30` §1, `70` §1 |
+| Échec structuré si décision manquante | ✅ | `30` §1/§4 |
+| Profils attendus (bootstrap_root/identity_network/base_tools/network_tools/dev_git/sharing/docker_official/vm_guest_tools + optionnels) | ✅ | `30` §2 |
+| Champs `identity_network` | ✅ | `30` §3 |
+| Champs `docker_official` | ✅ | `30` §3 |
+| Champs `sharing` | ✅ | `30` §3 |
+| Champs `vm_guest_tools` | ✅ | `30` §3 |
+| Champs préremplis modifiables | ✅ | `30` §3 |
+| Exemple de manifeste | ✅ | `30` §3 |
+| Templates custom attendus (bootstrap/identity/install-package-groups/docker-official/sharing/vm-guest-tools) | ✅ | `30` §4 |
+| Sources citées | ✅ | `30` §4 |
+| identity_network à risque (confirmation/preview/sauvegardes) | ✅ | `30` §4.2, `70` §2 |
+| Résultat JSON ancien/nouveau endpoint + reconnectHost | ✅ | `40` §4 (`PostInstallResult.networkChange`), `30` §4.2 |
+| Pas de coupure sans stratégie reconnexion ; reboot via reboot_verified | ✅ | `30` §4.2 |
+| Webapp vérifie reconnexion + met à jour machine | ✅ | `30` §4.2 |
+| Erreurs réseau distinguées (6 codes) | ✅ | `50` §4 |
+| `ExecutionResult.postInstall` rétrocompatible | ✅ | `40` §4 |
+| Résultat liste profils/variables non sensibles/fichiers/paquets/services/reboots/erreurs | ✅ | `40` §4 |
+| Secrets jamais inclus | ✅ | `30` §5, `70` §1 |
+| Changements réseau/Docker marqués dans rapport MD | ✅ | `30` §5, `70` §4 |
+| Même mécanique (templates/preview/SSH/WS/executions/rapport/log) | ✅ | `30` §6 |
+| Valeurs réutilisables stockées (où) | ✅ | `30` §6 (`script_variables_presets`/`machine_profile_state`) |
+| Hermes propose/explique, JSON réduit, pas de déclenchement risqué | ✅ | `30` §6, `70` §2/§3 |
+| Profils découpés en sous-jalons indépendants | ✅ | `80` SJ-8/SJ-9 |
+
+---
+
+## Réserves résiduelles (⚠️)
+
+1. **Non-régression build/tests (§4)** : non exécutée dans cette mission de design (aucun code touché, par consigne). L'orchestrateur doit lancer `pnpm check/test/build` pour confirmer 0 régression — attendu vert puisque aucune modification de code.
+2. **Rendu UI fin (§7/§8 « UI avant/après »)** : le design pose les données et les exigences ; le rendu visuel exact relève de la **tâche 3**. Couvert au niveau contrat/exigence, pas au niveau JSX.
+3. **Détails Mustache vs Go-templates Docker** : les `{{ }}` de `docker inspect --format` entrent en conflit avec Mustache ; le pseudo-shell le signale (échappement) — choix de délimiteurs à figer en implémentation (SJ-4).
+
+Aucune réserve bloquante identifiée. Verdict visé : **✅ Accepté**.
diff --git a/docs/superpowers/plans/2026-06-05-jalon2-polish-design-system.md b/docs/superpowers/plans/2026-06-05-jalon2-polish-design-system.md
index c5f99a3..f960dc6 100644
--- a/docs/superpowers/plans/2026-06-05-jalon2-polish-design-system.md
+++ b/docs/superpowers/plans/2026-06-05-jalon2-polish-design-system.md
@@ -1,5 +1,7 @@
# Jalon 2 — Polish design system — Implementation Plan
+> **⚠️ STATUT (2026-06-05) : ABSORBÉ PAR LA TÂCHE 3.** La roadmap `liste_taches.md` / `coherence_taches.md` regroupe tout le frontend (layout, tuiles, volet Hermes, terminal, paramètres, thème, status bar, icônes) dans la **tâche 3 (design frontend)**, gate `validation_tache3.md`. Ce plan jalon-2 reste valide comme **matériau d'implémentation du polish** : le wiring DS (exports ESM + Font Awesome + polices, Tasks 1-4) est **déjà commité** et acquis ; les Tasks 5-12 (Header, StatusBar, refonte MachineTile/AddMachineModal/TerminalPanel/Dashboard/App) seront **implémentées plus tard dans le cadre de la tâche 3**, après validation de son design. Ne pas exécuter ce plan isolément.
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Refondre l'UI existante avec les composants du design system Gruvbox (Button, IconButton, StatusLed, Popup), brancher Font Awesome + les polices en offline, ajouter un header (titre + ajout + bascule thème) et une status bar tmux, et rendre le terminal non ambigu entre machines.
diff --git a/docs/superpowers/plans/2026-06-05-tache1.9-phase1-schema-socle.md b/docs/superpowers/plans/2026-06-05-tache1.9-phase1-schema-socle.md
new file mode 100644
index 0000000..144aa6d
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-05-tache1.9-phase1-schema-socle.md
@@ -0,0 +1,659 @@
+# Tâche 1.9 — Phase 1 (schéma BDD socle) — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Implémenter la Phase 1 du schéma BDD cible (`tache1.9.md §14`) : étendre `machines/snapshots/executions`, créer les tables socle (`machine_state`, `machine_hardware`, `machine_metrics_latest`, `machine_events`, `important_messages`, `reports`, `raw_artifacts`), et alimenter l'état dérivé + la timeline lors des refresh/exécutions existants.
+
+**Architecture:** Extension additive (rétro-compatible) du schéma Drizzle/SQLite. Migration générée par drizzle-kit. Un service `machineState` dérive l'état courant d'une machine depuis un snapshot/exécution et l'« upsert » dans `machine_state` ; `refreshMachine` et `runAction` (existants) sont enrichis pour peupler `machine_state`, `machine_events`, et (pour les exécutions) `reports` + `raw_artifacts`, ainsi que les nouveaux champs `kind/schema_version/important_json` des snapshots/exécutions. **Aucune modification de l'API ni du frontend** (réservé tâches 3/5).
+
+**Tech Stack:** Drizzle ORM, better-sqlite3, drizzle-kit, vitest.
+
+---
+
+## Contexte & invariants
+- État actuel : `server/db/schema.ts` contient `machines`, `snapshots`, `executions` (jalon 1, en prod). Voir le fichier.
+- **Rétro-compatibilité stricte** : on AJOUTE des colonnes/tables ; on ne renomme ni ne supprime rien. En particulier on conserve `snapshots.checked_at` (le design tache1.9 le nomme `created_at`, mais le code jalon 1 `refresh.ts` utilise `checkedAt` — on ne casse pas).
+- Les nouveaux champs sont nullable ou ont une valeur par défaut, pour que les lignes du jalon 1 restent valides.
+- `payload_json` / `result_json` restent la vérité canonique ; `machine_state` n'est qu'un cache dérivé pour l'UI (jamais source de vérité métier).
+- Pas de FK vers des tables non encore créées (jobs/action_requests/schedules = phases ultérieures) : `running_job_id`, `request_id`, `job_id` sont de simples colonnes `text` nullable.
+- Ne pas committer (l'utilisateur gère les commits en fin de parcours). Les étapes « Commit » du template sont **remplacées par une vérification** ; ne PAS exécuter `git commit`.
+
+> **Note exécution** : ce plan se construit sur l'état courant du working tree (qui contient du WIP non commité : feature `capabilities`, scaffold Rust). Ne pas annuler ce WIP. Les fichiers touchés ici (`server/db/*`, `server/services/*`) ne chevauchent pas le WIP `capabilities`/frontend.
+
+---
+
+## File Structure
+
+```
+server/db/
+├─ schema.ts # MODIF : +colonnes machines/snapshots/executions, +7 tables
+├─ migrations/ # +1 migration générée (drizzle-kit)
+└─ schema.test.ts # NOUVEAU : test que la migration applique le schéma cible
+server/services/
+├─ machineState.ts # NOUVEAU : dériver + upsert machine_state, insert events
+├─ machineState.test.ts # NOUVEAU : tests purs de dérivation
+├─ refresh.ts # MODIF : peupler snapshot.kind/schema_version/important_json + machine_state + event
+└─ execute.ts # MODIF : champs executions + machine_state + event + reports + raw_artifacts
+```
+
+---
+
+## Task 1 : Étendre le schéma Drizzle + migration
+
+**Files:**
+- Modify: `server/db/schema.ts`
+- Create: migration sous `server/db/migrations/` (générée)
+- Create: `server/db/schema.test.ts`
+
+- [ ] **Step 1 : Remplacer le contenu de `server/db/schema.ts`**
+
+> **⚠️ Tree déplacé** : `schema.ts` contient déjà la table `apiClients` (WIP api_clients, migration `0001_api_clients.sql`). Le contenu ci-dessous **préserve `apiClients`** (et l'import `uniqueIndex`). NE PAS supprimer `apiClients`. La migration générée à l'étape suivante sera donc `0002_*` (et non `0001`).
+
+```ts
+// server/db/schema.ts
+import { sqliteTable, text, integer, real, uniqueIndex } from "drizzle-orm/sqlite-core";
+
+export const machines = sqliteTable("machines", {
+ id: text("id").primaryKey(),
+ name: text("name").notNull(),
+ hostname: text("hostname").notNull(),
+ port: integer("port").notNull().default(22),
+ osFamily: text("os_family").notNull().default("unknown"),
+ osVersion: text("os_version"),
+ osCodename: text("os_codename"),
+ arch: text("arch"),
+ machineKind: text("machine_kind"), // physical | vm | proxmox_host | lxc | raspberry_pi | workstation | unknown
+ virtualization: text("virtualization"), // none | qemu | kvm | lxc | docker | vmware | ...
+ hardwareProfile: text("hardware_profile"), // generic_vm | baremetal_server | raspberry_pi | gpu_server | proxmox_host | ...
+ username: text("username").notNull(),
+ encPassword: text("enc_password").notNull(),
+ encSudoPassword: text("enc_sudo_password"),
+ aptProxyMode: text("apt_proxy_mode").notNull().default("direct"),
+ aptProxyUrl: text("apt_proxy_url"),
+ status: text("status").notNull().default("unknown"),
+ lastCheckedAt: text("last_checked_at"),
+ lastSeenAt: text("last_seen_at"),
+ createdAt: text("created_at").notNull(),
+ updatedAt: text("updated_at"),
+ deletedAt: text("deleted_at"),
+});
+
+export const snapshots = sqliteTable("snapshots", {
+ id: text("id").primaryKey(),
+ machineId: text("machine_id").notNull().references(() => machines.id, { onDelete: "cascade" }),
+ kind: text("kind").notNull().default("apt_update_analyze"),
+ schemaVersion: integer("schema_version").notNull().default(1),
+ checkedAt: text("checked_at").notNull(),
+ status: text("status").notNull(),
+ payloadJson: text("payload_json").notNull(),
+ importantJson: text("important_json"),
+ rawLogPath: text("raw_log_path"),
+ rawArtifactId: text("raw_artifact_id"),
+ sourceJobId: text("source_job_id"),
+});
+
+export const executions = sqliteTable("executions", {
+ id: text("id").primaryKey(),
+ machineId: text("machine_id").notNull().references(() => machines.id, { onDelete: "cascade" }),
+ action: text("action").notNull(),
+ mode: text("mode").notNull().default("manual"),
+ schemaVersion: integer("schema_version").notNull().default(1),
+ startedAt: text("started_at").notNull(),
+ finishedAt: text("finished_at"),
+ status: text("status").notNull(),
+ requestId: text("request_id"),
+ jobId: text("job_id"),
+ resultJson: text("result_json"),
+ importantJson: text("important_json"),
+ reportPath: text("report_path"),
+ rawLogPath: text("raw_log_path"),
+ reportId: text("report_id"),
+ exitCode: integer("exit_code"),
+ errorKind: text("error_kind"),
+ errorMessage: text("error_message"),
+});
+
+export const machineState = sqliteTable("machine_state", {
+ machineId: text("machine_id").primaryKey().references(() => machines.id, { onDelete: "cascade" }),
+ status: text("status").notNull(),
+ aptStatus: text("apt_status"),
+ aptUpdatesCount: integer("apt_updates_count").notNull().default(0),
+ aptRebootRequired: integer("apt_reboot_required").notNull().default(0),
+ aptLastAnalyzeAt: text("apt_last_analyze_at"),
+ dockerStatus: text("docker_status"),
+ dockerInstalled: integer("docker_installed").notNull().default(0),
+ dockerStacksCount: integer("docker_stacks_count").notNull().default(0),
+ dockerUpdatesCount: integer("docker_updates_count").notNull().default(0),
+ dockerPruneAvailable: integer("docker_prune_available").notNull().default(0),
+ postInstallStatus: text("post_install_status"),
+ metricsLastCollectedAt: text("metrics_last_collected_at"),
+ cpuLoad1: real("cpu_load1"),
+ memoryUsedPercent: real("memory_used_percent"),
+ rootUsedPercent: real("root_used_percent"),
+ diskWarningsCount: integer("disk_warnings_count").notNull().default(0),
+ hardwareWarningsCount: integer("hardware_warnings_count").notNull().default(0),
+ runningJobId: text("running_job_id"),
+ lastErrorKind: text("last_error_kind"),
+ lastErrorMessage: text("last_error_message"),
+ updatedAt: text("updated_at").notNull(),
+});
+
+export const machineHardware = sqliteTable("machine_hardware", {
+ machineId: text("machine_id").primaryKey().references(() => machines.id, { onDelete: "cascade" }),
+ probeSnapshotId: text("probe_snapshot_id"),
+ cpuModel: text("cpu_model"),
+ cpuCores: integer("cpu_cores"),
+ memoryBytes: integer("memory_bytes"),
+ gpusJson: text("gpus_json"),
+ disksJson: text("disks_json"),
+ networkJson: text("network_json"),
+ firmwareJson: text("firmware_json"),
+ driverJson: text("driver_json"),
+ warningsJson: text("warnings_json"),
+ updatedAt: text("updated_at").notNull(),
+});
+
+export const machineMetricsLatest = sqliteTable("machine_metrics_latest", {
+ machineId: text("machine_id").primaryKey().references(() => machines.id, { onDelete: "cascade" }),
+ snapshotId: text("snapshot_id"),
+ collectedAt: text("collected_at").notNull(),
+ cpuLoad1: real("cpu_load1"),
+ cpuLoad5: real("cpu_load5"),
+ cpuCores: integer("cpu_cores"),
+ memoryTotalBytes: integer("memory_total_bytes"),
+ memoryUsedBytes: integer("memory_used_bytes"),
+ memoryAvailableBytes: integer("memory_available_bytes"),
+ memoryUsedPercent: real("memory_used_percent"),
+ filesystemsJson: text("filesystems_json"),
+ rootUsedPercent: real("root_used_percent"),
+ warningsJson: text("warnings_json"),
+});
+
+export const machineEvents = sqliteTable("machine_events", {
+ id: text("id").primaryKey(),
+ machineId: text("machine_id").references(() => machines.id, { onDelete: "cascade" }),
+ eventType: text("event_type").notNull(),
+ severity: text("severity").notNull(), // info | warning | error
+ createdAt: text("created_at").notNull(),
+ actorType: text("actor_type"), // user | system | schedule | hermes
+ actorId: text("actor_id"),
+ snapshotId: text("snapshot_id"),
+ executionId: text("execution_id"),
+ jobId: text("job_id"),
+ message: text("message"),
+ payloadJson: text("payload_json"),
+});
+
+export const importantMessages = sqliteTable("important_messages", {
+ id: text("id").primaryKey(),
+ machineId: text("machine_id").references(() => machines.id, { onDelete: "cascade" }),
+ source: text("source").notNull(), // apt | docker | post_install | ssh | system
+ category: text("category").notNull(), // error | warning | future_major_change | ...
+ severity: text("severity").notNull(),
+ packageName: text("package_name"),
+ component: text("component"),
+ message: text("message").notNull(),
+ rawLineRef: text("raw_line_ref"),
+ snapshotId: text("snapshot_id"),
+ executionId: text("execution_id"),
+ firstSeenAt: text("first_seen_at").notNull(),
+ lastSeenAt: text("last_seen_at").notNull(),
+ acknowledged: integer("acknowledged").notNull().default(0),
+ acknowledgedAt: text("acknowledged_at"),
+ acknowledgedBy: text("acknowledged_by"),
+ payloadJson: text("payload_json"),
+});
+
+export const reports = sqliteTable("reports", {
+ id: text("id").primaryKey(),
+ machineId: text("machine_id").references(() => machines.id, { onDelete: "cascade" }),
+ executionId: text("execution_id"),
+ kind: text("kind").notNull(), // machine | global | cleanup | hermes
+ title: text("title").notNull(),
+ path: text("path").notNull(),
+ createdAt: text("created_at").notNull(),
+ pinned: integer("pinned").notNull().default(0),
+ summaryJson: text("summary_json"),
+});
+
+export const rawArtifacts = sqliteTable("raw_artifacts", {
+ id: text("id").primaryKey(),
+ machineId: text("machine_id").references(() => machines.id, { onDelete: "cascade" }),
+ kind: text("kind").notNull(), // raw_log | rendered_template | export | screenshot
+ path: text("path").notNull(),
+ bytes: integer("bytes"),
+ sha256: text("sha256"),
+ createdAt: text("created_at").notNull(),
+ expiresAt: text("expires_at"),
+ pinned: integer("pinned").notNull().default(0),
+ redacted: integer("redacted").notNull().default(1),
+ retentionPolicy: text("retention_policy"), // default | failed | pinned | short
+ deletedAt: text("deleted_at"),
+ deleteReason: text("delete_reason"),
+ metadataJson: text("metadata_json"),
+});
+
+// --- Préexistant (WIP api_clients) : NE PAS supprimer ---
+export const apiClients = sqliteTable(
+ "api_clients",
+ {
+ id: text("id").primaryKey(),
+ name: text("name").notNull(),
+ tokenPrefix: text("token_prefix").notNull(),
+ tokenHash: text("token_hash").notNull(),
+ scopesJson: text("scopes_json").notNull(),
+ createdAt: text("created_at").notNull(),
+ lastUsedAt: text("last_used_at"),
+ revokedAt: text("revoked_at"),
+ },
+ (table) => ({
+ tokenHashIdx: uniqueIndex("api_clients_token_hash_unique").on(table.tokenHash),
+ }),
+);
+```
+
+> Avant d'écrire : relire l'état RÉEL de `server/db/schema.ts` (`rtk read server/db/schema.ts`). Si `apiClients` y a évolué (colonnes différentes), reprendre sa définition à l'identique plutôt que celle ci-dessus, pour ne pas régresser le WIP. N'ajouter que les colonnes étendues + les 7 nouvelles tables.
+
+- [ ] **Step 2 : Générer la migration**
+
+Run: `rtk pnpm db:generate`
+Expected: un nouveau fichier `server/db/migrations/0002_*.sql` est créé (ALTER TABLE machines/snapshots/executions + CREATE TABLE des 7 nouvelles tables). La migration `0001_api_clients.sql` reste intacte. Aucune erreur drizzle-kit, aucun DROP de `api_clients`.
+
+- [ ] **Step 3 : Écrire le test de migration `server/db/schema.test.ts`**
+
+```ts
+// server/db/schema.test.ts
+import { describe, it, expect } from "vitest";
+import Database from "better-sqlite3";
+import { drizzle } from "drizzle-orm/better-sqlite3";
+import { migrate } from "drizzle-orm/better-sqlite3/migrator";
+
+function freshMigratedDb() {
+ const sqlite = new Database(":memory:");
+ const db = drizzle(sqlite);
+ migrate(db, { migrationsFolder: "./server/db/migrations" });
+ return sqlite;
+}
+
+function tableNames(sqlite: Database.Database): string[] {
+ return sqlite.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((r: any) => r.name);
+}
+function columnNames(sqlite: Database.Database, table: string): string[] {
+ return sqlite.prepare(`PRAGMA table_info(${table})`).all().map((r: any) => r.name);
+}
+
+describe("schéma Phase 1", () => {
+ it("crée les tables socle", () => {
+ const sqlite = freshMigratedDb();
+ const tables = tableNames(sqlite);
+ for (const t of [
+ "machines", "snapshots", "executions",
+ "machine_state", "machine_hardware", "machine_metrics_latest",
+ "machine_events", "important_messages", "reports", "raw_artifacts",
+ ]) {
+ expect(tables, `table ${t}`).toContain(t);
+ }
+ });
+
+ it("ajoute les colonnes étendues sans casser l'existant", () => {
+ const sqlite = freshMigratedDb();
+ expect(columnNames(sqlite, "machines")).toEqual(
+ expect.arrayContaining(["machine_kind", "virtualization", "hardware_profile", "os_version", "updated_at"]),
+ );
+ expect(columnNames(sqlite, "snapshots")).toEqual(
+ expect.arrayContaining(["kind", "schema_version", "important_json"]),
+ );
+ expect(columnNames(sqlite, "executions")).toEqual(
+ expect.arrayContaining(["schema_version", "error_kind", "error_message", "exit_code"]),
+ );
+ // colonnes jalon 1 conservées
+ expect(columnNames(sqlite, "snapshots")).toContain("checked_at");
+ expect(columnNames(sqlite, "machines")).toContain("enc_password");
+ });
+});
+```
+
+- [ ] **Step 4 : Lancer le test**
+
+Run: `rtk pnpm vitest run server/db/schema.test.ts`
+Expected: PASS (2 tests). Si une colonne attendue manque, corriger `schema.ts` et régénérer la migration (`rtk pnpm db:generate`) — ne pas modifier le test.
+
+- [ ] **Step 5 : Vérifier la compilation + non-régression**
+
+Run: `rtk pnpm check && rtk pnpm test`
+Expected: 0 erreur TS ; toute la suite verte (les tests existants ne doivent pas casser).
+
+- [ ] **Step 6 : (pas de commit — vérification seulement)**
+
+Vérifier `git status` : seuls `server/db/schema.ts`, `server/db/migrations/0001_*.sql`, `server/db/schema.test.ts` ajoutés à la liste des modifs. Ne PAS committer.
+
+---
+
+## Task 2 : Service `machineState` (dérivation + upsert + events)
+
+**Files:**
+- Create: `server/services/machineState.ts`, `server/services/machineState.test.ts`
+
+- [ ] **Step 1 : Écrire le test (échec attendu)**
+
+```ts
+// server/services/machineState.test.ts
+import { describe, it, expect } from "vitest";
+import { deriveAptState } from "./machineState.js";
+import type { UpdateSnapshot } from "@shared/types.js";
+
+const snap: UpdateSnapshot = {
+ machineId: "m1", hostname: "h", os: { family: "debian", version: "12" },
+ checkedAt: "2026-06-05T10:00:00Z", status: "updates_available",
+ apt: { enabled: true, count: 3, rebootRequired: true, packages: [] },
+};
+
+describe("deriveAptState", () => {
+ it("dérive le bloc APT de machine_state depuis un snapshot", () => {
+ expect(deriveAptState(snap)).toEqual({
+ status: "updates_available",
+ aptStatus: "updates_available",
+ aptUpdatesCount: 3,
+ aptRebootRequired: 1,
+ aptLastAnalyzeAt: "2026-06-05T10:00:00Z",
+ });
+ });
+
+ it("met rebootRequired à 0 quand absent", () => {
+ const s = { ...snap, status: "ok" as const, apt: { ...snap.apt, count: 0, rebootRequired: false } };
+ expect(deriveAptState(s)).toMatchObject({ aptUpdatesCount: 0, aptRebootRequired: 0, status: "ok" });
+ });
+});
+```
+
+- [ ] **Step 2 : Lancer (échec)**
+
+Run: `rtk pnpm vitest run server/services/machineState.test.ts`
+Expected: FAIL — module introuvable.
+
+- [ ] **Step 3 : Implémenter `server/services/machineState.ts`**
+
+```ts
+// server/services/machineState.ts
+import { randomUUID } from "node:crypto";
+import { sql } from "drizzle-orm";
+import { db, schema } from "../db/client.js";
+import type { UpdateSnapshot } from "@shared/types.js";
+
+export interface AptDerivedState {
+ status: string;
+ aptStatus: string;
+ aptUpdatesCount: number;
+ aptRebootRequired: number;
+ aptLastAnalyzeAt: string;
+}
+
+/** Dérive le bloc APT de l'état courant depuis un snapshot (fonction pure). */
+export function deriveAptState(snapshot: UpdateSnapshot): AptDerivedState {
+ return {
+ status: snapshot.status,
+ aptStatus: snapshot.status,
+ aptUpdatesCount: snapshot.apt.count,
+ aptRebootRequired: snapshot.apt.rebootRequired ? 1 : 0,
+ aptLastAnalyzeAt: snapshot.checkedAt,
+ };
+}
+
+type MachineStateInsert = typeof schema.machineState.$inferInsert;
+
+/** Insère ou met à jour les champs fournis de machine_state pour une machine. */
+export function upsertMachineState(
+ machineId: string,
+ fields: Partial> & { status: string },
+): void {
+ const now = new Date().toISOString();
+ db.insert(schema.machineState)
+ .values({ machineId, updatedAt: now, ...fields })
+ .onConflictDoUpdate({
+ target: schema.machineState.machineId,
+ set: { ...fields, updatedAt: now },
+ })
+ .run();
+}
+
+/** Ajoute une ligne à la timeline machine_events. */
+export function recordEvent(input: {
+ machineId: string;
+ eventType: string;
+ severity: "info" | "warning" | "error";
+ actorType?: string;
+ snapshotId?: string;
+ executionId?: string;
+ message?: string;
+}): void {
+ db.insert(schema.machineEvents).values({
+ id: randomUUID(),
+ machineId: input.machineId,
+ eventType: input.eventType,
+ severity: input.severity,
+ createdAt: new Date().toISOString(),
+ actorType: input.actorType ?? "system",
+ snapshotId: input.snapshotId,
+ executionId: input.executionId,
+ message: input.message,
+ }).run();
+}
+
+/** Utilitaire interne réservé aux migrations/tests éventuels. */
+export const _internal = { sql };
+```
+
+> Note : `_internal` exporte `sql` pour rester explicite sur l'import drizzle ; supprime-le si lint le signale inutilisé et retire l'import `sql` correspondant.
+
+- [ ] **Step 4 : Lancer (succès)**
+
+Run: `rtk pnpm vitest run server/services/machineState.test.ts`
+Expected: PASS (2 tests). `deriveAptState` est pure et n'importe pas de DB au moment du test — mais le module importe `../db/client.js`. Si l'import de `db/client` fait échouer le test en environnement node (chargement better-sqlite3), refactorer en isolant la fonction pure dans le même fichier sans exécuter de requête à l'import (c'est déjà le cas : aucune requête n'est lancée à l'import). Le test n'appelle que `deriveAptState`.
+
+- [ ] **Step 5 : Vérifier**
+
+Run: `rtk pnpm check`
+Expected: 0 erreur.
+
+- [ ] **Step 6 : (pas de commit)**
+
+---
+
+## Task 3 : Peupler l'état + la timeline dans `refreshMachine`
+
+**Files:**
+- Modify: `server/services/refresh.ts`
+
+- [ ] **Step 1 : Lire l'état actuel de `refresh.ts`**
+
+Run: `rtk read server/services/refresh.ts`
+Repère : la construction de `snapshot`, l'`insert` dans `schema.snapshots`, et l'`update` de `machines.status`.
+
+- [ ] **Step 2 : Enrichir l'insertion du snapshot (kind/schema_version/important_json)**
+
+Dans `refreshMachine`, remplacer l'insertion actuelle du snapshot par :
+
+```ts
+ const snapshotId = randomUUID();
+ db.insert(schema.snapshots).values({
+ id: snapshotId,
+ machineId,
+ kind: "apt_update_analyze",
+ schemaVersion: 1,
+ checkedAt,
+ status,
+ payloadJson: JSON.stringify(snapshot),
+ importantJson: JSON.stringify(snapshot.rawHints?.logImportantLines ?? []),
+ }).run();
+```
+
+(Le `randomUUID` est déjà importé dans `refresh.ts`.)
+
+- [ ] **Step 3 : Mettre à jour `machine_state` + event après le snapshot**
+
+Ajouter, juste après l'`update` de `machines` (status/lastCheckedAt), et après avoir importé en tête de fichier
+`import { deriveAptState, upsertMachineState, recordEvent } from "./machineState.js";` :
+
+```ts
+ upsertMachineState(machineId, deriveAptState(snapshot));
+ recordEvent({
+ machineId,
+ eventType: "apt_refresh",
+ severity: status === "error" ? "error" : "info",
+ snapshotId,
+ message: `Refresh APT : ${snapshot.apt.count} mise(s) à jour`,
+ });
+```
+
+- [ ] **Step 4 : Vérifier compilation + tests**
+
+Run: `rtk pnpm check && rtk pnpm vitest run server/services/refresh.test.ts`
+Expected: 0 erreur TS ; le test existant `extractSection` reste vert (il n'importe pas la DB grâce au mock en place).
+
+- [ ] **Step 5 : (pas de commit)**
+
+---
+
+## Task 4 : Enrichir `runAction` (champs executions + state + event + reports + raw_artifacts)
+
+**Files:**
+- Modify: `server/services/execute.ts`
+
+- [ ] **Step 1 : Lire l'état actuel de `execute.ts`**
+
+Run: `rtk read server/services/execute.ts`
+Repère : l'`insert` initial dans `executions`, le bloc d'archivage (`writeFileSync` du log + rapport), l'`update` final de `executions` et de `machines`.
+
+- [ ] **Step 2 : Importer les helpers**
+
+En tête de `execute.ts`, ajouter :
+```ts
+import { randomUUID } from "node:crypto"; // déjà présent — ne pas dupliquer
+import { statSync } from "node:fs";
+import { upsertMachineState, recordEvent } from "./machineState.js";
+```
+(Si `randomUUID` est déjà importé, n'ajouter que `statSync` et la ligne `machineState`.)
+
+- [ ] **Step 3 : Mettre `running_job_id`/status dans machine_state au démarrage**
+
+Juste après l'`update` initial de `machines` en `status: "running"` et l'`insert` de l'exécution (status `running`), ajouter :
+```ts
+ upsertMachineState(machineId, { status: "running", runningJobId: executionId });
+```
+
+- [ ] **Step 4 : Enrichir l'`update` final de l'exécution**
+
+Remplacer l'`update` final de `schema.executions` par (ajout `schemaVersion`, `importantJson`, `exitCode`, `errorKind`, `errorMessage`, `reportId`) :
+
+```ts
+ const reportId = randomUUID();
+ const exitMatch = /===SU:EXIT=(\d+)===/.exec(raw);
+ db.update(schema.executions).set({
+ finishedAt,
+ status,
+ schemaVersion: 1,
+ resultJson: JSON.stringify(result),
+ importantJson: JSON.stringify(result.importantLogLines),
+ reportPath,
+ rawLogPath,
+ reportId,
+ exitCode: exitMatch ? Number(exitMatch[1]) : null,
+ errorKind: status === "error" ? "execution_failed" : null,
+ errorMessage: status === "error" ? (result.importantLogLines.at(-1) ?? null) : null,
+ }).where(eq(schema.executions.id, executionId)).run();
+```
+
+- [ ] **Step 5 : Insérer `reports` + `raw_artifacts` + state + event**
+
+Juste après l'`update` final de `machines`, ajouter :
+```ts
+ db.insert(schema.reports).values({
+ id: reportId,
+ machineId,
+ executionId,
+ kind: "machine",
+ title: `${m.name} — ${action}`,
+ path: reportPath,
+ createdAt: finishedAt,
+ }).run();
+
+ db.insert(schema.rawArtifacts).values({
+ id: randomUUID(),
+ machineId,
+ kind: "raw_log",
+ path: rawLogPath,
+ bytes: statSync(rawLogPath).size,
+ createdAt: finishedAt,
+ retentionPolicy: status === "error" ? "failed" : "default",
+ }).run();
+
+ upsertMachineState(machineId, {
+ status: status === "error" ? "error" : "unknown",
+ runningJobId: null,
+ lastErrorKind: status === "error" ? "execution_failed" : null,
+ lastErrorMessage: status === "error" ? (result.importantLogLines.at(-1) ?? null) : null,
+ });
+
+ recordEvent({
+ machineId,
+ eventType: `action_${action}`,
+ severity: status === "error" ? "error" : status === "warning" ? "warning" : "info",
+ executionId,
+ message: `Action ${action} : ${status}`,
+ });
+```
+
+- [ ] **Step 6 : Vérifier compilation + tests**
+
+Run: `rtk pnpm check && rtk pnpm test`
+Expected: 0 erreur TS ; suite complète verte.
+
+- [ ] **Step 7 : (pas de commit)**
+
+---
+
+## Task 5 : Vérification finale Phase 1
+
+**Files:** aucun (vérification).
+
+- [ ] **Step 1 : Suite + build**
+
+Run: `rtk pnpm check && rtk pnpm test && rtk pnpm build`
+Expected: 0 erreur TS ; tests verts (jalon 1 + schema migration + machineState + helpers existants) ; `dist/index.js` + `dist/client` produits.
+
+- [ ] **Step 2 : Démarrage runtime + migration appliquée**
+
+Run (clé jetable, DB jetable) :
+```bash
+export SU_MASTER_KEY=$(openssl rand -hex 32) SU_DB_PATH=./data/phase1-check.db SU_REPORTS_DIR=./data/phase1-reports
+node dist/index.js > ./data/phase1.log 2>&1 &
+sleep 3
+curl -s localhost:8787/health
+sqlite3 ./data/phase1-check.db ".tables" 2>/dev/null || echo "(sqlite3 absent — vérifier via le test de migration)"
+kill %1 2>/dev/null
+rm -rf ./data/phase1-check.db* ./data/phase1-reports ./data/phase1.log
+```
+Expected: `{"ok":true}` et la liste des tables inclut `machine_state`, `machine_events`, `reports`, `raw_artifacts`, etc. (migration appliquée au boot via `runMigrations()`).
+
+- [ ] **Step 3 : Synthèse à l'utilisateur**
+
+Reporter : tables/colonnes ajoutées, `machine_state`/`machine_events`/`reports`/`raw_artifacts` peuplés lors des refresh/exécutions, non-régression confirmée. **Ne pas committer** (l'utilisateur gère les commits en fin de parcours).
+
+---
+
+## Self-Review (couverture tache1.9 §14 Phase 1)
+
+- machine_state → Task 1 (table) + Task 2/3/4 (peuplement). ✓
+- machine_kind/virtualization/hardware_profile dans machines → Task 1. ✓
+- machine_hardware → Task 1 (table ; producteur = tâche 4, hors Phase 1). ✓
+- machine_metrics_latest → Task 1 (table ; producteur = tâche 4). ✓
+- machine_events → Task 1 + Task 3/4 (peuplement). ✓
+- important_messages → Task 1 (table ; peuplement fin = tâche 5/7, l'`important_json` du snapshot/exécution est déjà capturé). ✓
+- reports → Task 1 + Task 4 (peuplement depuis le rapport déjà écrit). ✓
+- raw_artifacts → Task 1 + Task 4 (peuplement depuis le log déjà écrit). ✓
+- snapshots.kind/schema_version/important_json → Task 1 + Task 3. ✓
+- executions.schema_version/important_json/error_kind/error_message → Task 1 + Task 4. ✓
+
+Décision assumée (rétro-compat) : `snapshots.checked_at` conservé (non renommé en `created_at`) pour ne pas casser `refresh.ts`. Tables sans producteur en Phase 1 (`machine_hardware`, `machine_metrics_latest`, `important_messages`) créées vides, alimentées aux tâches 4/5/7 — conforme au principe « migration progressive ».
+
+Pas de placeholder. Noms cohérents : `deriveAptState`/`upsertMachineState`/`recordEvent` définis Task 2 et utilisés Tasks 3-4 ; `reportId` défini Task 4 Step 4 et réutilisé Step 5.
diff --git a/docs/superpowers/plans/2026-06-05-tache1.9-phase2-credentials.md b/docs/superpowers/plans/2026-06-05-tache1.9-phase2-credentials.md
new file mode 100644
index 0000000..e371d04
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-05-tache1.9-phase2-credentials.md
@@ -0,0 +1,269 @@
+# Tâche 1.9 — Phase 2 (sécurité credentials) — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development ou superpowers:executing-plans. Étapes en checkbox.
+
+**Goal:** Isoler les secrets SSH dans une table dédiée `machine_credentials` (+ table `machine_host_keys`), de façon **non destructive** : nouvelle table, écriture dédiée, lecture prioritaire avec fallback sur `machines.enc_password`, et backfill des machines existantes.
+
+**Architecture:** Ajout additif (Drizzle/SQLite, migration `0003`). `machines.enc_password`/`enc_sudo_password` sont CONSERVÉS (non droppés) comme fallback/legacy. Un service `credentials` écrit/lit `machine_credentials` ; `createMachine` y insère, `getCreds` lit `machine_credentials` puis retombe sur les colonnes `machines` si absent ; un backfill (idempotent) crée les lignes manquantes au démarrage. `machine_host_keys` est créée (schéma) pour la future vérification host key (pas de logique de vérif en Phase 2).
+
+**Tech Stack:** Drizzle ORM, better-sqlite3, drizzle-kit, vitest.
+
+---
+
+## Invariants
+- **Non destructif** : ne pas dropper `machines.enc_password`/`enc_sudo_password` (NOT NULL conservé). Pas de perte des machines réelles existantes.
+- Secrets uniquement chiffrés (AES-256-GCM existant, `server/crypto/secrets.ts`). `machine_credentials` n'est JAMAIS exposée via l'API publique (la `MachineView` reste sans secret).
+- Rétro-compatibilité : une machine sans ligne `machine_credentials` reste utilisable (fallback). Le backfill comble le manque.
+- **Ne pas committer** (l'utilisateur gère les commits). Étapes « commit » remplacées par vérification.
+- Tree partagé avec du WIP concurrent : ne toucher QUE `server/db/schema.ts`, migrations, `server/services/credentials.ts` (+test), `server/services/machines.ts`, et le point de backfill (`server/db/migrate.ts` ou `server/index.ts`). Relire chaque fichier avant édition (drift possible).
+
+## File Structure
+```
+server/db/schema.ts # MODIF : +machine_credentials, +machine_host_keys
+server/db/migrations/0003_*.sql # généré
+server/services/credentials.ts # NOUVEAU : writeCredentials/readCreds/backfill
+server/services/credentials.test.ts # NOUVEAU
+server/services/machines.ts # MODIF : createMachine écrit credentials ; getCreds lit credentials+fallback
+server/db/migrate.ts # MODIF : appeler backfill après migrate
+```
+
+---
+
+## Task 1 : Tables `machine_credentials` + `machine_host_keys`
+
+**Files:** Modify `server/db/schema.ts` ; generate migration ; extend `server/db/schema.test.ts`.
+
+- [ ] **Step 1 : Relire le schéma réel**
+
+Run: `rtk read server/db/schema.ts` (capter l'état courant, préserver tout l'existant : machines/snapshots/executions/apiClients + les 7 tables Phase 1).
+
+- [ ] **Step 2 : Ajouter les deux tables** (à la fin de `schema.ts`, avant ou après `apiClients`, sans rien supprimer)
+
+```ts
+export const machineCredentials = sqliteTable("machine_credentials", {
+ machineId: text("machine_id").primaryKey().references(() => machines.id, { onDelete: "cascade" }),
+ authMethod: text("auth_method").notNull(), // password | ssh_key
+ encPassword: text("enc_password"),
+ encSudoPassword: text("enc_sudo_password"),
+ encPrivateKey: text("enc_private_key"),
+ encKeyPassphrase: text("enc_key_passphrase"),
+ sudoMode: text("sudo_mode").notNull(), // same_as_ssh | separate | none
+ createdAt: text("created_at").notNull(),
+ updatedAt: text("updated_at").notNull(),
+ lastTestAt: text("last_test_at"),
+ status: text("status"), // ok | error | unknown
+});
+
+export const machineHostKeys = sqliteTable("machine_host_keys", {
+ id: text("id").primaryKey(),
+ machineId: text("machine_id").references(() => machines.id, { onDelete: "cascade" }),
+ hostname: text("hostname").notNull(),
+ port: integer("port").notNull(),
+ keyType: text("key_type"),
+ fingerprintSha256: text("fingerprint_sha256").notNull(),
+ publicKey: text("public_key"),
+ status: text("status").notNull(), // approved | changed | rejected | unknown
+ firstSeenAt: text("first_seen_at").notNull(),
+ lastSeenAt: text("last_seen_at").notNull(),
+});
+```
+
+- [ ] **Step 3 : Générer la migration**
+
+Run: `rtk pnpm db:generate`
+Expected: `server/db/migrations/0003_*.sql` (CREATE TABLE machine_credentials + machine_host_keys uniquement). Vérifier qu'aucun DROP ni recréation de table existante n'apparaît (sinon corriger le schéma et régénérer).
+
+- [ ] **Step 4 : Étendre `server/db/schema.test.ts`** — ajouter un test
+
+```ts
+ it("crée les tables de credentials Phase 2", () => {
+ const sqlite = freshMigratedDb();
+ const tables = tableNames(sqlite);
+ expect(tables).toEqual(expect.arrayContaining(["machine_credentials", "machine_host_keys"]));
+ // machines conserve ses colonnes secrets legacy (fallback)
+ expect(columnNames(sqlite, "machines")).toContain("enc_password");
+ });
+```
+
+- [ ] **Step 5 :** Run `rtk pnpm vitest run server/db/schema.test.ts` → PASS. Puis `rtk pnpm check` → 0 erreur.
+
+- [ ] **Step 6 : (pas de commit)**
+
+---
+
+## Task 2 : Service `credentials` (write / read / backfill) — TDD
+
+**Files:** Create `server/services/credentials.ts`, `server/services/credentials.test.ts`.
+
+- [ ] **Step 1 : Test (échec attendu)** — `server/services/credentials.test.ts`
+
+```ts
+import { describe, it, expect } from "vitest";
+import { resolveCreds } from "./credentials.js";
+
+describe("resolveCreds", () => {
+ it("préfère la ligne machine_credentials", () => {
+ const out = resolveCreds(
+ { encPassword: "M_PWD", encSudoPassword: null }, // machines (legacy)
+ { encPassword: "C_PWD", encSudoPassword: "C_SUDO" }, // machine_credentials
+ );
+ expect(out).toEqual({ encPassword: "C_PWD", encSudoPassword: "C_SUDO" });
+ });
+ it("retombe sur machines si pas de credentials", () => {
+ const out = resolveCreds({ encPassword: "M_PWD", encSudoPassword: "M_SUDO" }, null);
+ expect(out).toEqual({ encPassword: "M_PWD", encSudoPassword: "M_SUDO" });
+ });
+});
+```
+
+- [ ] **Step 2 :** Run `rtk pnpm vitest run server/services/credentials.test.ts` → FAIL (module manquant).
+
+- [ ] **Step 3 : Implémenter `server/services/credentials.ts`**
+
+```ts
+// server/services/credentials.ts
+import { eq } from "drizzle-orm";
+import { db, schema } from "../db/client.js";
+
+interface EncPair { encPassword: string | null; encSudoPassword: string | null; }
+
+/** Résout la source des secrets : machine_credentials prioritaire, sinon legacy machines (fonction pure). */
+export function resolveCreds(legacy: EncPair, creds: EncPair | null): EncPair {
+ if (creds && creds.encPassword) return { encPassword: creds.encPassword, encSudoPassword: creds.encSudoPassword };
+ return { encPassword: legacy.encPassword, encSudoPassword: legacy.encSudoPassword };
+}
+
+/** Écrit (insert/replace) la ligne machine_credentials pour une machine (secrets déjà chiffrés). */
+export function writeCredentials(input: {
+ machineId: string;
+ encPassword: string | null;
+ encSudoPassword: string | null;
+}): void {
+ const now = new Date().toISOString();
+ db.insert(schema.machineCredentials)
+ .values({
+ machineId: input.machineId,
+ authMethod: "password",
+ encPassword: input.encPassword,
+ encSudoPassword: input.encSudoPassword,
+ sudoMode: input.encSudoPassword ? "separate" : "same_as_ssh",
+ createdAt: now,
+ updatedAt: now,
+ status: "unknown",
+ })
+ .onConflictDoUpdate({
+ target: schema.machineCredentials.machineId,
+ set: { encPassword: input.encPassword, encSudoPassword: input.encSudoPassword, updatedAt: now },
+ })
+ .run();
+}
+
+/** Lit la ligne machine_credentials (ou null). */
+export function readCredentials(machineId: string): EncPair | null {
+ const row = db.select().from(schema.machineCredentials)
+ .where(eq(schema.machineCredentials.machineId, machineId)).get();
+ return row ? { encPassword: row.encPassword, encSudoPassword: row.encSudoPassword } : null;
+}
+
+/** Backfill idempotent : crée une ligne machine_credentials pour chaque machine qui n'en a pas. */
+export function backfillCredentials(): number {
+ const machines = db.select().from(schema.machines).all();
+ let created = 0;
+ for (const m of machines) {
+ if (readCredentials(m.id)) continue;
+ writeCredentials({ machineId: m.id, encPassword: m.encPassword, encSudoPassword: m.encSudoPassword });
+ created++;
+ }
+ return created;
+}
+```
+
+- [ ] **Step 4 :** Run `rtk pnpm vitest run server/services/credentials.test.ts` → PASS (2). (Le test n'appelle que `resolveCreds`, pur ; pas de DB.)
+
+- [ ] **Step 5 : (pas de commit)**
+
+---
+
+## Task 3 : Brancher dans `machines.ts` + backfill au démarrage
+
+**Files:** Modify `server/services/machines.ts`, `server/db/migrate.ts`.
+
+- [ ] **Step 1 : Relire `server/services/machines.ts`** (état réel : `getCreds`, `createMachine`).
+
+- [ ] **Step 2 : `createMachine` écrit aussi machine_credentials**
+
+Importer en tête : `import { writeCredentials, readCredentials, resolveCreds } from "./credentials.js";`
+Après l'`insert` de la ligne `machines` (avant `return toView(row)`), ajouter :
+```ts
+ writeCredentials({ machineId: id, encPassword: row.encPassword, encSudoPassword: row.encSudoPassword });
+```
+(On conserve aussi l'écriture dans `machines.enc_password` — non destructif.)
+
+- [ ] **Step 3 : `getCreds` lit machine_credentials en priorité**
+
+Remplacer le corps de `getCreds` par :
+```ts
+export function getCreds(m: MachineRow): SshCreds {
+ const key = env.requireMasterKey();
+ const { encPassword, encSudoPassword } = resolveCreds(
+ { encPassword: m.encPassword, encSudoPassword: m.encSudoPassword },
+ readCredentials(m.id),
+ );
+ if (!encPassword) throw new Error("Aucun secret pour cette machine");
+ return {
+ hostname: m.hostname,
+ port: m.port,
+ username: m.username,
+ password: decryptSecret(encPassword, key),
+ sudoPassword: encSudoPassword ? decryptSecret(encSudoPassword, key) : null,
+ };
+}
+```
+
+- [ ] **Step 4 : Backfill au démarrage** — dans `server/db/migrate.ts`, après `runMigrations()`, exposer et appeler le backfill. Modifier `runMigrations` pour enchaîner :
+```ts
+// server/db/migrate.ts
+import { migrate } from "drizzle-orm/better-sqlite3/migrator";
+import { db } from "./client.js";
+import { backfillCredentials } from "../services/credentials.js";
+
+export function runMigrations(): void {
+ migrate(db, { migrationsFolder: "./server/db/migrations" });
+ const n = backfillCredentials();
+ if (n > 0) console.log(`[migrate] backfill credentials: ${n} machine(s)`);
+}
+```
+
+- [ ] **Step 5 :** Run `rtk pnpm check && rtk pnpm test` → 0 erreur TS ; tests verts (48 attendus : +2 credentials). Si un test hors périmètre (WIP concurrent) casse, le signaler sans corriger.
+
+- [ ] **Step 6 : (pas de commit)**
+
+---
+
+## Task 4 : Vérification finale Phase 2
+
+- [ ] **Step 1 :** `rtk pnpm check && rtk pnpm test && rtk pnpm build` → tout vert + `dist` produit.
+
+- [ ] **Step 2 : Boot + backfill + tables**
+```bash
+export SU_MASTER_KEY=$(openssl rand -hex 32) SU_DB_PATH=./data/p2.db SU_REPORTS_DIR=./data/p2-reports
+node dist/index.js > ./data/p2.log 2>&1 &
+sleep 3
+curl -s localhost:8787/health
+node -e "const D=require('better-sqlite3');const db=new D('./data/p2.db');console.log(db.prepare(\"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'machine_%'\").all().map(r=>r.name).join(', '));"
+kill %1 2>/dev/null
+rm -rf ./data/p2.db* ./data/p2-reports ./data/p2.log
+```
+Expected: `{"ok":true}` ; tables incluent `machine_credentials`, `machine_host_keys`. (Backfill = 0 sur DB neuve, normal.)
+
+- [ ] **Step 3 :** Reporter à l'utilisateur (tables ajoutées, dual-read/backfill, non-régression). **Ne pas committer.**
+
+---
+
+## Self-Review (couverture tache1.9 §14 Phase 2)
+- créer `machine_credentials` → Task 1. ✓
+- migrer `enc_password`/`enc_sudo_password` → approche non destructive : dual-write + backfill + lecture prioritaire (Tasks 2-3). Les colonnes legacy restent comme fallback (drop = phase ultérieure de nettoyage). ✓
+- créer `machine_host_keys` → Task 1 (schéma ; vérification host key = logique ultérieure). ✓
+- audit événements secrets → léger : non inclus en Phase 2 (le `recordEvent` Phase 1 existe ; l'audit systématique des déchiffrements relève de tâche 7 sécurité). Noté comme suite.
+
+Décision assumée : non destructif (pas de DROP des colonnes secrets de `machines`) pour protéger les machines réelles existantes. Noms cohérents : `resolveCreds`/`writeCredentials`/`readCredentials`/`backfillCredentials` définis Task 2, utilisés Task 3.
diff --git a/docs/superpowers/plans/2026-06-05-tache2-sj0-socle.md b/docs/superpowers/plans/2026-06-05-tache2-sj0-socle.md
new file mode 100644
index 0000000..d2e6c1c
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-05-tache2-sj0-socle.md
@@ -0,0 +1,408 @@
+# Tâche 2 — SJ-0 (socle : types + réduction + résolution de profil) — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: subagent-driven-development / executing-plans. Étapes checkbox.
+
+**Goal:** Poser le socle de la tâche 2, **purement additif** : étendre `shared/types.ts` (unions élargies + blocs optionnels, rétro-compatibles), enrichir le réducteur de lignes (préfixes Docker), et ajouter `resolveTemplate(action, osFamily)` avec fallback `base`. Aucun changement de wiring (refresh/execute inchangés).
+
+**Architecture:** Extensions additives. Référence design : `docs/design/tache2/40-contrats-json.md` (types), `60-profils-os-machine.md` (résolution), `99-couverture-gate.md`. Tous les ajouts sont optionnels/élargis ⇒ un `UpdateSnapshot`/`ExecutionResult` du jalon 1 reste strictement valide (vérifié par `tsc`).
+
+**Tech Stack:** TypeScript, vitest.
+
+---
+
+## Invariants
+- **Rétro-compat stricte** : ne rien retirer/renommer. Préserver `MachineStatus`, `MachineView`, `ServerCapabilities` (WIP) et tout autre contenu actuel de `shared/types.ts`.
+- **Aucun changement de comportement** : on n'altère PAS `refresh.ts`/`execute.ts` en SJ-0 (la bascule du refresh sur les nouveaux templates = SJ-1).
+- Réducteur : **garder `reduceAptLines`** (imports existants dans refresh/execute) ; ajouter les préfixes Docker et un alias `reduceLines`. **Ne PAS renommer le fichier** `aptReduce.ts` (éviter de toucher les imports de refresh/execute — churn/concurrence).
+- Tree partagé / WIP concurrent : ne toucher QUE `shared/types.ts`, `server/templates/aptReduce.ts` (+test), `server/templates/render.ts` (+ test resolveTemplate), et les fichiers de test. **Ne pas committer.**
+
+## File Structure
+```
+shared/types.ts # MODIF : unions élargies + interfaces + champs optionnels
+shared/types.test.ts # NOUVEAU : verrouille la rétro-compat (compile + runtime léger)
+server/templates/aptReduce.ts # MODIF : préfixes Docker + alias reduceLines
+server/templates/aptReduce.test.ts # MODIF : +cas Docker
+server/templates/render.ts # MODIF : +resolveTemplate
+server/templates/resolveTemplate.test.ts # NOUVEAU
+```
+
+---
+
+## Task 1 : Étendre `shared/types.ts`
+
+**Files:** Modify `shared/types.ts` ; Create `shared/types.test.ts`.
+
+- [ ] **Step 1 : Relire le fichier réel** (`rtk read shared/types.ts`) pour repérer le contenu à préserver (`MachineStatus`, `MachineView`, `ServerCapabilities`, etc.).
+
+- [ ] **Step 2 : Appliquer les extensions** (élargir les unions existantes, remplacer `AptPackage`/`UpdateSnapshot`/`ExecutionResult` par les versions étendues, AJOUTER les nouvelles interfaces). Ne pas supprimer l'existant. Contenu cible (depuis `docs/design/tache2/40-contrats-json.md`) :
+
+```ts
+export type OsFamily = "debian" | "ubuntu" | "proxmox" | "raspbian" | "unknown";
+export type MachineKind =
+ | "physical" | "vm" | "proxmox_host" | "lxc"
+ | "raspberry_pi" | "workstation" | "unknown";
+export type AptProxyMode = "direct" | "runtime" | "persistent";
+export type ActionType =
+ | "apt_full_upgrade" | "reboot"
+ | "apt_update_analyze" | "apt_upgrade" | "apt_dist_upgrade"
+ | "apt_autoremove" | "apt_clean" | "reboot_verified"
+ | "docker_scan" | "docker_inspect_current" | "docker_pull_check"
+ | "docker_compose_apply" | "docker_prune_images" | "docker_compose_down"
+ | "machine_probe" | "post_install";
+export type SnapshotStatus = "ok" | "updates_available" | "warning" | "error";
+// ExecutionStatus, MachineStatus : INCHANGÉS (préserver l'existant)
+
+export interface AptPackage {
+ name: string;
+ currentVersion: string | null;
+ targetVersion: string;
+ origin: string | null;
+ arch?: string;
+ operation?: "upgrade" | "install" | "remove" | "hold";
+ severityHint?: "normal" | "security";
+}
+
+export interface AptSnapshotDetail {
+ enabled: boolean;
+ count: number;
+ rebootRequired: boolean;
+ packages: AptPackage[];
+ status?: SnapshotStatus;
+ upgradeCount?: number;
+ distUpgradeCount?: number;
+ installed?: AptPackage[];
+ removed?: AptPackage[];
+ held?: string[];
+ rebootPkgs?: string[];
+}
+
+export interface DockerSnapshotService {
+ serviceName: string;
+ image: string;
+ currentImageId?: string | null;
+ currentDigest?: string | null;
+ candidateImageId?: string | null;
+ candidateDigest?: string | null;
+ currentVersion?: string | null;
+ candidateVersion?: string | null;
+ sourceUrl?: string | null;
+ status?: "up_to_date" | "updates_available" | "warning" | "error";
+}
+export interface DockerSnapshotStack {
+ name: string;
+ workingDir: string;
+ composeFiles: string[];
+ projectName?: string | null;
+ status: "candidate" | "enabled" | "ignored" | "error";
+ detectedBy?: "root_scan" | "label" | "manual";
+ services: DockerSnapshotService[];
+}
+export interface DockerSnapshot {
+ enabled: boolean;
+ installed: boolean;
+ count: number;
+ declaredRoots?: string[];
+ stacks: DockerSnapshotStack[];
+ status?: SnapshotStatus;
+}
+
+export interface SnapshotError {
+ source: "apt" | "docker" | "post_install" | "ssh" | "system";
+ kind: string;
+ severity: "info" | "warning" | "error";
+ message: string;
+ remediation?: string;
+ importantLines?: string[];
+}
+
+export interface UpdateSnapshot {
+ machineId: string;
+ hostname: string;
+ os: { family: OsFamily; version: string };
+ checkedAt: string;
+ status: MachineStatus;
+ apt: AptSnapshotDetail;
+ schemaVersion?: number;
+ kind?: "apt_update_analyze" | "docker_scan" | "reboot_check" | "combined";
+ machineKind?: MachineKind;
+ docker?: DockerSnapshot;
+ errors?: SnapshotError[];
+ rawHints?: { logImportantLines: string[] };
+}
+
+export interface AptChange {
+ name: string;
+ arch?: string;
+ fromVersion: string | null;
+ toVersion: string | null;
+ operation: "upgraded" | "installed" | "removed" | "unchanged";
+ origin?: string | null;
+}
+export interface AptExecutionResult {
+ planned: AptPackage[];
+ applied: AptChange[];
+ installed: AptChange[];
+ removed: AptChange[];
+ held: string[];
+ errors?: SnapshotError[];
+ rebootRequiredAfterRun: boolean;
+}
+export interface DockerImageChange {
+ stack: string;
+ serviceName?: string;
+ imageRef?: string;
+ fromImageId?: string | null;
+ toImageId?: string | null;
+ fromDigest?: string | null;
+ toDigest?: string | null;
+ operation: "pulled" | "recreated" | "pruned";
+}
+export interface DockerExecutionResult {
+ pull?: { changes: DockerImageChange[]; errors?: SnapshotError[] };
+ up?: { recreated: string[]; running: string[]; exited: string[]; errors?: SnapshotError[] };
+ prune?: { imagesDeleted: string[]; bytesReclaimed: number; errors?: SnapshotError[] };
+ errors?: SnapshotError[];
+}
+export interface RebootResult {
+ beforeBootId: string | null;
+ afterBootId: string | null;
+ requestedAt: string;
+ sshWentDownAt: string | null;
+ sshCameBackAt: string | null;
+ waitedSeconds: number;
+ status: "ok" | "reboot_command_failed" | "ssh_never_went_down"
+ | "machine_did_not_return" | "boot_id_unchanged" | "timeout";
+ lastRebootDurationSeconds?: number;
+ nextRecommendedWaitSeconds?: number;
+ errors?: SnapshotError[];
+}
+export interface PostInstallResult {
+ profilesRun: string[];
+ variablesUsed: Record;
+ filesModified: string[];
+ packagesInstalled: string[];
+ servicesEnabled: string[];
+ rebootsRequested: boolean;
+ networkChange?: { oldEndpoint: string | null; newEndpoint: string | null; reconnectHost: string | null };
+ errors?: SnapshotError[];
+}
+
+export interface ExecutionResult {
+ executionId: string;
+ machineId: string;
+ startedAt: string;
+ finishedAt: string;
+ mode: "manual" | "scheduled" | "hermes_requested";
+ action: ActionType;
+ status: ExecutionStatus;
+ rebootRequiredAfterRun: boolean;
+ importantLogLines: string[];
+ rawLogRef: string;
+ reportRef: string;
+ schemaVersion?: number;
+ apt?: AptExecutionResult;
+ docker?: DockerExecutionResult;
+ reboot?: RebootResult;
+ postInstall?: PostInstallResult;
+ errors?: SnapshotError[];
+}
+```
+
+> Préserver `MachineStatus`, `MachineView`, `ServerCapabilities` et tout autre contenu présent. Le bloc `apt` de `UpdateSnapshot` reste **requis** (forme jalon 1) ; `mode` de `ExecutionResult` était le littéral `"manual"` → l'union l'inclut.
+
+- [ ] **Step 3 : Test de rétro-compat `shared/types.test.ts`**
+
+```ts
+import { describe, it, expect } from "vitest";
+import type { UpdateSnapshot, ExecutionResult } from "./types.js";
+
+describe("rétro-compatibilité des contrats", () => {
+ it("un snapshot jalon 1 (sans blocs optionnels) reste valide", () => {
+ const snap: UpdateSnapshot = {
+ machineId: "m1", hostname: "h", os: { family: "debian", version: "12" },
+ checkedAt: "2026-06-05T10:00:00Z", status: "ok",
+ apt: { enabled: true, count: 0, rebootRequired: false, packages: [] },
+ };
+ expect(snap.apt.count).toBe(0);
+ });
+
+ it("une exécution jalon 1 (mode manual, sans blocs) reste valide", () => {
+ const exec: ExecutionResult = {
+ executionId: "e1", machineId: "m1", startedAt: "a", finishedAt: "b",
+ mode: "manual", action: "apt_full_upgrade", status: "ok",
+ rebootRequiredAfterRun: false, importantLogLines: [], rawLogRef: "r", reportRef: "rr",
+ };
+ expect(exec.action).toBe("apt_full_upgrade");
+ });
+
+ it("accepte les nouveaux blocs optionnels", () => {
+ const snap: UpdateSnapshot = {
+ machineId: "m1", hostname: "h", os: { family: "proxmox", version: "8" },
+ checkedAt: "t", status: "updates_available",
+ apt: { enabled: true, count: 1, rebootRequired: false, packages: [], status: "updates_available" },
+ schemaVersion: 1, kind: "apt_update_analyze", machineKind: "proxmox_host",
+ docker: { enabled: false, installed: false, count: 0, stacks: [] },
+ errors: [],
+ };
+ expect(snap.docker?.installed).toBe(false);
+ });
+});
+```
+
+- [ ] **Step 4 :** Run `rtk pnpm vitest run shared/types.test.ts` → PASS (3). Puis `rtk pnpm check` → **0 erreur** (c'est le vrai test de rétro-compat : si un consommateur existant casse à cause d'un retrait/renommage, tsc le révèle). Si `check` signale une erreur dans un fichier consommateur (`refresh.ts`/`execute.ts`/`machines.ts`/WIP) causée par TON changement de types, corrige le type (rends additif) — ne casse pas les consommateurs.
+
+- [ ] **Step 5 : (pas de commit)**
+
+---
+
+## Task 2 : Réducteur enrichi (préfixes Docker)
+
+**Files:** Modify `server/templates/aptReduce.ts`, `server/templates/aptReduce.test.ts`.
+
+- [ ] **Step 1 : Relire `server/templates/aptReduce.ts`** (état réel).
+
+- [ ] **Step 2 : Ajouter un cas Docker au test `aptReduce.test.ts`**
+
+```ts
+ it("garde aussi les lignes Docker utiles", () => {
+ const raw = [
+ "Pulling jellyfin ...",
+ "Status: Downloaded newer image for jellyfin/jellyfin:latest",
+ "Recreating jellyfin ...",
+ "Started jellyfin",
+ "blabla inutile",
+ "Total reclaimed space: 1.2GB",
+ ].join("\n");
+ expect(reduceLines(raw)).toEqual([
+ "Pulling jellyfin ...",
+ "Status: Downloaded newer image for jellyfin/jellyfin:latest",
+ "Recreating jellyfin ...",
+ "Started jellyfin",
+ "Total reclaimed space: 1.2GB",
+ ]);
+ });
+```
+Ajouter `reduceLines` à l'import existant : `import { reduceAptLines, reduceLines } from "./aptReduce.js";`
+
+- [ ] **Step 3 : Lancer (échec attendu)** — `rtk pnpm vitest run server/templates/aptReduce.test.ts` → FAIL (`reduceLines` introuvable / lignes Docker non gardées).
+
+- [ ] **Step 4 : Étendre `server/templates/aptReduce.ts`**
+
+```ts
+// server/templates/aptReduce.ts
+const PREFIXES = [
+ // APT / dpkg (jalon 1)
+ "Inst ", "Conf ", "Remv ", "Err ", "E:", "W:", "dpkg:",
+ // Docker (SJ-0)
+ "Pulling", "Digest", "Status", "Downloaded newer image", "Recreating", "Started", "Error",
+];
+const CONTAINS = [
+ "reboot-required", "REBOOT_REQUIRED",
+ "deleted", "Total reclaimed space",
+];
+
+/** Garde uniquement les lignes informatives (APT + Docker) d'une sortie brute. */
+export function reduceLines(raw: string): string[] {
+ return raw
+ .split("\n")
+ .map((l) => l.trimEnd())
+ .filter((l) => PREFIXES.some((p) => l.startsWith(p)) || CONTAINS.some((c) => l.includes(c)));
+}
+
+/** Alias rétro-compatible (jalon 1) : même comportement, conserve les imports existants. */
+export const reduceAptLines = reduceLines;
+```
+
+> Garder l'export `reduceAptLines` (utilisé par `refresh.ts`/`execute.ts`). `reduceLines` est le nouveau nom canonique.
+
+- [ ] **Step 5 :** Run `rtk pnpm vitest run server/templates/aptReduce.test.ts` → PASS (cas APT existants + nouveau cas Docker). `rtk pnpm check` → 0 erreur.
+
+- [ ] **Step 6 : (pas de commit)**
+
+---
+
+## Task 3 : `resolveTemplate(action, osFamily)`
+
+**Files:** Modify `server/templates/render.ts` ; Create `server/templates/resolveTemplate.test.ts`.
+
+- [ ] **Step 1 : Relire `server/templates/render.ts`** (état réel : `TEMPLATES_ROOT`, `renderTemplate`, `TemplateVars`).
+
+- [ ] **Step 2 : Test `server/templates/resolveTemplate.test.ts`**
+
+```ts
+import { describe, it, expect } from "vitest";
+import { resolveTemplate } from "./render.js";
+
+describe("resolveTemplate", () => {
+ it("retombe sur apt/ quand aucun dossier OS spécifique n'existe (fonction exists fournie)", () => {
+ const noneExist = () => false;
+ expect(resolveTemplate("full-upgrade", "proxmox", noneExist)).toBe("apt/full-upgrade.sh.tpl");
+ expect(resolveTemplate("update-analyze", "debian", noneExist)).toBe("apt/update-analyze.sh.tpl");
+ });
+
+ it("choisit le template OS spécifique quand il existe", () => {
+ const proxmoxExists = (rel: string) => rel === "proxmox/full-upgrade.sh.tpl";
+ expect(resolveTemplate("full-upgrade", "proxmox", proxmoxExists)).toBe("proxmox/full-upgrade.sh.tpl");
+ });
+
+ it("unknown retombe toujours sur apt/", () => {
+ const all = () => true;
+ expect(resolveTemplate("clean", "unknown", all)).toBe("apt/clean.sh.tpl");
+ });
+});
+```
+
+- [ ] **Step 3 : Lancer (échec)** — `rtk pnpm vitest run server/templates/resolveTemplate.test.ts` → FAIL.
+
+- [ ] **Step 4 : Ajouter `resolveTemplate` à `server/templates/render.ts`** (sans toucher `renderTemplate`/`TemplateVars` existants ; ajouter l'import `existsSync`) :
+
+```ts
+import { existsSync } from "node:fs";
+// ... (TEMPLATES_ROOT, renderTemplate existants inchangés) ...
+
+/** Existence par défaut d'un template relatif à templates/. */
+function defaultExists(rel: string): boolean {
+ return existsSync(resolve(TEMPLATES_ROOT, rel));
+}
+
+/**
+ * Résout le chemin de template le plus spécifique pour (action, OS) :
+ * `/.sh.tpl` s'il existe, sinon fallback base `apt/.sh.tpl`.
+ * `exists` est injectable pour les tests.
+ */
+export function resolveTemplate(
+ action: string,
+ osFamily: string,
+ exists: (rel: string) => boolean = defaultExists,
+): string {
+ const specific = `${osFamily}/${action}.sh.tpl`;
+ if (osFamily !== "unknown" && osFamily !== "apt" && exists(specific)) return specific;
+ return `apt/${action}.sh.tpl`;
+}
+```
+
+> Note : `renderTemplate` accepte déjà un `relPath` (ex. `apt/full-upgrade.sh.tpl`), donc `renderTemplate(resolveTemplate(action, osFamily), vars)` fonctionnera en SJ-1 sans modifier `renderTemplate`.
+
+- [ ] **Step 5 :** Run `rtk pnpm vitest run server/templates/resolveTemplate.test.ts` → PASS (3). `rtk pnpm check` → 0 erreur.
+
+- [ ] **Step 6 : (pas de commit)**
+
+---
+
+## Task 4 : Vérification finale SJ-0
+
+- [ ] **Step 1 :** `rtk pnpm check && rtk pnpm test && rtk pnpm build`
+Expected: 0 erreur TS ; tous tests verts (49 Phase 2 + 3 types + 1 Docker reduce + 3 resolveTemplate ≈ 56) ; build OK.
+
+- [ ] **Step 2 :** Reporter : types étendus rétro-compatibles (tsc vert = preuve), réducteur Docker, `resolveTemplate` prêt pour SJ-1. **Ne pas committer.**
+
+---
+
+## Self-Review (couverture SJ-0)
+- Types étendus (unions + blocs optionnels) → Task 1, rétro-compat verrouillée par `tsc` + test. ✓
+- Réducteur + préfixes Docker → Task 2 (`reduceLines` + alias `reduceAptLines` conservé). ✓
+- `resolveTemplate(action, osFamily)` + fallback base → Task 3. ✓
+- `schemaVersion` → présent dans `UpdateSnapshot`/`ExecutionResult` (optionnel). ✓
+- Aucun wiring modifié (refresh/execute intacts) ⇒ non-régression jalon 1. ✓
+
+Décisions assumées : fichier `aptReduce.ts` NON renommé (alias `reduceLines` ajouté) pour éviter de toucher les imports de refresh/execute (churn/concurrence) — le nom canonique `reduceLines` est exporté ; renommage physique reporté à un nettoyage ultérieur. `resolveTemplate` avec `exists` injectable pour testabilité des deux branches.
diff --git a/docs/superpowers/plans/2026-06-05-tache2-sj1-apt-update-analyze.md b/docs/superpowers/plans/2026-06-05-tache2-sj1-apt-update-analyze.md
new file mode 100644
index 0000000..acbf6cf
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-05-tache2-sj1-apt-update-analyze.md
@@ -0,0 +1,349 @@
+# Tâche 2 — SJ-1 (APT update/analyse enrichi) — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: subagent-driven-development / executing-plans. Étapes checkbox.
+
+**Goal:** Introduire `apt/update-analyze.sh.tpl` (refresh index + simulations `upgrade` et `dist-upgrade` + held + reboot-check, non destructif), son parsing enrichi (`AptSnapshotDetail` : upgrade/dist-upgrade/installed/removed/held/rebootPkgs + statut `ok|updates_available|warning|error`), et **basculer `refreshMachine` dessus** via `resolveTemplate`, en conservant `check.sh.tpl`.
+
+**Architecture:** Additif. Référence design : `docs/design/tache2/10-templates-apt.md §4.1` (template) et `40-contrats-json.md §3` (`AptSnapshotDetail`). Le parsing est en TS (réutilise `parseAptSimulate` SJ-0/jalon 1) ; `buildAptSnapshotDetail` est une fonction pure testée sur fixtures. Le refresh bascule sur le nouveau template via `resolveTemplate("update-analyze", osFamily)` (fallback `apt/`). `check.sh.tpl` reste en place (non supprimé). Aucune rupture : `snapshot.apt` garde ses champs jalon 1 (enabled/count/rebootRequired/packages) + champs additifs.
+
+**Tech Stack:** TypeScript, Mustache, vitest.
+
+---
+
+## Invariants
+- `snapshot.apt` reste de forme jalon 1 (champs requis présents) ; on l'enrichit via les champs optionnels de `AptSnapshotDetail` (SJ-0).
+- `MachineStatus` (union jalon 1, sans "warning") **inchangée** : le statut `warning` vit dans `snapshot.apt.status` ; `snapshot.status` (MachineStatus) mappe warning→`updates_available`.
+- `check.sh.tpl` conservé. Wiring : seul `refreshMachine` bascule sur `update-analyze`.
+- Tree partagé / WIP concurrent : ne toucher QUE `server/services/aptParse.ts` (+test/fixtures), `templates/apt/update-analyze.sh.tpl`, `server/services/refresh.ts`, `server/templates/render.test.ts` éventuel. **Ne pas committer.**
+
+## File Structure
+```
+server/services/aptParse.ts # MODIF : +parseAptRemovals/parseHeld/parseRebootDetail/buildAptSnapshotDetail
+server/services/aptParse.test.ts # MODIF : +tests build detail
+server/services/__fixtures__/apt-update-analyze.txt # NOUVEAU : sortie complète du template
+templates/apt/update-analyze.sh.tpl # NOUVEAU
+server/services/refresh.ts # MODIF : bascule sur update-analyze + detail
+```
+
+---
+
+## Task 1 : Parsing enrichi APT (TDD)
+
+**Files:** Modify `server/services/aptParse.ts`, `server/services/aptParse.test.ts` ; Create `server/services/__fixtures__/apt-update-analyze.txt`.
+
+- [ ] **Step 1 : Créer la fixture `server/services/__fixtures__/apt-update-analyze.txt`**
+
+```
+===SU:APT_UPDATE===
+Hit:1 http://deb.debian.org/debian bookworm InRelease
+Reading package lists...
+===SU:APT_SIM_UPGRADE===
+Reading package lists...
+Building dependency tree...
+Inst libc6 [2.31-13] (2.31-13+deb11u5 Debian:11.6/stable [amd64])
+Conf libc6 (2.31-13+deb11u5 Debian:11.6/stable [amd64])
+===SU:APT_SIM_DISTUPGRADE===
+Reading package lists...
+Inst libc6 [2.31-13] (2.31-13+deb11u5 Debian:11.6/stable [amd64])
+Inst newdep (1.0.0 Debian:11.6/stable [all])
+Remv oldpkg [3.2-1]
+Conf libc6 (2.31-13+deb11u5 Debian:11.6/stable [amd64])
+===SU:APT_HELD===
+frozenpkg
+===SU:REBOOT===
+REBOOT_REQUIRED=1
+PKG=linux-image-amd64
+===SU:EXIT=0===
+```
+
+- [ ] **Step 2 : Écrire le test (échec attendu)** — ajouter à `server/services/aptParse.test.ts`
+
+```ts
+import { readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { parseAptRemovals, parseHeld, parseRebootDetail, buildAptSnapshotDetail } from "./aptParse.js";
+
+const ua = readFileSync(fileURLToPath(new URL("./__fixtures__/apt-update-analyze.txt", import.meta.url)), "utf8");
+function section(raw: string, start: string, end: string): string {
+ const s = raw.indexOf(start); if (s === -1) return "";
+ const from = s + start.length; const e = raw.indexOf(end, from);
+ return raw.slice(from, e === -1 ? undefined : e).trim();
+}
+
+describe("parseAptRemovals", () => {
+ it("extrait les suppressions Remv", () => {
+ expect(parseAptRemovals("Remv oldpkg [3.2-1]\nInst x [1] (2 Y [amd64])"))
+ .toEqual([{ name: "oldpkg", currentVersion: "3.2-1" }]);
+ });
+});
+
+describe("parseHeld", () => {
+ it("liste les paquets retenus non vides", () => {
+ expect(parseHeld("frozenpkg\n\n other ")).toEqual(["frozenpkg", "other"]);
+ });
+});
+
+describe("parseRebootDetail", () => {
+ it("lit le flag et les paquets reboot", () => {
+ expect(parseRebootDetail("REBOOT_REQUIRED=1\nPKG=linux-image-amd64\nPKG=foo"))
+ .toEqual({ rebootRequired: true, pkgs: ["linux-image-amd64", "foo"] });
+ expect(parseRebootDetail("REBOOT_REQUIRED=0")).toEqual({ rebootRequired: false, pkgs: [] });
+ });
+});
+
+describe("buildAptSnapshotDetail", () => {
+ it("construit le détail enrichi depuis les sections", () => {
+ const detail = buildAptSnapshotDetail({
+ upgradeSim: section(ua, "===SU:APT_SIM_UPGRADE===", "===SU:APT_SIM_DISTUPGRADE==="),
+ distUpgradeSim: section(ua, "===SU:APT_SIM_DISTUPGRADE===", "===SU:APT_HELD==="),
+ heldRaw: section(ua, "===SU:APT_HELD===", "===SU:REBOOT==="),
+ rebootRaw: section(ua, "===SU:REBOOT===", "===SU:EXIT"),
+ updateFailed: false,
+ });
+ expect(detail.enabled).toBe(true);
+ expect(detail.count).toBe(2); // 2 Inst en dist-upgrade
+ expect(detail.upgradeCount).toBe(1); // 1 Inst en upgrade
+ expect(detail.distUpgradeCount).toBe(2);
+ expect(detail.rebootRequired).toBe(true);
+ expect(detail.rebootPkgs).toEqual(["linux-image-amd64"]);
+ expect(detail.held).toEqual(["frozenpkg"]);
+ expect(detail.removed?.map((r) => r.name)).toEqual(["oldpkg"]);
+ expect(detail.installed?.map((p) => p.name)).toEqual(["newdep"]);
+ expect(detail.status).toBe("warning"); // car removed + held non vides
+ });
+
+ it("status=updates_available sans removed/held, error si update échoue", () => {
+ const ok = buildAptSnapshotDetail({ upgradeSim: "Inst a [1] (2 Y [amd64])", distUpgradeSim: "Inst a [1] (2 Y [amd64])", heldRaw: "", rebootRaw: "REBOOT_REQUIRED=0", updateFailed: false });
+ expect(ok.status).toBe("updates_available");
+ const err = buildAptSnapshotDetail({ upgradeSim: "", distUpgradeSim: "", heldRaw: "", rebootRaw: "REBOOT_REQUIRED=0", updateFailed: true });
+ expect(err.status).toBe("error");
+ });
+});
+```
+
+- [ ] **Step 3 : Lancer (échec)** — `rtk pnpm vitest run server/services/aptParse.test.ts` → FAIL.
+
+- [ ] **Step 4 : Étendre `server/services/aptParse.ts`** (garder `parseAptSimulate`/`parseRebootRequired` existants ; ajouter) :
+
+```ts
+import type { AptPackage, AptSnapshotDetail, SnapshotStatus } from "@shared/types.js";
+
+// ... (parseAptSimulate, parseRebootRequired existants conservés) ...
+
+const REMV_RE = /^Remv (\S+)(?: \[([^\]]+)\])?/;
+export function parseAptRemovals(raw: string): { name: string; currentVersion: string | null }[] {
+ const out: { name: string; currentVersion: string | null }[] = [];
+ for (const line of raw.split("\n")) {
+ const m = REMV_RE.exec(line.trimEnd());
+ if (m) out.push({ name: m[1]!, currentVersion: m[2] ?? null });
+ }
+ return out;
+}
+
+export function parseHeld(raw: string): string[] {
+ return raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
+}
+
+export function parseRebootDetail(raw: string): { rebootRequired: boolean; pkgs: string[] } {
+ const rebootRequired = /REBOOT_REQUIRED=1/.test(raw);
+ const pkgs: string[] = [];
+ for (const line of raw.split("\n")) {
+ const m = /^PKG=(.+)$/.exec(line.trim());
+ if (m) pkgs.push(m[1]!.trim());
+ }
+ return { rebootRequired, pkgs };
+}
+
+export interface AptSections {
+ upgradeSim: string;
+ distUpgradeSim: string;
+ heldRaw: string;
+ rebootRaw: string;
+ updateFailed: boolean;
+}
+
+export function buildAptSnapshotDetail(s: AptSections): AptSnapshotDetail {
+ const upgradePkgs = parseAptSimulate(s.upgradeSim);
+ const distPkgs = parseAptSimulate(s.distUpgradeSim);
+ const installed: AptPackage[] = distPkgs
+ .filter((p) => p.currentVersion === null)
+ .map((p) => ({ ...p, operation: "install" }));
+ const removed: AptPackage[] = parseAptRemovals(s.distUpgradeSim).map((r) => ({
+ name: r.name, currentVersion: r.currentVersion, targetVersion: "", origin: null, operation: "remove",
+ }));
+ const held = parseHeld(s.heldRaw);
+ const { rebootRequired, pkgs: rebootPkgs } = parseRebootDetail(s.rebootRaw);
+
+ let status: SnapshotStatus = "ok";
+ if (s.updateFailed) status = "error";
+ else if (removed.length > 0 || held.length > 0) status = "warning";
+ else if (distPkgs.length > 0) status = "updates_available";
+
+ return {
+ enabled: true,
+ count: distPkgs.length,
+ rebootRequired,
+ packages: distPkgs,
+ status,
+ upgradeCount: upgradePkgs.length,
+ distUpgradeCount: distPkgs.length,
+ installed,
+ removed,
+ held,
+ rebootPkgs,
+ };
+}
+```
+
+- [ ] **Step 5 : Lancer (succès)** — `rtk pnpm vitest run server/services/aptParse.test.ts` → PASS. `rtk pnpm check` → 0 erreur.
+
+- [ ] **Step 6 : (pas de commit)**
+
+---
+
+## Task 2 : Template `apt/update-analyze.sh.tpl`
+
+**Files:** Create `templates/apt/update-analyze.sh.tpl`.
+
+- [ ] **Step 1 : Créer le template** (depuis `10-templates-apt.md §4.1`)
+
+```sh
+#!/bin/sh
+# Refresh index + simulations upgrade/dist-upgrade + held + reboot-check.
+# Exécuté entier sous sudo par la couche SSH. Non destructif.
+export LC_ALL=C
+export DEBIAN_FRONTEND=noninteractive
+{{#aptProxy}}export http_proxy="{{aptProxy}}"; export https_proxy="{{aptProxy}}"
+{{/aptProxy}}
+
+echo "===SU:APT_UPDATE==="
+apt-get update -qq 2>&1
+UPD=$?
+
+echo "===SU:APT_SIM_UPGRADE==="
+apt-get -s -y upgrade 2>&1
+
+echo "===SU:APT_SIM_DISTUPGRADE==="
+apt-get -s -y dist-upgrade 2>&1
+
+echo "===SU:APT_HELD==="
+apt-mark showhold 2>/dev/null
+
+echo "===SU:REBOOT==="
+if [ -f /run/reboot-required ] || [ -f /var/run/reboot-required ]; then
+ echo "REBOOT_REQUIRED=1"
+ [ -f /var/run/reboot-required.pkgs ] && sed 's/^/PKG=/' /var/run/reboot-required.pkgs
+else
+ echo "REBOOT_REQUIRED=0"
+fi
+
+echo "===SU:EXIT=${UPD}==="
+```
+
+- [ ] **Step 2 : Vérifier le rendu** — `rtk pnpm vitest run server/templates/render.test.ts` reste vert (le test existant porte sur `check.sh.tpl` ; pas de régression). Optionnellement ajouter un cas :
+```ts
+ it("rend update-analyze.sh.tpl avec les sections attendues", () => {
+ const out = renderTemplate("apt/update-analyze.sh.tpl", { aptProxy: null });
+ expect(out).toContain("===SU:APT_SIM_DISTUPGRADE===");
+ expect(out).toContain("apt-mark showhold");
+ });
+```
+
+- [ ] **Step 3 : (pas de commit)**
+
+---
+
+## Task 3 : Basculer `refreshMachine` sur update-analyze
+
+**Files:** Modify `server/services/refresh.ts`.
+
+- [ ] **Step 1 : Relire `server/services/refresh.ts`** (état réel, incl. wiring Phase 1 machine_state/event).
+
+- [ ] **Step 2 : Adapter les imports**
+```ts
+import { renderTemplate, resolveTemplate } from "../templates/render.js";
+import {
+ parseAptSimulate, parseRebootRequired, // existants (peuvent rester importés)
+ buildAptSnapshotDetail,
+} from "./aptParse.js";
+import type { UpdateSnapshot, MachineStatus, AptSnapshotDetail } from "@shared/types.js";
+```
+
+- [ ] **Step 3 : Remplacer la construction du snapshot** dans `refreshMachine`. Remplacer le rendu + le parsing actuels (`check.sh.tpl`, `extractSection(...SIMULATE...)`) par :
+
+```ts
+ const proxy = m.aptProxyMode === "runtime" ? m.aptProxyUrl : null;
+ const script = renderTemplate(resolveTemplate("update-analyze", m.osFamily), { aptProxy: proxy });
+
+ let raw = "";
+ try {
+ const res = await runScriptSudo(getCreds(m), script, (c) => {
+ raw += c;
+ outputHub.publish(machineId, c);
+ });
+ raw = res.stdout;
+ } catch (err) {
+ db.update(schema.machines).set({ status: "error" }).where(eq(schema.machines.id, machineId)).run();
+ throw err;
+ }
+
+ const updateExit = /===SU:EXIT=(\d+)===/.exec(raw);
+ const detail: AptSnapshotDetail = buildAptSnapshotDetail({
+ upgradeSim: extractSection(raw, "===SU:APT_SIM_UPGRADE===", "===SU:APT_SIM_DISTUPGRADE==="),
+ distUpgradeSim: extractSection(raw, "===SU:APT_SIM_DISTUPGRADE===", "===SU:APT_HELD==="),
+ heldRaw: extractSection(raw, "===SU:APT_HELD===", "===SU:REBOOT==="),
+ rebootRaw: extractSection(raw, "===SU:REBOOT===", "===SU:EXIT"),
+ updateFailed: updateExit ? Number(updateExit[1]) !== 0 : false,
+ });
+
+ // MachineStatus n'a pas "warning" : warning => updates_available côté machine.
+ const status: MachineStatus =
+ detail.status === "error" ? "error" : detail.count > 0 || detail.status === "warning" ? "updates_available" : "ok";
+ const checkedAt = new Date().toISOString();
+
+ const snapshot: UpdateSnapshot = {
+ machineId,
+ hostname: m.hostname,
+ os: { family: m.osFamily as UpdateSnapshot["os"]["family"], version: m.osVersion ?? "" },
+ checkedAt,
+ status,
+ apt: detail,
+ schemaVersion: 1,
+ kind: "apt_update_analyze",
+ rawHints: { logImportantLines: reduceAptLines(raw) },
+ };
+```
+
+> Conserver ensuite TOUT le bloc Phase 1 inchangé : insertion du snapshot (`kind`/`schemaVersion`/`importantJson`), update `machines`, `upsertMachineState(machineId, deriveAptState(snapshot))`, `recordEvent(...)`, `return snapshot;`. `deriveAptState` lit `snapshot.status`/`apt.count`/`apt.rebootRequired`/`checkedAt` — inchangé.
+
+- [ ] **Step 4 : Vérifier** — `rtk pnpm check && rtk pnpm vitest run server/services/refresh.test.ts server/services/aptParse.test.ts` → 0 erreur, tests verts (`extractSection` + parsing). Note : `check.sh.tpl` n'est plus référencé par le refresh mais reste sur disque (non supprimé), comme prévu.
+
+- [ ] **Step 5 : (pas de commit)**
+
+---
+
+## Task 4 : Vérification finale SJ-1
+
+- [ ] **Step 1 :** `rtk pnpm check && rtk pnpm test && rtk pnpm build` → 0 erreur, tous tests verts, build OK.
+
+- [ ] **Step 2 : Boot smoke** (DB jetable) — confirmer que le serveur démarre (`/health`) avec le refresh branché sur le nouveau template (pas d'exécution SSH réelle ici) :
+```bash
+export SU_MASTER_KEY=$(openssl rand -hex 32) SU_DB_PATH=./data/sj1.db SU_REPORTS_DIR=./data/sj1-reports
+node dist/index.js > ./data/sj1.log 2>&1 &
+sleep 3; curl -s localhost:8787/health; kill %1 2>/dev/null
+rm -rf ./data/sj1.db* ./data/sj1-reports ./data/sj1.log
+```
+Expected: `{"ok":true}`.
+
+- [ ] **Step 3 :** Reporter. Note pour l'utilisateur : la **vérif live** (refresh réel sur une machine Debian) confirmera le parsing des vraies sorties `apt-get -s`. **Ne pas committer.**
+
+---
+
+## Self-Review (couverture SJ-1)
+- `apt/update-analyze.sh.tpl` (update + sim upgrade + sim dist-upgrade + held + reboot-check) → Task 2. ✓
+- parsing des sections + `AptSnapshotDetail` enrichi (upgrade/dist/installed/removed/held/rebootPkgs + status) → Task 1 (TDD fixtures). ✓
+- statut `ok|updates_available|warning|error` → `buildAptSnapshotDetail`. ✓
+- bascule du refresh sur update-analyze (via `resolveTemplate`), `check.sh.tpl` conservé → Task 3. ✓
+- non-régression : `snapshot.apt` garde la forme jalon 1 ; `MachineStatus` inchangée (warning→updates_available) ; machine_state/events Phase 1 préservés. ✓
+
+Décision : `count = distUpgradeCount` (toutes les mises à jour disponibles, cohérent avec le jalon 1 qui comptait la simulation full-upgrade). `warning` (removed/held) exposé dans `apt.status`, mappé `updates_available` pour `machine.status`. Noms cohérents : `parseAptRemovals`/`parseHeld`/`parseRebootDetail`/`buildAptSnapshotDetail` définis Task 1, utilisés Task 3.
diff --git a/docs/superpowers/plans/2026-06-05-tache2-sj2-apt-apply-diff.md b/docs/superpowers/plans/2026-06-05-tache2-sj2-apt-apply-diff.md
new file mode 100644
index 0000000..8a60d0c
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-05-tache2-sj2-apt-apply-diff.md
@@ -0,0 +1,325 @@
+# Tâche 2 — SJ-2 (APT apply + diff dpkg réel) — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: subagent-driven-development / executing-plans. Étapes checkbox.
+
+**Goal:** Enrichir `apt/full-upgrade.sh.tpl` du snapshot dpkg avant/après, ajouter `apt/upgrade.sh.tpl`, `apt/autoremove.sh.tpl`, `apt/clean.sh.tpl`, calculer le **diff dpkg réel** (`AptExecutionResult` : applied/installed/removed), brancher les actions `apt_upgrade`/`apt_autoremove`/`apt_clean` (+ `apt_full_upgrade` enrichi) dans `runAction`, et ajouter un **timeout d'inactivité** optionnel à la couche SSH.
+
+**Architecture:** Additif. Référence : `docs/design/tache2/10-templates-apt.md §4.2-4.4`, `40-contrats-json.md §4` (`AptExecutionResult`/`AptChange`), `50-erreurs.md` (`human_interaction_required`). Le diff dpkg est calculé en TS (`buildAptExecutionResult`, pure, TDD). `runScriptSudo` reçoit une option `inactivityTimeoutMs` (défaut 0 = désactivé ⇒ comportement jalon 1 inchangé) ; `runAction` la passe (600000) pour les actions APT. Les confirmations UI des suppressions relèvent de la tâche 3 ; SJ-2 expose `removed[]` dans le résultat.
+
+**Tech Stack:** TypeScript, Mustache, ssh2, vitest.
+
+---
+
+## Invariants
+- `apt_full_upgrade` et `reboot` (jalon 1) restent fonctionnels ; on **enrichit** sans casser le parsing exit/reboot existant de `execute.ts`.
+- `runScriptSudo` : nouveau paramètre **optionnel** `inactivityTimeoutMs` (défaut 0 = pas de timeout) ⇒ `refreshMachine` et tout appelant existant **inchangés** de comportement.
+- `ExecutionResult.apt` est optionnel (SJ-0) ⇒ une exécution sans diff reste valide.
+- Tree partagé / WIP concurrent : ne toucher QUE `server/services/aptParse.ts` (+test/fixtures), `templates/apt/{full-upgrade,upgrade,autoremove,clean}.sh.tpl`, `server/ssh/client.ts`, `server/services/execute.ts`. **Ne pas committer.**
+
+## File Structure
+```
+server/services/aptParse.ts # MODIF : +parseDpkgList/diffDpkg/buildAptExecutionResult
+server/services/aptParse.test.ts # MODIF : +tests diff dpkg
+templates/apt/full-upgrade.sh.tpl # MODIF : +DPKG_BEFORE/AFTER
+templates/apt/upgrade.sh.tpl # NOUVEAU
+templates/apt/autoremove.sh.tpl # NOUVEAU
+templates/apt/clean.sh.tpl # NOUVEAU
+server/ssh/client.ts # MODIF : +inactivityTimeoutMs (additif)
+server/services/execute.ts # MODIF : actions APT + buildAptExecutionResult + timeout
+```
+
+---
+
+## Task 1 : Diff dpkg (TDD)
+
+**Files:** Modify `server/services/aptParse.ts`, `server/services/aptParse.test.ts`.
+
+- [ ] **Step 1 : Test (échec attendu)** — ajouter à `aptParse.test.ts`
+
+```ts
+import { parseDpkgList, buildAptExecutionResult } from "./aptParse.js";
+
+const BEFORE = "libc6\t2.31-13\tamd64\noldpkg\t3.2-1\tamd64\nstable\t1.0\tamd64";
+const AFTER = "libc6\t2.31-14\tamd64\nnewpkg\t1.0.0\tall\nstable\t1.0\tamd64";
+
+describe("parseDpkgList", () => {
+ it("indexe par package:arch", () => {
+ const m = parseDpkgList("libc6\t2.31-13\tamd64");
+ expect(m["libc6:amd64"]).toEqual({ version: "2.31-13", arch: "amd64" });
+ });
+});
+
+describe("buildAptExecutionResult", () => {
+ it("calcule le diff réel before/after", () => {
+ const r = buildAptExecutionResult(BEFORE, AFTER, "REBOOT_REQUIRED=1");
+ expect(r.applied.find((c) => c.name === "libc6")).toMatchObject({ operation: "upgraded", fromVersion: "2.31-13", toVersion: "2.31-14" });
+ expect(r.installed.map((c) => c.name)).toEqual(["newpkg"]);
+ expect(r.removed.map((c) => c.name)).toEqual(["oldpkg"]);
+ expect(r.applied.some((c) => c.name === "stable")).toBe(false); // unchanged exclu
+ expect(r.rebootRequiredAfterRun).toBe(true);
+ });
+});
+```
+
+- [ ] **Step 2 : Lancer (échec)** — `rtk pnpm vitest run server/services/aptParse.test.ts` → FAIL.
+
+- [ ] **Step 3 : Étendre `server/services/aptParse.ts`**
+
+```ts
+import type { AptChange, AptExecutionResult } from "@shared/types.js";
+
+export function parseDpkgList(raw: string): Record {
+ const out: Record = {};
+ for (const line of raw.split("\n")) {
+ const parts = line.split("\t");
+ if (parts.length < 3) continue;
+ const [name, version, arch] = [parts[0]!.trim(), parts[1]!.trim(), parts[2]!.trim()];
+ if (!name) continue;
+ out[`${name}:${arch}`] = { version, arch };
+ }
+ return out;
+}
+
+/** Diff dpkg réel before/after → AptExecutionResult (planned/held vides : portés par le snapshot). */
+export function buildAptExecutionResult(beforeRaw: string, afterRaw: string, rebootRaw: string): AptExecutionResult {
+ const before = parseDpkgList(beforeRaw);
+ const after = parseDpkgList(afterRaw);
+ const applied: AptChange[] = [];
+ const installed: AptChange[] = [];
+ const removed: AptChange[] = [];
+
+ for (const key of Object.keys(after)) {
+ const [name] = key.split(":");
+ const a = after[key]!;
+ const b = before[key];
+ if (!b) {
+ const change: AptChange = { name: name!, arch: a.arch, fromVersion: null, toVersion: a.version, operation: "installed" };
+ installed.push(change); applied.push(change);
+ } else if (b.version !== a.version) {
+ applied.push({ name: name!, arch: a.arch, fromVersion: b.version, toVersion: a.version, operation: "upgraded" });
+ }
+ }
+ for (const key of Object.keys(before)) {
+ if (!after[key]) {
+ const [name] = key.split(":");
+ const b = before[key]!;
+ const change: AptChange = { name: name!, arch: b.arch, fromVersion: b.version, toVersion: null, operation: "removed" };
+ removed.push(change); applied.push(change);
+ }
+ }
+
+ return {
+ planned: [],
+ applied,
+ installed,
+ removed,
+ held: [],
+ rebootRequiredAfterRun: /REBOOT_REQUIRED=1/.test(rebootRaw),
+ };
+}
+```
+
+- [ ] **Step 4 : Lancer (succès)** — `rtk pnpm vitest run server/services/aptParse.test.ts` → PASS. `rtk pnpm check` → 0 erreur.
+
+- [ ] **Step 5 : (pas de commit)**
+
+---
+
+## Task 2 : Templates APT (full-upgrade enrichi + upgrade/autoremove/clean)
+
+**Files:** Modify `templates/apt/full-upgrade.sh.tpl` ; Create `upgrade.sh.tpl`, `autoremove.sh.tpl`, `clean.sh.tpl`.
+
+- [ ] **Step 1 : Remplacer `templates/apt/full-upgrade.sh.tpl`** (ajoute DPKG_BEFORE/AFTER ; conserve REBOOT + EXIT que `execute.ts` parse déjà)
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+export DEBIAN_FRONTEND=noninteractive
+{{#aptProxy}}export http_proxy="{{aptProxy}}"; export https_proxy="{{aptProxy}}"
+{{/aptProxy}}
+echo "===SU:DPKG_BEFORE==="
+dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
+echo "===SU:APT_FULLUPGRADE==="
+apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold dist-upgrade 2>&1
+CODE=$?
+echo "===SU:DPKG_AFTER==="
+dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
+echo "===SU:REBOOT==="
+if [ -f /run/reboot-required ] || [ -f /var/run/reboot-required ]; then echo "REBOOT_REQUIRED=1"; else echo "REBOOT_REQUIRED=0"; fi
+echo "===SU:EXIT=${CODE}==="
+```
+
+- [ ] **Step 2 : Créer `templates/apt/upgrade.sh.tpl`**
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+export DEBIAN_FRONTEND=noninteractive
+{{#aptProxy}}export http_proxy="{{aptProxy}}"; export https_proxy="{{aptProxy}}"
+{{/aptProxy}}
+echo "===SU:DPKG_BEFORE==="
+dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
+echo "===SU:APT_UPGRADE==="
+apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade 2>&1
+CODE=$?
+echo "===SU:DPKG_AFTER==="
+dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
+echo "===SU:REBOOT==="
+if [ -f /run/reboot-required ] || [ -f /var/run/reboot-required ]; then echo "REBOOT_REQUIRED=1"; else echo "REBOOT_REQUIRED=0"; fi
+echo "===SU:EXIT=${CODE}==="
+```
+
+- [ ] **Step 3 : Créer `templates/apt/autoremove.sh.tpl`**
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+export DEBIAN_FRONTEND=noninteractive
+echo "===SU:APT_SIM_AUTOREMOVE==="
+apt-get -s -y autoremove 2>&1
+echo "===SU:DPKG_BEFORE==="
+dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
+echo "===SU:APT_AUTOREMOVE==="
+apt-get -y autoremove 2>&1
+CODE=$?
+echo "===SU:DPKG_AFTER==="
+dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
+echo "===SU:REBOOT==="
+if [ -f /run/reboot-required ] || [ -f /var/run/reboot-required ]; then echo "REBOOT_REQUIRED=1"; else echo "REBOOT_REQUIRED=0"; fi
+echo "===SU:EXIT=${CODE}==="
+```
+
+- [ ] **Step 4 : Créer `templates/apt/clean.sh.tpl`**
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+echo "===SU:APT_CLEAN==="
+BEFORE=$(du -sb /var/cache/apt/archives 2>/dev/null | awk '{print $1}')
+apt-get clean 2>&1
+AFTER=$(du -sb /var/cache/apt/archives 2>/dev/null | awk '{print $1}')
+echo "FREED_BYTES=$((BEFORE - AFTER))"
+echo "===SU:EXIT=0==="
+```
+
+- [ ] **Step 5 :** `rtk pnpm vitest run server/templates/render.test.ts` reste vert. (pas de commit)
+
+---
+
+## Task 3 : Timeout d'inactivité SSH (additif)
+
+**Files:** Modify `server/ssh/client.ts`.
+
+- [ ] **Step 1 : Relire `server/ssh/client.ts`** (signatures `runScriptSudo`, `execStream`).
+
+- [ ] **Step 2 : Ajouter un paramètre optionnel `inactivityTimeoutMs`** (défaut 0 = désactivé) à `runScriptSudo` et `execStream`. Dans `execStream`, armer un timer réinitialisé à chaque `data`/`stderr data` ; à expiration, `stream.close()`/`conn.end()` et `reject(new Error("human_interaction_required: aucune sortie depuis " + (ms/1000) + "s"))`.
+
+```ts
+export async function runScriptSudo(
+ creds: SshCreds,
+ script: string,
+ onData: (chunk: string) => void,
+ inactivityTimeoutMs = 0,
+): Promise {
+ const conn = await connect(creds);
+ try {
+ const b64 = Buffer.from(script, "utf8").toString("base64");
+ const cmd = `sudo -S -p '' sh -c "$(printf '%s' '${b64}' | base64 -d)"`;
+ return await execStream(conn, cmd, (creds.sudoPassword ?? creds.password) + "\n", onData, inactivityTimeoutMs);
+ } finally {
+ conn.end();
+ }
+}
+```
+
+Dans `execStream(conn, command, stdinData, onData, inactivityTimeoutMs = 0)` : après obtention du `stream`,
+```ts
+ let timer: NodeJS.Timeout | undefined;
+ const arm = () => {
+ if (!inactivityTimeoutMs) return;
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ stream.close();
+ reject(new Error(`human_interaction_required: aucune sortie depuis ${Math.round(inactivityTimeoutMs / 1000)}s`));
+ }, inactivityTimeoutMs);
+ };
+ arm();
+```
+Réinitialiser `arm()` dans les handlers `data` et `stderr data` ; `clearTimeout(timer)` dans `close`. (Garder le `runPlain` existant inchangé : il appelle `execStream` sans le 5e arg ⇒ timeout 0.)
+
+- [ ] **Step 3 :** `rtk pnpm check` → 0 erreur. (Pas de test unitaire SSH ; vérif manuelle en live ultérieure.) (pas de commit)
+
+---
+
+## Task 4 : Brancher les actions APT dans `runAction`
+
+**Files:** Modify `server/services/execute.ts`.
+
+- [ ] **Step 1 : Relire `server/services/execute.ts`** (TEMPLATE_FOR, flux, update executions, blocs Phase 1 reports/artifacts/state/event).
+
+- [ ] **Step 2 : Étendre `TEMPLATE_FOR`**
+```ts
+const TEMPLATE_FOR: Partial> = {
+ apt_full_upgrade: "apt/full-upgrade.sh.tpl",
+ apt_upgrade: "apt/upgrade.sh.tpl",
+ apt_autoremove: "apt/autoremove.sh.tpl",
+ apt_clean: "apt/clean.sh.tpl",
+ reboot: "apt/reboot.sh.tpl",
+};
+```
+(Adapter l'accès : `const rel = TEMPLATE_FOR[action]; if (!rel) throw new Error("Action sans template: " + action);`)
+
+- [ ] **Step 3 : Passer le timeout d'inactivité** pour les actions APT (pas pour reboot) :
+```ts
+ const inactivity = action === "reboot" ? 0 : 600000;
+ const res = await runScriptSudo(getCreds(m), script, (c) => { raw += c; outputHub.publish(machineId, c); }, inactivity);
+```
+
+- [ ] **Step 4 : Construire `result.apt` (diff dpkg) pour les actions APT applicatives.** Après calcul de `raw` et avant l'écriture du rapport, ajouter :
+```ts
+ let aptResult: AptExecutionResult | undefined;
+ if (raw.includes("===SU:DPKG_BEFORE===") && raw.includes("===SU:DPKG_AFTER===")) {
+ aptResult = buildAptExecutionResult(
+ extractSection(raw, "===SU:DPKG_BEFORE===", "==="), // jusqu'au prochain marqueur
+ extractSection(raw, "===SU:DPKG_AFTER===", "===SU:REBOOT==="),
+ extractSection(raw, "===SU:REBOOT===", "===SU:EXIT"),
+ );
+ }
+```
+> ⚠️ `extractSection(raw, "===SU:DPKG_BEFORE===", "===")` : le 2ᵉ marqueur générique `"==="` capture jusqu'au prochain `===SU:...===`. Vérifier que `extractSection` (dans `refresh.ts`) coupe bien au 1ᵉʳ `"==="` rencontré ; sinon, utiliser le marqueur réel suivant (`"===SU:APT_FULLUPGRADE==="` / `"===SU:APT_UPGRADE==="` / `"===SU:APT_AUTOREMOVE==="`). **Préférer** le marqueur explicite : détecter lequel est présent. Implémentation robuste :
+```ts
+ const afterBeforeMarker =
+ raw.includes("===SU:APT_FULLUPGRADE===") ? "===SU:APT_FULLUPGRADE===" :
+ raw.includes("===SU:APT_UPGRADE===") ? "===SU:APT_UPGRADE===" :
+ "===SU:APT_AUTOREMOVE===";
+ if (raw.includes("===SU:DPKG_BEFORE===") && raw.includes("===SU:DPKG_AFTER===")) {
+ aptResult = buildAptExecutionResult(
+ extractSection(raw, "===SU:DPKG_BEFORE===", afterBeforeMarker),
+ extractSection(raw, "===SU:DPKG_AFTER===", "===SU:REBOOT==="),
+ extractSection(raw, "===SU:REBOOT===", "===SU:EXIT"),
+ );
+ }
+```
+
+- [ ] **Step 5 : Attacher `aptResult` au `ExecutionResult`** : dans la construction de `result`, ajouter `...(aptResult ? { apt: aptResult } : {})`. Importer en tête : `import { parseRebootRequired, extractSection } ...` (extractSection vient de `./refresh.js` — déjà importé) et `import { buildAptExecutionResult } from "./aptParse.js";` ainsi que `import type { AptExecutionResult } from "@shared/types.js";`.
+
+- [ ] **Step 6 : Vérifier** — `rtk pnpm check && rtk pnpm test` → 0 erreur, tests verts. (pas de commit)
+
+---
+
+## Task 5 : Vérification finale SJ-2
+
+- [ ] **Step 1 :** `rtk pnpm check && rtk pnpm test && rtk pnpm build` → tout vert.
+- [ ] **Step 2 : Boot smoke** (DB jetable) → `/health` OK. Nettoyer.
+- [ ] **Step 3 :** Reporter. Vérif live ultérieure : `apt_full_upgrade` réel sur Debian → vérifier `result.apt.applied` (diff dpkg réel) + détection removed/held + comportement du timeout. **Ne pas committer.**
+
+---
+
+## Self-Review (couverture SJ-2)
+- Templates `upgrade`/`full-upgrade` enrichi/`autoremove`/`clean` → Task 2. ✓
+- Capture `DPKG_BEFORE/AFTER` + diff réel (`AptExecutionResult`) → Task 1 + Task 4. ✓
+- Timeout d'inactivité + `human_interaction_required` → Task 3 (additif, off par défaut) + Task 4 (600s pour APT). ✓
+- Confirmations UI suppressions → hors périmètre (tâche 3) ; la donnée `removed[]` est exposée dans `result.apt`. ✓ (noté)
+- Non-régression : `apt_full_upgrade`/`reboot` jalon 1 conservés ; `runScriptSudo` rétro-compatible (timeout 0 par défaut) ; `ExecutionResult.apt` optionnel ; blocs Phase 1 préservés. ✓
+
+Décision : `planned`/`held` laissés vides dans `AptExecutionResult` (portés par le snapshot SJ-1, pas re-simulés à l'exécution). `extractSection` utilisé avec marqueur explicite pour `DPKG_BEFORE`. Noms cohérents : `parseDpkgList`/`buildAptExecutionResult` (Task 1) utilisés Task 4 ; `inactivityTimeoutMs` (Task 3) passé Task 4.
diff --git a/docs/superpowers/plans/2026-06-05-tache2-sj3-reboot-verifie.md b/docs/superpowers/plans/2026-06-05-tache2-sj3-reboot-verifie.md
new file mode 100644
index 0000000..c754e2a
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-05-tache2-sj3-reboot-verifie.md
@@ -0,0 +1,252 @@
+# Tâche 2 — SJ-3 (reboot vérifié) — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: subagent-driven-development / executing-plans. Étapes checkbox.
+
+**Goal:** Ajouter l'action `reboot_verified` : capture du `boot_id` avant reboot, orchestration backend (attente de la coupure SSH, reconnexion avec délai adaptatif, relecture du `boot_id`), production d'un `RebootResult` (`ok` seulement si la machine revient ET le `boot_id` a changé). L'action `reboot` (jalon 1) reste inchangée.
+
+**Architecture:** Référence `docs/design/tache2/10-templates-apt.md §4.5` + `40-contrats-json.md §4` (`RebootResult`). Le template `apt/reboot.sh.tpl` est enrichi pour émettre `===SU:BOOT_ID_BEFORE===`. Un module `server/services/rebootVerify.ts` contient : `classifyReboot(...)` (fonction **pure**, TDD) + `verifyReboot(machineId)` (orchestration réseau : poll `runPlain` jusqu'à coupure puis retour). `execute.ts` route l'action `reboot_verified` vers cette orchestration. Délai adaptatif stocké dans `machine_state` (réutilise la table Phase 1).
+
+**Tech Stack:** TypeScript, ssh2, vitest.
+
+---
+
+## Invariants
+- `reboot` (jalon 1) **inchangé** (toujours via `apt/reboot.sh.tpl`, fire-and-forget). `reboot_verified` est une **nouvelle** action.
+- `ExecutionResult.reboot` est optionnel (SJ-0) → rétro-compatible.
+- Pas de blocage indéfini : timeouts bornés (détection coupure ≤ 60 s ; retour machine ≤ 600 s par défaut).
+- Tree partagé / WIP concurrent : ne toucher QUE `templates/apt/reboot.sh.tpl`, `server/services/rebootVerify.ts` (+test), `server/services/execute.ts`. **Ne pas committer.**
+
+## File Structure
+```
+templates/apt/reboot.sh.tpl # MODIF : +===SU:BOOT_ID_BEFORE===
+server/services/rebootVerify.ts # NOUVEAU : classifyReboot (pure) + verifyReboot (orchestration)
+server/services/rebootVerify.test.ts # NOUVEAU : tests classifyReboot
+server/services/execute.ts # MODIF : route action reboot_verified
+```
+
+---
+
+## Task 1 : Template `reboot.sh.tpl` (capture boot_id)
+
+**Files:** Modify `templates/apt/reboot.sh.tpl`.
+
+- [ ] **Step 1 : Remplacer `templates/apt/reboot.sh.tpl`** (ajoute BOOT_ID_BEFORE ; conserve REBOOT_NOW)
+
+```sh
+#!/bin/sh
+export LC_ALL=C
+echo "===SU:BOOT_ID_BEFORE==="
+cat /proc/sys/kernel/random/boot_id 2>/dev/null
+echo "===SU:REBOOT_NOW==="
+# Reboot différé pour laisser le canal SSH se fermer proprement.
+nohup sh -c 'sleep 2; reboot' >/dev/null 2>&1 &
+echo "reboot planifié"
+```
+
+- [ ] **Step 2 : (pas de commit)** — `templates/apt/reboot.sh.tpl` reste utilisé par l'action `reboot` (jalon 1) ET `reboot_verified`.
+
+---
+
+## Task 2 : `classifyReboot` (pure, TDD)
+
+**Files:** Create `server/services/rebootVerify.ts`, `server/services/rebootVerify.test.ts`.
+
+- [ ] **Step 1 : Test (échec attendu)** — `server/services/rebootVerify.test.ts`
+
+```ts
+import { describe, it, expect } from "vitest";
+import { classifyReboot, parseBootIdBefore } from "./rebootVerify.js";
+
+describe("parseBootIdBefore", () => {
+ it("extrait le boot_id de la sortie du template", () => {
+ const raw = "===SU:BOOT_ID_BEFORE===\nabcd-1234\n===SU:REBOOT_NOW===\nreboot planifié";
+ expect(parseBootIdBefore(raw)).toBe("abcd-1234");
+ });
+ it("retourne null si absent", () => {
+ expect(parseBootIdBefore("rien")).toBeNull();
+ });
+});
+
+describe("classifyReboot", () => {
+ it("ok si la machine revient avec un boot_id différent", () => {
+ expect(classifyReboot({ beforeBootId: "A", afterBootId: "B", wentDown: true, cameBack: true }).status).toBe("ok");
+ });
+ it("boot_id_unchanged si même boot_id", () => {
+ expect(classifyReboot({ beforeBootId: "A", afterBootId: "A", wentDown: true, cameBack: true }).status).toBe("boot_id_unchanged");
+ });
+ it("ssh_never_went_down si la coupure n'a pas été observée", () => {
+ expect(classifyReboot({ beforeBootId: "A", afterBootId: null, wentDown: false, cameBack: false }).status).toBe("ssh_never_went_down");
+ });
+ it("machine_did_not_return si coupure mais pas de retour", () => {
+ expect(classifyReboot({ beforeBootId: "A", afterBootId: null, wentDown: true, cameBack: false }).status).toBe("machine_did_not_return");
+ });
+});
+```
+
+- [ ] **Step 2 : Lancer (échec)** — `rtk pnpm vitest run server/services/rebootVerify.test.ts` → FAIL.
+
+- [ ] **Step 3 : Implémenter le socle pur dans `server/services/rebootVerify.ts`**
+
+```ts
+// server/services/rebootVerify.ts
+import { runPlain, type SshCreds } from "../ssh/client.js";
+import type { RebootResult } from "@shared/types.js";
+
+export function parseBootIdBefore(raw: string): string | null {
+ const s = raw.indexOf("===SU:BOOT_ID_BEFORE===");
+ if (s === -1) return null;
+ const from = s + "===SU:BOOT_ID_BEFORE===".length;
+ const e = raw.indexOf("===SU:REBOOT_NOW===", from);
+ const id = raw.slice(from, e === -1 ? undefined : e).trim();
+ return id || null;
+}
+
+export interface RebootSignals {
+ beforeBootId: string | null;
+ afterBootId: string | null;
+ wentDown: boolean;
+ cameBack: boolean;
+}
+
+/** Détermine le statut d'un reboot vérifié (fonction pure). */
+export function classifyReboot(s: RebootSignals): { status: RebootResult["status"] } {
+ if (!s.wentDown) return { status: "ssh_never_went_down" };
+ if (!s.cameBack || s.afterBootId === null) return { status: "machine_did_not_return" };
+ if (s.beforeBootId !== null && s.afterBootId === s.beforeBootId) return { status: "boot_id_unchanged" };
+ return { status: "ok" };
+}
+
+async function readBootId(creds: SshCreds): Promise {
+ try {
+ const res = await runPlain(creds, "cat /proc/sys/kernel/random/boot_id");
+ const id = res.stdout.trim();
+ return id || null;
+ } catch {
+ return null; // connexion impossible (machine down)
+ }
+}
+const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
+
+export interface VerifyOptions {
+ beforeBootId: string | null;
+ requestedAt: string;
+ downTimeoutMs?: number; // détection de la coupure
+ upTimeoutMs?: number; // attente du retour
+ pollMs?: number;
+}
+
+/**
+ * Orchestration : attend la coupure SSH (machine qui reboote) puis le retour,
+ * relit le boot_id, et classe le résultat. Réseau ; non testé unitairement.
+ */
+export async function verifyReboot(creds: SshCreds, opt: VerifyOptions): Promise {
+ const downTimeoutMs = opt.downTimeoutMs ?? 60000;
+ const upTimeoutMs = opt.upTimeoutMs ?? 600000;
+ const pollMs = opt.pollMs ?? 5000;
+ const t0 = Date.now();
+
+ // Phase A : attendre que la machine devienne injoignable.
+ let wentDown = false;
+ let sshWentDownAt: string | null = null;
+ while (Date.now() - t0 < downTimeoutMs) {
+ const id = await readBootId(creds);
+ if (id === null) { wentDown = true; sshWentDownAt = new Date().toISOString(); break; }
+ await sleep(pollMs);
+ }
+
+ // Phase B : attendre le retour (seulement si on a vu la coupure).
+ let cameBack = false;
+ let sshCameBackAt: string | null = null;
+ let afterBootId: string | null = null;
+ if (wentDown) {
+ const tB = Date.now();
+ while (Date.now() - tB < upTimeoutMs) {
+ const id = await readBootId(creds);
+ if (id !== null) { cameBack = true; sshCameBackAt = new Date().toISOString(); afterBootId = id; break; }
+ await sleep(pollMs);
+ }
+ }
+
+ const { status } = classifyReboot({ beforeBootId: opt.beforeBootId, afterBootId, wentDown, cameBack });
+ const waitedSeconds = Math.round((Date.now() - t0) / 1000);
+ return {
+ beforeBootId: opt.beforeBootId,
+ afterBootId,
+ requestedAt: opt.requestedAt,
+ sshWentDownAt,
+ sshCameBackAt,
+ waitedSeconds,
+ status,
+ lastRebootDurationSeconds: status === "ok" ? waitedSeconds : undefined,
+ nextRecommendedWaitSeconds: status === "ok" ? Math.round(waitedSeconds * 1.5) + 30 : undefined,
+ };
+}
+```
+
+- [ ] **Step 4 : Lancer (succès)** — `rtk pnpm vitest run server/services/rebootVerify.test.ts` → PASS (6). `rtk pnpm check` → 0 erreur.
+
+- [ ] **Step 5 : (pas de commit)**
+
+---
+
+## Task 3 : Router l'action `reboot_verified` dans `execute.ts`
+
+**Files:** Modify `server/services/execute.ts`.
+
+- [ ] **Step 1 : Relire `server/services/execute.ts`** (TEMPLATE_FOR, flux, blocs Phase 1).
+
+- [ ] **Step 2 : Ajouter `reboot_verified` à `TEMPLATE_FOR`** (réutilise le même template que `reboot`)
+```ts
+ reboot_verified: "apt/reboot.sh.tpl",
+```
+
+- [ ] **Step 3 : Après l'exécution du script (raw obtenu), lancer la vérification pour `reboot_verified`** et attacher `result.reboot`. Importer en tête :
+```ts
+import { parseBootIdBefore, verifyReboot } from "./rebootVerify.js";
+import type { RebootResult } from "@shared/types.js";
+```
+Puis, après le bloc qui calcule `status`/`raw` et avant la construction de `result` (ou juste après, en enrichissant `result`), ajouter une branche :
+```ts
+ let rebootResult: RebootResult | undefined;
+ if (action === "reboot_verified") {
+ const beforeBootId = parseBootIdBefore(raw);
+ rebootResult = await verifyReboot(getCreds(m), { beforeBootId, requestedAt: startedAt });
+ // Le statut de l'exécution suit la vérif : ok si reboot ok, sinon error.
+ if (rebootResult.status !== "ok") status = "error";
+ }
+```
+Puis dans la construction de `result`, ajouter `...(rebootResult ? { reboot: rebootResult } : {})` ; et conserver `rebootRequiredAfterRun` existant.
+
+> ⚠️ `verifyReboot` est **long** (jusqu'à plusieurs minutes). C'est acceptable : `runAction` est déjà lancé en arrière-plan (la route POST renvoie 202). La sortie live reste streamée pendant l'attente n'est pas nécessaire ; on peut publier un message d'attente : `outputHub.publish(machineId, "\n[reboot] attente du redémarrage...\n")` avant `verifyReboot`.
+
+- [ ] **Step 4 : Persister le délai adaptatif** (optionnel, simple) : après `verifyReboot`, si `rebootResult.lastRebootDurationSeconds`, l'écrire dans un event :
+```ts
+ if (rebootResult.status === "ok") {
+ recordEvent({ machineId, eventType: "reboot_verified", severity: "info", executionId,
+ message: `Reboot vérifié en ${rebootResult.waitedSeconds}s (boot_id changé)` });
+ }
+```
+(Le stockage en colonne dédiée `machine_state` peut venir plus tard ; l'event suffit au MVP.)
+
+- [ ] **Step 5 : Vérifier** — `rtk pnpm check && rtk pnpm test` → 0 erreur, tests verts. Les blocs Phase 1 (executions/reports/rawArtifacts/state/event) restent intacts ; `reboot` (jalon 1) inchangé.
+
+- [ ] **Step 6 : (pas de commit)**
+
+---
+
+## Task 4 : Vérification finale SJ-3
+
+- [ ] **Step 1 :** `rtk pnpm check && rtk pnpm test && rtk pnpm build` → tout vert.
+- [ ] **Step 2 : Boot smoke** (DB jetable) → `/health` OK. Nettoyer.
+- [ ] **Step 3 :** Reporter. **Vérif live indispensable** : `reboot_verified` réel sur une machine de test (la boucle réseau attente-coupure/retour + comparaison `boot_id` ne peut être validée qu'en conditions réelles). **Ne pas committer.**
+
+---
+
+## Self-Review (couverture SJ-3)
+- `apt/reboot.sh.tpl` capture `boot_id` → Task 1. ✓
+- Orchestration backend (attente coupure → reconnexion délai adaptatif → relecture boot_id) → Task 2 (`verifyReboot`). ✓
+- `RebootResult` + statuts (`ok`/`ssh_never_went_down`/`machine_did_not_return`/`boot_id_unchanged`/`timeout`) → `classifyReboot` (TDD) + `verifyReboot`. ✓
+- Délai adaptatif `lastRebootDurationSeconds`→`nextRecommendedWaitSeconds` → `verifyReboot`. ✓
+- Conserve l'action `reboot` jalon 1 → Task 3 (nouvelle action distincte). ✓
+
+Décision : la boucle réseau utilise des timeouts bornés (down ≤ 60 s, up ≤ 600 s, poll 5 s) ; seule `classifyReboot` (+`parseBootIdBefore`) est testée unitairement, l'orchestration est validée en live. `timeout` (statut) est couvert par `machine_did_not_return` quand le retour n'arrive pas dans `upTimeoutMs` (mêmes conséquences ; un raffinement `timeout` explicite est notable mais non bloquant au MVP).
diff --git a/liste_taches.md b/liste_taches.md
new file mode 100644
index 0000000..3fdae4f
--- /dev/null
+++ b/liste_taches.md
@@ -0,0 +1,148 @@
+# Liste des tâches projet — system_update
+
+> **But** : garder une vue claire de la numérotation des tâches et de leur périmètre.
+
+---
+
+## Tâches existantes
+
+### Tâche 1.9 — Architecture BDD cible
+
+Fichier : `tache1.9.md`
+
+Validation : `validation_tache1.9.md`
+
+Périmètre :
+
+- schéma SQLite/Drizzle cible ;
+- migration future PostgreSQL ;
+- machines, snapshots, exécutions ;
+- logs/rapports/messages importants ;
+- Docker, post-install, jobs ;
+- Hermes/MCP ;
+- métriques, nettoyage, découverte ;
+- préférences frontend ;
+- app locale future.
+
+### Tâche 2 — Moteur de templates et contrats JSON
+
+Fichier : `tache2.md`
+
+Périmètre :
+
+- APT update/analyse/upgrade/reboot ;
+- Docker Compose ;
+- scripts custom/post-install ;
+- profils OS et type de machine ;
+- JSON canoniques ;
+- intégration Hermes/MCP.
+
+Validation : `validation_tache2.md`
+
+### Tâche 3 — Frontend web, tuiles, layout, paramètres
+
+Fichier : `tache3.md`
+
+Validation : `validation_tache3.md`
+
+Périmètre :
+
+- tuiles machine extensibles ;
+- layout web global header/Hermes/centre/terminal/footer ;
+- volet Hermes ;
+- terminal droit ;
+- mode smartphone ;
+- paramètres app ;
+- favicon, icônes smartphone, icônes SVG spécifiques ;
+- brief icônes : `consigne_icon.md`.
+
+### Tâche 4 — Scripts post-install et installateurs
+
+Fichier : `tache4.md`
+
+Validation : `validation_tache4.md`
+
+Périmètre :
+
+- profils post-install ;
+- Docker officiel ;
+- partage Samba/NFS/wsdd2 ;
+- dev-tools, domotique, ESP/PlatformIO ;
+- détection hardware ;
+- drivers/firmware ;
+- métriques simples ;
+- benchmark.
+
+### Tâche 5 — Backend, historique JSON et automatisations
+
+Fichier : `tache5.md`
+
+Validation : `validation_tache5.md`
+
+Périmètre :
+
+- stockage JSON ;
+- état courant machine ;
+- logs/rapports/messages ;
+- schedules/jobs ;
+- API webapp/Hermes/app locale ;
+- rétention.
+
+### Tâche 6 — Hermes, MCP, skills et messagerie
+
+Fichier : `tache6.md`
+
+Validation : `validation_tache6.md`
+
+Périmètre :
+
+- volet Hermes web ;
+- API Hermes ;
+- MCP HTTP ;
+- skill `system-update-ops` ;
+- messagerie/TUI ;
+- accès rapports/logs réduits.
+
+### Tâche 7 — Optimisation, métriques, nettoyage, sécurité
+
+Fichier : `tache7.md`
+
+Validation : `validation_tache7.md`
+
+Périmètre :
+
+- footer métriques ;
+- métriques simples par machine ;
+- optimisation tokens Hermes ;
+- nettoyage DB/logs ;
+- découverte SSH ;
+- sécurité mots de passe/secrets ;
+- smartphone à brainstormer.
+
+### Tâche 8 — App locale Rust/GNOME
+
+Fichier : `tache8.md`
+
+Validation : `validation_tache8.md`
+
+Périmètre :
+
+- application native Rust ;
+- GTK4/libadwaita ;
+- API commune avec backend ;
+- thème Gruvbox GNOME ;
+- mode sans navigateur ;
+- sécurité token locale ;
+- cache lecture seule ;
+- notifications desktop.
+
+---
+
+## Fichiers transverses
+
+- `validation_tache2.md` : gate de validation tâche 2.
+- `validation_tache1.9.md`, `validation_tache3.md` à `validation_tache8.md` : gates des autres tâches.
+- `coherence_taches.md` : revue de cohérence globale et ordre de développement recommandé.
+- `consigne_icon.md` : brief de création icônes SVG/favicon/smartphone.
+- `design_system/consigne_design_system.md` : règles design system web/Gruvbox.
+- `design_system/tokens/tokens.gnome.css` : base thème pour future app GNOME.
diff --git a/plan_3.md b/plan_3.md
new file mode 100644
index 0000000..0838b45
--- /dev/null
+++ b/plan_3.md
@@ -0,0 +1,164 @@
+# Plan de développement — Tâche 3
+
+> Suivi vivant du développement lié à `tache3.md`.
+> Objectif : faire évoluer la webapp vers les tuiles machine extensibles, paramètres frontend et layout dashboard cible.
+
+---
+
+## 0. Position actuelle
+
+- Date de démarrage dev : 2026-06-05.
+- État : démarrage après validation tâche 3.
+- Validation disponible : `validation_tache3.md`, verdict **accepté avec réserves**.
+- Périmètre immédiat : webapp React, design system, tuiles machine.
+
+---
+
+## 1. Réserves à traiter
+
+- [ ] Clarifier logos officiels : favicon/app icon original, logos officiels uniquement pour types/outils si autorisés.
+- [ ] Ajouter ou décider le composant `Checkbox`/sélection profil post-install.
+- [ ] Prévoir spec mobile dédiée.
+- [ ] Aligner largeurs min/max Hermes/terminal avec design system.
+- [ ] Ajouter état machine erreur/hors ligne dans les maquettes et l'UI.
+- [ ] Trancher sauvegarde paramètres : auto-save ou bouton save.
+- [ ] Prévoir composants `Select`/`Dropdown` design system.
+
+---
+
+## 2. Jalons
+
+### 3.0 — Reprise et cadrage
+
+- [x] Vérifier que `plan_8.md` est à jour.
+- [x] Mettre la tâche 8 en pause.
+- [x] Relire `tache3.md` et `validation_tache3.md`.
+- [x] Inspecter `MachineTile`, `Dashboard`, `App`, `ui-kit`, CSS.
+
+### 3.1 — Tuile machine compacte/extensible
+
+- [x] Remplacer les boutons texte système par `IconButton`.
+- [x] Utiliser `StatusLed` du ui-kit.
+- [x] Afficher OS/type/status/dernier check de façon compacte.
+- [x] Ajouter sections repliables Docker et Post-install.
+- [x] Ajouter état erreur/hors ligne lisible dans la tuile.
+- [x] Ajouter CSS dédié sans inline styles excessifs.
+- [x] Vérifier TypeScript/build.
+
+### 3.2 — Layout global webapp
+
+- [x] Ajouter header webapp.
+- [x] Ajouter footer/barre de tâche avec métriques minimales.
+- [x] Vérifier que Hermes/centre/terminal ne se chevauchent pas.
+- [x] Préparer largeurs bornées et future redimension.
+
+### 3.3 — Paramètres frontend
+
+- [x] Créer vue Paramètres.
+- [x] Apparence/thème/zoom/tuiles.
+- [x] Layout volets Hermes/terminal.
+- [x] Scripts custom, Docker roots, nettoyage logs.
+- [x] Persistance backend à préparer, pas localStorage seul.
+
+### 3.4 — Mode smartphone
+
+- [ ] Brainstorm UX dédiée.
+- [ ] Décider onglets/bottom nav.
+- [ ] Définir vues prioritaires mobile.
+
+---
+
+## 3. Avancement du tour en cours
+
+- [x] Contexte tâche 8 vérifié.
+- [x] Tâche 3 relue.
+- [x] Réserves de validation listées.
+- [x] Refonte `MachineTile` premier incrément terminée.
+
+---
+
+## 4. Premier incrément — Tuile machine
+
+Fichiers modifiés :
+
+- `client/src/features/machines/MachineTile.tsx`
+- `client/src/styles/app.css`
+- `client/src/components/ui-kit.tsx`
+
+Ce qui est en place :
+
+- tuile compacte plus dense ;
+- actions système en icônes avec tooltips ;
+- statut via `StatusLed` ;
+- résumé updates/reboot/dernier check ;
+- alerte visible pour état `error` ou `unknown` ;
+- sections repliables Docker et Post-install ;
+- placeholders UI tant que les données Docker/Post-install ne sont pas exposées par le backend ;
+- nouveaux alias icônes utiles dans `ICON_MAP`.
+
+Vérifications :
+
+- `tsc --noEmit` : OK.
+- `vitest run` : OK, 16 fichiers de test, 42 tests.
+- `vite build && tsup` : OK.
+
+Note :
+
+- Vite signale un warning de chunk JS > 500 kB. Non bloquant pour ce jalon, à traiter plus tard par découpage dynamique si nécessaire.
+
+---
+
+## 5. Deuxième incrément — Layout global
+
+Fichiers modifiés :
+
+- `client/src/App.tsx`
+- `client/src/panels/Dashboard.tsx`
+- `client/src/lib/api.ts`
+- `client/src/styles/app.css`
+
+Ce qui est en place :
+
+- header webapp avec identité, résumé machines/updates/jobs/erreurs et toggle thème ;
+- footer/barre de tâche style terminal avec machines, apt, jobs, métriques process et load ;
+- appel frontend vers `/api/system/metrics` ;
+- largeurs Hermes et terminal bornées avec `clamp(...)` ;
+- Dashboard remonte un résumé à `App`.
+
+Vérifications :
+
+- `tsc --noEmit` : OK.
+- `vitest run` : OK, 16 fichiers de test, 42 tests.
+- `vite build && tsup` : OK.
+
+Note :
+
+- warning Vite chunk > 500 kB toujours présent, non bloquant.
+
+---
+
+## 6. Troisième incrément — Paramètres frontend
+
+Fichiers modifiés :
+
+- `client/src/App.tsx`
+- `client/src/panels/SettingsModal.tsx`
+- `client/src/styles/app.css`
+
+Ce qui est en place :
+
+- bouton Paramètres dans le header ;
+- modale Paramètres avec navigation latérale ;
+- catégories Apparence, Tuiles, Volets, Docker, Scripts, Hermes, Terminal, Nettoyage ;
+- contrôles prêts à brancher à une future API `/api/settings` ;
+- mention explicite côté UI que la persistance backend reste à venir.
+
+Vérifications :
+
+- `tsc --noEmit` : OK.
+- `vitest run` : OK, 16 fichiers de test, 42 tests.
+- `vite build && tsup` : OK.
+
+Note :
+
+- warning Vite chunk > 500 kB toujours présent, non bloquant.
diff --git a/plan_8.md b/plan_8.md
new file mode 100644
index 0000000..eb031b6
--- /dev/null
+++ b/plan_8.md
@@ -0,0 +1,298 @@
+# Plan de développement — Tâche 8
+
+> Suivi vivant du développement lié à `tache8.md`.
+> Objectif : préparer puis développer l'app locale Rust/GNOME sans casser la webapp serveur.
+
+---
+
+## 0. Position actuelle
+
+- Date de démarrage : 2026-06-05.
+- État : développement validé par l'utilisateur.
+- Décision : le gate qui bloquait le code Rust est levé par validation utilisateur du 2026-06-05. Le scaffold Rust peut démarrer, avec une approche progressive centrée sur le client API avant l'UI GTK/libadwaita.
+- Dossier dédié Rust : `app_rust/system-update-gnome/`.
+
+---
+
+## 1. Vision suffisante pour démarrer ?
+
+Oui pour un premier incrément.
+
+La direction est claire :
+
+- l'app locale Rust/GNOME est un client du backend `system_update` ;
+- elle ne fait pas de SSH direct au MVP ;
+- elle consomme les mêmes JSON que la webapp ;
+- elle doit découvrir les capacités du serveur avant d'afficher ses actions ;
+- les secrets machines restent côté backend ;
+- le token client local sera stocké côté app via trousseau système, quand le scaffold Rust sera autorisé.
+
+Point d'environnement :
+
+- Rust est installé.
+- GTK4/libadwaita ne sont pas encore visibles via `pkg-config`, donc l'UI GNOME complète attendra les paquets système de développement.
+
+---
+
+## 2. Jalons
+
+### 8.0 — API commune minimale
+
+- [x] Relire `tache8.md` et `validation_tache8.md`.
+- [x] Identifier le premier endpoint utile pour app locale.
+- [x] Ajouter le type partagé `ServerCapabilities`.
+- [x] Exposer `GET /api/capabilities`.
+- [x] Ajouter un test du contrat capabilities.
+- [x] Vérifier TypeScript/tests ciblés.
+
+### 8.1 — Préparation sécurité client local
+
+- [x] Définir le modèle `api_clients` côté backend.
+- [x] Prévoir scopes : lecture seule, opérateur, admin, debug.
+- [x] Prévoir révocation de token.
+- [x] Documenter stockage token app locale via keyring.
+- [x] Préparer un middleware d'auth API après validation du mode d'amorçage admin.
+- [x] Ajouter une commande locale de création de token.
+- [ ] Activer le middleware sur les routes après choix du mode bootstrap admin.
+
+### 8.2 — Contrat API app locale
+
+- [ ] Stabiliser endpoints machines/state/metrics/hardware.
+- [ ] Stabiliser snapshots/executions/reports/messages.
+- [ ] Clarifier pagination et erreurs structurées.
+- [ ] Clarifier WebSocket/SSE pour sortie live.
+- [x] Ajouter `/api/system/status`.
+- [x] Ajouter `/api/system/metrics`.
+
+### 8.3 — Scaffold Rust/GNOME
+
+- [x] Créer workspace Rust après validation.
+- [x] Utiliser un sous-dossier dédié : `app_rust/system-update-gnome/`.
+- [ ] Choisir GTK4/libadwaita direct ou Relm4.
+- [x] Implémenter configuration URL serveur.
+- [x] Implémenter test de connexion via `/api/capabilities`.
+- [ ] Stocker token via keyring.
+- [x] Isoler la stratégie token dans `src/token_store.rs`.
+
+### 8.4 — UI native MVP
+
+- [x] Première fenêtre GTK/libadwaita derrière feature `gui`.
+- [x] Champ URL serveur.
+- [x] Boutons `Capabilities`, `Status`, `Metrics`.
+- [x] Zone résultat JSON.
+- [ ] HeaderBar + Sidebar complète.
+- [ ] Liste machines.
+- [ ] Tuile machine compacte.
+- [ ] Détail machine.
+- [ ] Lancement `apt_update_analyze`.
+- [ ] Lecture rapports/logs réduits.
+- [ ] Notifications desktop simples.
+
+---
+
+## 3. Avancement du tour en cours
+
+- [x] Le repo a été inspecté.
+- [x] Le manque prioritaire est identifié : endpoint capabilities absent.
+- [x] Patch API capabilities appliqué.
+- [x] Vérification TypeScript passée.
+- [x] Vérification Vitest ciblée passée.
+- [x] Vérification Vitest complète passée.
+
+---
+
+## 4. Résultat du premier incrément
+
+Fichiers ajoutés/modifiés pour le démarrage tâche 8 :
+
+- `shared/types.ts` : ajout du contrat partagé `ServerCapabilities`.
+- `server/services/capabilities.ts` : génération du JSON de capabilities.
+- `server/services/capabilities.test.ts` : test du contrat capabilities.
+- `server/routes/index.ts` : exposition de `GET /api/capabilities`.
+- `plan_8.md` : suivi d'avancement.
+
+Vérifications :
+
+- `tsc --noEmit` : OK.
+- `vitest run server/services/capabilities.test.ts` : OK.
+- `vitest run` : OK, 11 fichiers de test, 25 tests.
+
+Décision de suite :
+
+- Continuer par `8.1` et `8.2` : sécurité client local, scopes de token, erreurs structurées et endpoints stables.
+- Débuter le scaffold Rust dans `app_rust/system-update-gnome`.
+- Garder l'UI GTK/libadwaita pour un incrément suivant, car les bibliothèques système ne sont pas encore installées.
+
+---
+
+## 5. Validation utilisateur du démarrage dev
+
+- [x] Demande reçue : "ok je valide pour que tu commences le dev".
+- [x] `tache8.md` mis à jour : la tâche passe de design futur à développement progressif.
+- [x] `validation_tache8.md` mis à jour : le code Rust est autorisé dans `app_rust/system-update-gnome`.
+- [x] Scaffold Rust minimal créé dans `app_rust/system-update-gnome`.
+- [x] `.gitignore` local ajouté pour ignorer `target/`.
+- [x] `cargo fmt` passé.
+- [x] `cargo test` passé : 7 tests.
+
+---
+
+## 6. Jalon 8.1 — Sécurité client local
+
+Fichiers ajoutés/modifiés :
+
+- `shared/types.ts` : scopes API et vues client API sans secret.
+- `server/db/schema.ts` : table `api_clients`.
+- `server/db/migrations/0001_api_clients.sql` : migration SQLite.
+- `server/crypto/apiTokens.ts` : génération, préfixe, hash HMAC, vérification.
+- `server/services/apiClients.ts` : création/liste/révocation côté service.
+
+Vérifications :
+
+- `tsc --noEmit` : OK.
+- `vitest run server/crypto/apiTokens.test.ts server/services/apiClients.test.ts` : OK.
+- `vitest run` : OK, 13 fichiers de test, 32 tests.
+- migration SQLite temporaire : OK.
+
+Décision :
+
+- Ne pas exposer encore une route publique de création de token sans mécanisme admin.
+- Garder `authTokens: false` dans `/api/capabilities` tant que l'auth n'est pas réellement activée.
+
+Complément :
+
+- `server/auth/apiAuth.ts` : middleware `requireApiScope` prêt à brancher.
+- `server/auth/apiAuth.test.ts` : tests extraction Bearer.
+- `app_rust/system-update-gnome/src/token_store.rs` : séparation token CLI/env/futur trousseau.
+- `app_rust/system-update-gnome/docs/token-storage.md` : choix et règles du stockage token.
+- `server/cli/createApiClient.ts` : création locale d'un token API sans route publique.
+- `vitest run` : OK, 16 fichiers de test, 42 tests.
+- `cargo test` : OK, 11 tests.
+
+---
+
+## 9. Passe compilation/test
+
+Dernière passe lancée après validation du dossier projet :
+
+- `tsc --noEmit` : OK.
+- `vitest run` : OK, 16 fichiers de test, 42 tests.
+- `vite build && tsup` : OK.
+- `cargo fmt` : OK.
+- `cargo test` : OK, 11 tests.
+- `cargo build` : OK, sans warning après utilisation de l'identité keyring dans l'aide CLI.
+
+---
+
+## 10. Test réel client Rust ↔ backend
+
+Backend temporaire lancé avec :
+
+- `SU_DB_PATH=/tmp/system-update-rust-client-test.db`.
+- `SU_REPORTS_DIR=/tmp/system-update-rust-client-reports`.
+- `SU_PORT=8787`.
+
+Commandes Rust testées :
+
+- `cargo run -- capabilities` : OK, JSON capabilities reçu.
+- `cargo run -- status` : OK, JSON status reçu.
+- `cargo run -- metrics` : OK, JSON metrics reçu.
+
+Nettoyage :
+
+- backend temporaire arrêté après test ;
+- vérification `/health` après arrêt : connexion refusée, donc port libéré.
+
+---
+
+## 11. Début interface graphique Rust
+
+Décision :
+
+- UI GTK4/libadwaita ajoutée derrière la feature Cargo `gui`.
+- Le client CLI reste compilable sans GTK.
+- Lancement prévu : `cargo run --features gui -- gui`.
+
+Pré-requis système manquants sur la machine au moment du test :
+
+- `pkg-config --modversion gtk4` : paquet absent.
+- `pkg-config --modversion libadwaita-1` : paquet absent.
+
+Installation à faire dans un terminal utilisateur :
+
+- `sudo apt install libgtk-4-dev libadwaita-1-dev`.
+
+État :
+
+- Code GUI ajouté.
+- Crates GTK/libadwaita résolues via Cargo.
+- `cargo test` sans feature GUI : OK, 12 tests.
+- `cargo build` sans feature GUI : OK.
+- `cargo check --features gui` : bloqué par paquets système manquants (`gtk4`, `pango`, `cairo`, `glib-2.0`, `gio-2.0`, `gdk-pixbuf-2.0`, `graphene-gobject-1.0`).
+
+Commande utilisateur à lancer dans un terminal interactif :
+
+```bash
+sudo apt install libgtk-4-dev libadwaita-1-dev
+```
+
+Puis retester :
+
+```bash
+cd /home/gilles/Documents/projet/system_update/app_rust/system-update-gnome
+cargo check --features gui
+cargo run --features gui -- --server http://10.0.1.137:8787 gui
+```
+
+Correction suivante :
+
+- Bug observé : GTK recevait `--server` et affichait `Option inconnue --server`.
+- Cause : `adw::Application::run()` relisait les arguments du processus.
+- Fix : lancement GUI via `run_with_args::<&str>(&[])`.
+- Warning supprimé : import GTK inutilisé.
+- Vérification : `cargo check --features gui` OK, `cargo test` OK.
+- Layout natif aligné webapp : Hermes gauche, Machines centre, terminal API droit, barre de tâche basse.
+- `/api/machines` consommé par la GUI pour remplir la zone centrale.
+- Commande CLI `machines` ajoutée pour tester le même endpoint hors GUI.
+
+---
+
+## 12. Pause tâche 8
+
+- Date : 2026-06-05.
+- Décision utilisateur : terminer la tâche 8 plus tard.
+- État au moment de la pause :
+ - client Rust CLI fonctionnel ;
+ - commandes `capabilities`, `status`, `metrics`, `machines` ;
+ - première GUI GTK/libadwaita disponible derrière `--features gui` ;
+ - layout GUI rapproché de la webapp : Hermes gauche, Machines centre, terminal droit, barre basse ;
+ - compilation/test GUI OK après installation des paquets système ;
+ - prochaine reprise : améliorer UX native, keyring, vrai modèle machines/actions.
+
+---
+
+## 7. Notes techniques
+
+- Le backend actuel expose déjà `/api/machines`, actions APT/reboot et WebSocket de sortie machine.
+- L'app locale a besoin de savoir quelles fonctions sont réellement disponibles pour masquer les fonctions futures : Hermes, Docker, post-install, SSH interactif, settings, etc.
+- `GET /api/capabilities` doit retourner un JSON stable, sans secret, exploitable par webapp, Hermes et future app Rust.
+
+---
+
+## 8. Jalon 8.2 — Endpoints système app locale
+
+Fichiers ajoutés/modifiés :
+
+- `shared/types.ts` : types `SystemStatus` et `SystemMetrics`.
+- `server/services/system.ts` : status et métriques process/hôte.
+- `server/routes/index.ts` : routes `GET /api/system/status` et `GET /api/system/metrics`.
+- `server/services/capabilities.ts` : capabilities enrichies avec les endpoints système.
+- `app_rust/system-update-gnome` : commandes CLI `status` et `metrics`.
+
+Vérifications :
+
+- `tsc --noEmit` : OK.
+- `vitest run server/services/system.test.ts server/services/capabilities.test.ts` : OK.
+- `vitest run` : OK, 14 fichiers de test, 35 tests.
+- `cargo fmt` : OK.
+- `cargo test` : OK, 8 tests.
diff --git a/tache1.9.md b/tache1.9.md
new file mode 100644
index 0000000..4034ac1
--- /dev/null
+++ b/tache1.9.md
@@ -0,0 +1,1335 @@
+# Consigne de dev — Architecture base de données cible
+
+> **Type** : mission d'**analyse + design architecture BDD** (PAS d'implémentation).
+> **Langue** : français.
+> **Livrable final attendu** : spec de schéma de données prête à passer en plan d'implémentation.
+
+---
+
+## 0. Contexte
+
+Le projet `system_update` dispose actuellement d'un schéma SQLite minimal :
+
+- `machines`
+- `snapshots`
+- `executions`
+
+Ce schéma suffit au jalon 1, mais les tâches suivantes ajoutent de nouveaux besoins :
+
+- APT update/analyse, diff avant/après, reboot vérifié ;
+- Docker Compose, roots, stacks, pull-check, apply, prune ;
+- profils post-install et installateurs externes ;
+- sauvegarde de tous les JSON machine ↔ webapp ;
+- automatisations planifiées ;
+- Hermes/MCP/skills/rapports globaux ;
+- optimisation tokens ;
+- nettoyage DB/logs ;
+- découverte de machines SSH ;
+- sécurité credentials et host keys ;
+- observabilité backend.
+
+Objectif de cette tâche : concevoir la **meilleure structure BDD cible**, compatible avec le MVP SQLite/Drizzle, mais prévue pour évoluer vers PostgreSQL si le projet grandit.
+
+---
+
+## 1. Analyse du schéma actuel
+
+Schéma actuel :
+
+```text
+machines
+├─ id
+├─ name
+├─ hostname
+├─ port
+├─ os_family
+├─ username
+├─ enc_password
+├─ enc_sudo_password
+├─ apt_proxy_mode
+├─ apt_proxy_url
+├─ status
+├─ last_checked_at
+└─ created_at
+
+snapshots
+├─ id
+├─ machine_id
+├─ checked_at
+├─ status
+└─ payload_json
+
+executions
+├─ id
+├─ machine_id
+├─ action
+├─ mode
+├─ started_at
+├─ finished_at
+├─ status
+├─ result_json
+├─ report_path
+└─ raw_log_path
+```
+
+Points forts :
+
+- simple ;
+- compatible jalon 1 ;
+- `payload_json` et `result_json` permettent une évolution rapide ;
+- séparation snapshot/exécution correcte ;
+- suppression cascade par machine.
+
+Limites :
+
+- credentials mélangés à la table machine ;
+- pas de table événements/timeline ;
+- pas de table jobs/schedules persistants ;
+- pas de configuration Docker/post-install par machine ;
+- pas de modèle de templates/scripts ;
+- pas de demandes d'action Hermes en attente de validation ;
+- pas de métriques système/token usage ;
+- pas de rétention/purge ;
+- pas de host keys SSH ;
+- pas de tags/groupes machines ;
+- pas de notion de snapshot kind ;
+- pas de version de schéma JSON.
+
+Conclusion : le modèle actuel doit être conservé comme base, mais élargi en couches spécialisées.
+
+---
+
+## 2. Principe d'architecture BDD
+
+### Recommandation MVP
+
+Continuer en **SQLite + Drizzle** pour l'application locale/mono-instance :
+
+- simple à déployer ;
+- cohérent avec le jalon 1 ;
+- suffisant pour quelques dizaines/centaines de machines ;
+- WAL déjà activé ;
+- backups faciles.
+
+### Prévoir PostgreSQL plus tard
+
+Garder une architecture compatible PostgreSQL :
+
+- IDs text/UUID ;
+- dates ISO ou timestamp abstrait côté code ;
+- JSON stocké en `text` côté SQLite, futur `jsonb` côté PostgreSQL ;
+- tables normalisées pour les recherches fréquentes ;
+- payload complet conservé en JSON ;
+- index explicites.
+
+### Règle structurante
+
+Chaque donnée métier importante existe sous deux formes :
+
+1. **JSON complet archivé** : vérité canonique, relecture, Hermes, audit.
+2. **Colonnes indexées dérivées** : recherche, filtres UI, badges, performances.
+
+Exemple :
+
+```text
+snapshots.payload_json ← JSON complet
+snapshots.kind/status/checked_at ← filtres rapides
+machine_state.apt_updates_count ← affichage tuile rapide
+```
+
+---
+
+## 3. Architecture cible — groupes de tables
+
+```text
+Core
+├─ machines
+├─ machine_credentials
+├─ machine_host_keys
+├─ machine_tags
+├─ tags
+├─ machine_state
+├─ machine_hardware
+├─ machine_metrics_latest
+├─ app_settings
+├─ user_preferences
+└─ machine_ui_state
+
+JSON / Historique
+├─ snapshots
+├─ executions
+├─ machine_events
+├─ important_messages
+├─ reports
+├─ raw_artifacts
+└─ ssh_terminal_sessions
+
+APT
+├─ apt_planned_packages
+├─ apt_applied_packages
+└─ apt_errors
+
+Docker
+├─ docker_settings
+├─ docker_compose_roots
+├─ docker_compose_stacks
+├─ docker_stack_services
+└─ docker_image_events
+
+Post-install / scripts
+├─ install_profiles
+├─ install_recipes
+├─ install_recipe_versions
+├─ machine_profile_state
+└─ script_variables_presets
+
+Jobs / automatisations
+├─ jobs
+├─ schedules
+├─ machine_locks
+└─ action_requests
+
+Hermes / MCP
+├─ hermes_sessions
+├─ hermes_runs
+├─ hermes_usage
+├─ mcp_audit_log
+└─ api_clients
+
+Optimisation / maintenance
+├─ system_metrics
+├─ cleanup_runs
+├─ discovery_scans
+└─ discovery_candidates
+```
+
+---
+
+## 4. Schéma ASCII global
+
+```text
+┌──────────────┐
+│ machines │
+└──────┬───────┘
+ │ 1
+ ├──────────────┬───────────────┬───────────────┐
+ │ │ │ │
+ ▼ ▼ ▼ ▼
+┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
+│ credentials │ │ host_keys │ │ machine_state│ │ docker_config│
+└──────────────┘ └──────────────┘ └──────────────┘ └──────┬───────┘
+ │
+ ▼
+ ┌──────────────┐
+ │ compose │
+ │ roots/stacks │
+ └──────────────┘
+
+┌──────────────┐ ┌──────────────┐ ┌──────────────┐
+│ snapshots │◄────►│ executions │◄────►│ reports │
+└──────┬───────┘ └──────┬───────┘ └──────────────┘
+ │ │
+ ▼ ▼
+┌──────────────┐ ┌──────────────┐
+│ apt planned │ │ apt applied │
+│ docker state │ │ docker events│
+└──────────────┘ └──────────────┘
+
+┌──────────────┐ ┌──────────────┐ ┌──────────────┐
+│ schedules │─────►│ jobs │─────►│ machine_locks│
+└──────────────┘ └──────────────┘ └──────────────┘
+
+┌──────────────┐ ┌──────────────┐
+│ hermes_runs │─────►│ hermes_usage │
+└──────────────┘ └──────────────┘
+```
+
+---
+
+## 5. Tables core
+
+### `machines`
+
+Table publique machine, sans secret.
+
+```text
+id text primary key
+name text not null
+hostname text not null
+port integer not null default 22
+os_family text not null
+os_version text
+os_codename text
+arch text
+machine_kind text -- physical | vm | proxmox_host | lxc | raspberry_pi | workstation | unknown
+virtualization text -- none | qemu | kvm | lxc | docker | vmware | virtualbox | ...
+hardware_profile text -- generic_vm | baremetal_server | raspberry_pi | gpu_server | proxmox_host | ...
+username text not null
+status text not null
+created_at text not null
+updated_at text not null
+last_seen_at text
+deleted_at text null
+```
+
+Évolutions :
+
+- `deleted_at` permet une suppression douce future ;
+- `hostname` peut être IP ou DNS ;
+- `last_seen_at` utile pour statut.
+- `os_family` et `machine_kind` sont deux dimensions différentes : Debian VM, Debian physique, Proxmox hôte, Raspberry Pi OS, etc.
+- ces champs peuvent être choisis manuellement à l'ajout puis corrigés par `machine_probe`.
+
+### `machine_credentials`
+
+Secrets chiffrés séparés de `machines`.
+
+```text
+machine_id text primary key references machines(id)
+auth_method text not null -- password | ssh_key
+enc_password text
+enc_sudo_password text
+enc_private_key text
+enc_key_passphrase text
+sudo_mode text not null -- same_as_ssh | separate | none
+created_at text not null
+updated_at text not null
+last_test_at text
+status text -- ok | error | unknown
+```
+
+Règles :
+
+- jamais exposée via API publique ;
+- audit obligatoire quand lue/déchiffrée ;
+- migration possible depuis `machines.enc_password`.
+
+### `machine_host_keys`
+
+Validation host key SSH.
+
+```text
+id text primary key
+machine_id text references machines(id)
+hostname text not null
+port integer not null
+key_type text
+fingerprint_sha256 text not null
+public_key text
+status text not null -- approved | changed | rejected | unknown
+first_seen_at text not null
+last_seen_at text not null
+```
+
+### `machine_state`
+
+État courant dérivé pour tuiles et dashboard.
+
+```text
+machine_id text primary key
+status text not null
+apt_status text
+apt_updates_count integer default 0
+apt_reboot_required integer default 0
+apt_last_analyze_at text
+docker_status text
+docker_installed integer default 0
+docker_stacks_count integer default 0
+docker_updates_count integer default 0
+docker_prune_available integer default 0
+post_install_status text
+metrics_last_collected_at text
+cpu_load1 real
+memory_used_percent real
+root_used_percent real
+disk_warnings_count integer default 0
+hardware_warnings_count integer default 0
+running_job_id text
+last_error_kind text
+last_error_message text
+updated_at text not null
+```
+
+Objectif :
+
+- éviter de parser les derniers snapshots à chaque affichage ;
+- les tuiles lisent cette table.
+
+### `tags` et `machine_tags`
+
+Pour groupes, schedules et filtres.
+
+```text
+tags(id, name, color, created_at)
+machine_tags(machine_id, tag_id)
+```
+
+Exemples :
+
+- `debian`
+- `proxmox`
+- `docker`
+- `domotique`
+- `prod`
+- `lab`
+
+### `machine_hardware`
+
+Dernier inventaire matériel détecté.
+
+```text
+machine_id text primary key references machines(id)
+probe_snapshot_id text references snapshots(id)
+cpu_model text
+cpu_cores integer
+memory_bytes integer
+gpus_json text -- vendor/model/driver/recommendations
+disks_json text -- name/type/size/rotational/smart
+network_json text
+firmware_json text
+driver_json text
+warnings_json text
+updated_at text not null
+```
+
+### `machine_metrics_latest`
+
+Dernières métriques simples par machine.
+
+```text
+machine_id text primary key references machines(id)
+snapshot_id text references snapshots(id)
+collected_at text not null
+cpu_load1 real
+cpu_load5 real
+cpu_cores integer
+memory_total_bytes integer
+memory_used_bytes integer
+memory_available_bytes integer
+memory_used_percent real
+filesystems_json text not null
+root_used_percent real
+warnings_json text
+```
+
+Ces métriques ne remplacent pas un vrai outil de monitoring ; elles servent à l'affichage rapide, aux alertes simples et aux rapports Hermes.
+
+---
+
+## 6. Snapshots, exécutions, événements
+
+### `snapshots`
+
+Élargir la table existante.
+
+```text
+id text primary key
+machine_id text not null references machines(id)
+kind text not null -- apt_update_analyze | docker_scan | reboot_check | ...
+schema_version integer not null default 1
+created_at text not null
+status text not null -- ok | warning | error
+payload_json text not null
+important_json text -- version réduite pour Hermes/UI
+raw_log_path text
+raw_artifact_id text
+source_job_id text
+```
+
+Index :
+
+```text
+(machine_id, kind, created_at desc)
+(kind, status, created_at desc)
+```
+
+### `executions`
+
+Élargir la table existante.
+
+```text
+id text primary key
+machine_id text not null references machines(id)
+action text not null
+mode text not null -- manual | scheduled | hermes_requested
+schema_version integer not null default 1
+started_at text not null
+finished_at text
+status text not null -- running | ok | warning | error | cancelled
+request_id text
+job_id text
+result_json text
+important_json text
+raw_log_path text
+raw_artifact_id text
+report_id text
+exit_code integer
+error_kind text
+error_message text
+```
+
+Index :
+
+```text
+(machine_id, started_at desc)
+(action, status, started_at desc)
+(job_id)
+(request_id)
+```
+
+### `machine_events`
+
+Timeline/audit unifiée.
+
+```text
+id text primary key
+machine_id text references machines(id)
+event_type text not null
+severity text not null -- info | warning | error
+created_at text not null
+actor_type text -- user | system | schedule | hermes
+actor_id text
+snapshot_id text
+execution_id text
+job_id text
+message text
+payload_json text
+```
+
+### `important_messages`
+
+Messages extraits des logs et JSON pour analyse UI/Hermes.
+
+```text
+id text primary key
+machine_id text references machines(id)
+source text not null -- apt | docker | post_install | ssh | system
+category text not null -- error | warning | future_major_change | deprecation | security_notice | repository_notice
+severity text not null -- info | warning | error
+package_name text
+component text
+message text not null -- résumé nettoyé sans secret
+raw_line_ref text -- artifact_id#line
+snapshot_id text references snapshots(id)
+execution_id text references executions(id)
+first_seen_at text not null
+last_seen_at text not null
+acknowledged integer default 0
+acknowledged_at text
+acknowledged_by text
+payload_json text
+```
+
+Objectif :
+
+- conserver les warnings importants même si le log brut est purgé ;
+- permettre à Hermes de rechercher les évolutions majeures futures, notices sécurité, dépôts obsolètes ou changements de politique paquet ;
+- alimenter les badges et alertes des tuiles machine ;
+- permettre un acquittement utilisateur sans supprimer l'historique.
+
+### `reports`
+
+```text
+id text primary key
+machine_id text references machines(id)
+execution_id text
+kind text not null -- machine | global | cleanup | hermes
+title text not null
+path text not null
+created_at text not null
+pinned integer default 0
+summary_json text
+```
+
+### `raw_artifacts`
+
+Pour logs bruts, previews, exports.
+
+```text
+id text primary key
+machine_id text
+kind text not null -- raw_log | rendered_template | export | screenshot
+path text not null
+bytes integer
+sha256 text
+created_at text not null
+expires_at text
+pinned integer default 0
+redacted integer default 1
+retention_policy text -- default | failed | pinned | short
+deleted_at text
+delete_reason text
+metadata_json text
+```
+
+### `ssh_terminal_sessions`
+
+Sessions de vrai terminal SSH interactif ouvertes depuis la webapp.
+
+```text
+id text primary key
+machine_id text not null references machines(id)
+username text
+opened_by text
+opened_at text not null
+closed_at text
+status text not null -- open | closed | error | killed
+interactive integer default 1
+recording_enabled integer default 0
+raw_artifact_id text
+last_error text
+metadata_json text
+```
+
+---
+
+## 6.1 Préférences frontend et paramètres UI
+
+Les paramètres frontend qui doivent survivre au navigateur sont stockés en BDD. Le `localStorage` ne sert qu'à accélérer l'affichage initial ou garder un fallback local.
+
+### `app_settings`
+
+Paramètres globaux de l'application.
+
+```text
+key text primary key
+value_json text not null
+updated_at text not null
+updated_by text
+```
+
+Exemples :
+
+- thème par défaut ;
+- densité UI ;
+- zoom global ;
+- largeur par défaut volet Hermes/terminal ;
+- terminal SSH interactif activé/désactivé ;
+- rétention logs ;
+- CIDR autorisés pour découverte réseau.
+
+### `user_preferences`
+
+Préférences par utilisateur/session opérateur, si authentification ajoutée.
+
+```text
+id text primary key
+user_id text
+key text not null
+value_json text not null
+updated_at text not null
+```
+
+Exemples :
+
+- thème choisi ;
+- zoom ;
+- largeur des volets ;
+- taille/densité des tuiles ;
+- mode compact/confort ;
+- onglet par défaut.
+
+### `machine_ui_state`
+
+État UI par machine, non critique mais utile.
+
+```text
+machine_id text primary key references machines(id)
+sections_open_json text -- docker/post-install/logs
+tile_mode text -- compact | expanded
+last_active_tab text
+updated_at text not null
+```
+
+Règle :
+
+- aucune décision métier ne dépend de cette table ;
+- elle sert uniquement à retrouver l'ergonomie choisie.
+
+## 7. APT
+
+Les détails APT peuvent rester dans `payload_json`, mais certaines tables dérivées accélèrent recherche/dédup/Hermes.
+
+### `apt_planned_packages`
+
+Paquets prévus avant upgrade.
+
+```text
+id text primary key
+snapshot_id text not null references snapshots(id)
+machine_id text not null
+mode text not null -- upgrade | dist_upgrade
+name text not null
+arch text
+current_version text
+target_version text
+origin text
+operation text not null -- upgrade | install | remove | hold
+dedup_key text not null
+```
+
+### `apt_applied_packages`
+
+Diff réel après exécution.
+
+```text
+id text primary key
+execution_id text not null references executions(id)
+machine_id text not null
+name text not null
+arch text
+from_version text
+to_version text
+operation text not null -- upgraded | installed | removed | unchanged
+origin text
+dedup_key text
+```
+
+### `apt_errors`
+
+```text
+id text primary key
+snapshot_id text
+execution_id text
+machine_id text not null
+kind text not null -- apt_lock_busy | dpkg_interrupted | ...
+severity text not null
+message text not null
+important_lines_json text
+remediation text
+created_at text not null
+```
+
+---
+
+## 8. Docker Compose
+
+### `docker_settings`
+
+Configuration Docker par machine.
+
+```text
+machine_id text primary key references machines(id)
+enabled integer default 0
+scan_depth integer default 4
+prune_mode text default 'safe' -- safe | aggressive
+last_scan_at text
+last_pull_check_at text
+updated_at text not null
+```
+
+### `docker_compose_roots`
+
+```text
+id text primary key
+machine_id text not null references machines(id)
+path text not null
+enabled integer default 1
+scan_depth integer
+created_at text not null
+updated_at text not null
+```
+
+### `docker_compose_stacks`
+
+```text
+id text primary key
+machine_id text not null
+name text not null
+working_dir text not null
+compose_files_json text not null
+project_name text
+env_file text
+status text not null -- candidate | enabled | ignored | error
+detected_by text -- root_scan | label | manual
+last_scan_at text
+last_update_at text
+created_at text not null
+updated_at text not null
+```
+
+### `docker_stack_services`
+
+Dernier état service/image par stack.
+
+```text
+id text primary key
+stack_id text not null references docker_compose_stacks(id)
+service_name text not null
+image_ref text
+current_image_id text
+current_digest text
+candidate_image_id text
+candidate_digest text
+version_label text
+status text -- up_to_date | updates_available | error
+updated_at text not null
+```
+
+### `docker_image_events`
+
+Historique pull/apply/prune.
+
+```text
+id text primary key
+execution_id text references executions(id)
+machine_id text not null
+stack_id text
+service_name text
+image_ref text
+from_image_id text
+to_image_id text
+from_digest text
+to_digest text
+operation text -- pulled | recreated | pruned
+bytes_reclaimed integer
+created_at text not null
+```
+
+---
+
+## 9. Post-install, scripts, templates
+
+### `install_profiles`
+
+Catalogue de profils.
+
+```text
+id text primary key
+label text not null
+category text not null
+risk text
+enabled integer default 1
+manifest_json text not null
+created_at text not null
+updated_at text not null
+```
+
+### `install_recipes`
+
+Installateurs externes ou scripts custom.
+
+```text
+id text primary key
+profile_id text references install_profiles(id)
+label text not null
+source_type text not null -- apt_packages | official_external | custom
+risk text
+current_version_id text
+enabled integer default 1
+created_at text not null
+updated_at text not null
+```
+
+### `install_recipe_versions`
+
+Versionner les scripts/manifests.
+
+```text
+id text primary key
+recipe_id text not null references install_recipes(id)
+version integer not null
+template_path text not null
+manifest_json text not null
+sha256 text
+created_at text not null
+created_by text
+```
+
+### `machine_profile_state`
+
+État des profils par machine.
+
+```text
+id text primary key
+machine_id text not null references machines(id)
+profile_id text not null references install_profiles(id)
+status text not null -- not_run | ok | warning | error | pending
+last_execution_id text
+last_run_at text
+variables_json text -- non sensible seulement
+```
+
+### `script_variables_presets`
+
+Préréglages réutilisables.
+
+```text
+id text primary key
+scope text not null -- global | machine | profile
+machine_id text
+profile_id text
+name text not null
+variables_json text not null
+created_at text not null
+updated_at text not null
+```
+
+---
+
+## 10. Jobs, schedules, demandes d'action
+
+### `jobs`
+
+```text
+id text primary key
+kind text not null
+machine_id text
+schedule_id text
+request_id text
+status text not null -- queued | running | ok | warning | error | cancelled
+priority integer default 0
+created_at text not null
+started_at text
+finished_at text
+attempt integer default 0
+max_attempts integer default 1
+payload_json text
+result_json text
+error_kind text
+error_message text
+```
+
+### `schedules`
+
+```text
+id text primary key
+name text not null
+enabled integer default 1
+cron text not null
+timezone text not null
+scope_json text not null
+actions_json text not null
+concurrency integer default 1
+notify_on_json text
+last_run_at text
+next_run_at text
+created_at text not null
+updated_at text not null
+```
+
+### `machine_locks`
+
+```text
+machine_id text primary key references machines(id)
+job_id text not null
+lock_kind text not null -- apt | docker | post_install | reboot | exclusive
+created_at text not null
+expires_at text
+```
+
+### `action_requests`
+
+Demandes venant UI ou Hermes, avec validation.
+
+```text
+id text primary key
+machine_id text references machines(id)
+requested_by_type text not null -- user | hermes | schedule
+requested_by_id text
+action text not null
+risk text
+status text not null -- pending | approved | rejected | executed | expired
+summary text
+payload_json text
+created_at text not null
+approved_at text
+approved_by text
+execution_id text
+expires_at text
+```
+
+---
+
+## 11. Hermes, MCP, token usage
+
+### `hermes_sessions`
+
+```text
+id text primary key
+source text not null -- webapp | tui | telegram | discord | ...
+session_key text
+created_at text not null
+last_message_at text
+status text
+```
+
+### `hermes_runs`
+
+```text
+id text primary key
+session_id text references hermes_sessions(id)
+prompt_kind text
+status text
+started_at text not null
+finished_at text
+request_json text -- réduit, sans secret
+response_json text
+report_id text
+```
+
+### `hermes_usage`
+
+```text
+id text primary key
+run_id text references hermes_runs(id)
+provider text
+model text
+prompt_tokens integer
+cached_tokens integer
+completion_tokens integer
+total_tokens integer
+raw_bytes integer
+reduced_bytes integer
+reduction_ratio real
+created_at text not null
+```
+
+### `mcp_audit_log`
+
+```text
+id text primary key
+server_name text not null
+tool_name text not null
+actor text
+request_json text
+response_summary_json text
+status text
+created_at text not null
+```
+
+### `api_clients`
+
+Clients API externes : app locale Rust/GNOME, scripts internes, clients admin.
+
+```text
+id text primary key
+label text not null
+client_kind text not null -- webapp | rust_gnome_app | script | mcp | other
+token_hash text not null
+scopes_json text not null -- read | operate | admin | debug_logs
+status text not null -- active | revoked | expired
+created_at text not null
+last_seen_at text
+expires_at text
+revoked_at text
+metadata_json text
+```
+
+Règles :
+
+- ne jamais stocker le token en clair ;
+- audit des actions via `machine_events` ou table dédiée si nécessaire ;
+- scopes minimaux ;
+- révocation depuis les paramètres sécurité.
+
+---
+
+## 12. Optimisation, maintenance, découverte
+
+### `system_metrics`
+
+```text
+id text primary key
+created_at text not null
+cpu_percent real
+memory_rss_bytes integer
+memory_heap_bytes integer
+db_bytes integer
+wal_bytes integer
+jobs_running integer
+machines_count integer
+payload_json text
+```
+
+### `cleanup_runs`
+
+```text
+id text primary key
+mode text not null -- dry_run | apply
+status text not null
+started_at text not null
+finished_at text
+deleted_counts_json text
+bytes_reclaimed integer
+result_json text
+```
+
+### `discovery_scans`
+
+```text
+id text primary key
+cidr text not null
+ports_json text not null
+method text not null -- nmap | tcp
+status text not null
+started_at text not null
+finished_at text
+result_json text
+```
+
+### `discovery_candidates`
+
+```text
+id text primary key
+scan_id text not null references discovery_scans(id)
+host text not null
+port integer not null
+service text
+host_key_fingerprint text
+reverse_dns text
+already_known integer default 0
+created_at text not null
+```
+
+---
+
+## 13. Index recommandés
+
+```text
+machines(hostname, port)
+machines(status)
+machines(os_family, machine_kind)
+machines(hardware_profile)
+
+snapshots(machine_id, kind, created_at desc)
+snapshots(kind, status, created_at desc)
+
+executions(machine_id, started_at desc)
+executions(action, status, started_at desc)
+executions(job_id)
+
+machine_events(machine_id, created_at desc)
+machine_events(event_type, created_at desc)
+
+machine_hardware(updated_at desc)
+machine_metrics_latest(collected_at desc)
+machine_metrics_latest(root_used_percent)
+
+apt_planned_packages(dedup_key)
+apt_planned_packages(machine_id, name)
+apt_applied_packages(dedup_key)
+apt_applied_packages(execution_id)
+
+docker_compose_stacks(machine_id, status)
+docker_stack_services(stack_id, status)
+
+jobs(status, created_at)
+jobs(machine_id, status)
+schedules(enabled, next_run_at)
+action_requests(status, created_at)
+
+reports(machine_id, created_at desc)
+reports(kind, created_at desc)
+
+important_messages(machine_id, severity, last_seen_at desc)
+important_messages(category, acknowledged, last_seen_at desc)
+important_messages(package_name)
+
+raw_artifacts(machine_id, created_at desc)
+raw_artifacts(expires_at)
+
+ssh_terminal_sessions(machine_id, opened_at desc)
+ssh_terminal_sessions(status)
+
+hermes_runs(session_id, started_at desc)
+hermes_usage(run_id)
+api_clients(status, client_kind)
+```
+
+---
+
+## 14. Migration progressive recommandée
+
+Ne pas tout implémenter d'un coup.
+
+### Phase 1 — Séparer et fiabiliser le socle
+
+- ajouter `machine_state` ;
+- ajouter `machine_kind/virtualization/hardware_profile` dans `machines` ;
+- ajouter `machine_hardware` ;
+- ajouter `machine_metrics_latest` ;
+- ajouter `machine_events` ;
+- ajouter `important_messages` ;
+- ajouter `reports` ;
+- ajouter `raw_artifacts` ;
+- ajouter `kind/schema_version/important_json` à `snapshots` ;
+- ajouter `schema_version/important_json/error_kind/error_message` à `executions`.
+
+### Phase 1.5 — Préférences frontend
+
+- créer `app_settings` ;
+- créer `user_preferences` ;
+- créer `machine_ui_state` ;
+- migrer le thème actuel depuis une logique `localStorage` vers une préférence persistée côté backend, avec fallback local.
+
+### Phase 2 — Sécurité credentials
+
+- créer `machine_credentials` ;
+- migrer `enc_password`, `enc_sudo_password` ;
+- créer `machine_host_keys` ;
+- ajouter audit événements secrets.
+
+### Phase 3 — APT complet
+
+- créer `apt_planned_packages` ;
+- créer `apt_applied_packages` ;
+- créer `apt_errors`.
+
+### Phase 4 — Docker
+
+- créer `docker_settings` ;
+- créer `docker_compose_roots` ;
+- créer `docker_compose_stacks` ;
+- créer `docker_stack_services` ;
+- créer `docker_image_events`.
+
+### Phase 5 — Post-install/scripts
+
+- créer `install_profiles` ;
+- créer `install_recipes` ;
+- créer `install_recipe_versions` ;
+- créer `machine_profile_state` ;
+- créer `script_variables_presets`.
+
+### Phase 6 — Automatisations
+
+- créer `jobs` ;
+- créer `schedules` ;
+- créer `machine_locks` ;
+- créer `action_requests`.
+
+### Phase 7 — Hermes/optimisation
+
+- créer `hermes_sessions` ;
+- créer `hermes_runs` ;
+- créer `hermes_usage` ;
+- créer `mcp_audit_log` ;
+- créer `api_clients`.
+
+### Phase 8 — Maintenance/découverte
+
+- créer `system_metrics` ;
+- créer `cleanup_runs` ;
+- créer `discovery_scans` ;
+- créer `discovery_candidates`.
+
+### Phase 9 — Terminal SSH interactif
+
+- créer `ssh_terminal_sessions` ;
+- définir activation globale dans `app_settings` ;
+- journaliser ouverture/fermeture ;
+- décider si l'enregistrement de session est désactivé, optionnel ou obligatoire selon profil de sécurité.
+
+---
+
+## 15. Recommandations importantes
+
+### Garder les JSON complets
+
+Même avec tables dérivées, conserver les payloads JSON complets :
+
+- audit ;
+- relecture ;
+- compatibilité Hermes ;
+- évolution future ;
+- debug.
+
+### Versionner les JSON
+
+Ajouter `schema_version` sur snapshots/executions.
+
+### Ne jamais stocker de secrets dans JSON métier
+
+Secrets seulement dans `machine_credentials`, chiffrés.
+
+### Dériver `machine_state`
+
+Le dashboard ne doit pas recalculer les compteurs depuis l'historique à chaque requête.
+
+### Prévoir rétention
+
+Les tables historiques peuvent grossir :
+
+- snapshots ;
+- executions ;
+- events ;
+- raw_artifacts ;
+- hermes_usage.
+
+Tâche 7 définit la purge.
+
+### Préparer PostgreSQL
+
+Si migration PostgreSQL future :
+
+- `payload_json` → `jsonb` ;
+- `text ISO` → `timestamptz` ;
+- index GIN sur JSON si besoin ;
+- jobs persistants avec `pg-boss`.
+
+---
+
+## 16. Livrables attendus
+
+À produire sous `docs/` :
+
+1. Schéma BDD détaillé.
+2. Diagramme relationnel.
+3. Plan de migration depuis le schéma actuel.
+4. Priorisation par phases.
+5. Stratégie SQLite maintenant / PostgreSQL futur.
+6. Liste des index.
+7. Règles de rétention.
+8. Règles sécurité secrets.
+9. Contrats JSON versionnés.
+
+---
+
+## 17. Définition de terminé
+
+- Le schéma couvre les tâches 2 à 8.
+- Le schéma reste compatible MVP SQLite.
+- Les évolutions futures sont prévues.
+- Les secrets sont isolés.
+- Les JSON complets restent archivés.
+- Les tuiles machine peuvent lire un état courant rapide.
+- Les automatisations et Hermes ont leurs tables dédiées.
+- Aucun code de production n'est livré pendant cette mission de design.
+
+---
+
+## 18. Technos à utiliser — checklist
+
+- [ ] SQLite au MVP, avec WAL activé.
+- [ ] Drizzle ORM pour schéma TypeScript et migrations.
+- [ ] JSON stocké en `text` côté SQLite, compatible futur `jsonb` PostgreSQL.
+- [ ] Index explicites sur colonnes de recherche/fréquence.
+- [ ] Volumes persistants Docker Compose pour DB, WAL, logs, reports, artifacts.
+- [ ] Chiffrement applicatif des secrets avant stockage.
+- [ ] Table `api_clients` pour tokens externes hashés/révocables.
+- [ ] Plan de migration progressif, phase par phase.
+
+## 19. URLs utiles
+
+- SQLite WAL : https://www.sqlite.org/wal.html
+- SQLite `PRAGMA optimize` : https://www.sqlite.org/pragma.html#pragma_optimize
+- SQLite VACUUM : https://www.sqlite.org/lang_vacuum.html
+- Drizzle ORM SQLite : https://orm.drizzle.team/docs/get-started-sqlite
+- Drizzle migrations : https://orm.drizzle.team/docs/migrations
+- PostgreSQL JSON types : https://www.postgresql.org/docs/current/datatype-json.html
+- Docker volumes : https://docs.docker.com/engine/storage/volumes/
+- Docker Compose volumes : https://docs.docker.com/reference/compose-file/volumes/
+
+## 20. Liens parent/enfant avec les autres tâches
+
+- Parent direct : aucune, cette tâche structure la donnée cible.
+- Enfants directs :
+ - `tache2.md` : contrats JSON/templates à stocker.
+ - `tache3.md` : préférences UI, état tuiles.
+ - `tache4.md` : profils/scripts/variables.
+ - `tache5.md` : API/jobs/historique.
+ - `tache6.md` : Hermes/MCP/audit.
+ - `tache7.md` : rétention, métriques, nettoyage.
+ - `tache8.md` : `api_clients` et compatibilité app locale.
+- Validation : `validation_tache1.9.md`.
diff --git a/tache2.md b/tache2.md
index 3a9ed6b..fa38bbf 100644
--- a/tache2.md
+++ b/tache2.md
@@ -61,8 +61,18 @@ Concevoir (investigation + design, pas implémentation) le **moteur de templates
Concevoir l'inventaire et le contenu des templates pour : `update` (refresh index, déjà partiellement là), `upgrade`, `full-upgrade`, `dist-upgrade`, `clean`, `autoremove`, plus `reboot`/`reboot-check`.
- Clarifier la **sémantique exacte** de chaque commande (s'appuyer sur le manpage APT cité dans le rapport) : `upgrade` n'enlève/installe pas de paquets, `dist-upgrade`/`full-upgrade` gèrent les changements de dépendances, `clean` vide le cache, `autoremove` retire les dépendances inutiles.
- **Profils par OS** : Debian, Ubuntu, Proxmox (`apt update` puis `apt dist-upgrade`, dépôts pve), Raspberry Pi OS. Le moteur doit être *profile-aware*, pas du collage de commandes. Proposer un mécanisme de profils + overrides par machine.
+- **Profils par type de machine** : distinguer machine physique, VM, hôte Proxmox, LXC/container, Raspberry Pi, serveur GPU/workstation. Ce type influence les scripts proposés : firmware, drivers, benchmark, reboot, outils Proxmox, détection hardware.
- Gérer le **proxy APT** (apt-cacher-ng) : modes direct / temporaire à l'exécution / persistant dans `/etc/apt/apt.conf.d/`.
+Spécificités à intégrer :
+
+- Debian récent : vérifier les composants APT nécessaires (`main`, `contrib`, `non-free`, `non-free-firmware`) avant de proposer firmware/drivers propriétaires ;
+- Ubuntu : prévoir `ubuntu-drivers` pour analyse/proposition drivers, surtout NVIDIA/GPU ;
+- Proxmox : utiliser le profil Proxmox dédié, ne pas le traiter comme une Debian générique ; contrôler dépôts PVE, meta-package `proxmox-ve`, kernel PVE, éventuel Ceph, puis `apt-get dist-upgrade` ;
+- Raspberry Pi OS : profil dédié, attention firmware/kernel, espace disque avant upgrade, usage de `full-upgrade` ;
+- VM : privilégier outils invités (`qemu-guest-agent`, `open-vm-tools` selon hyperviseur), éviter drivers GPU/firmware non pertinents sauf passthrough ;
+- machine physique : proposer détection hardware, firmware, SMART/disques, sensors, drivers GPU et benchmarks.
+
### Axe B — Capture des mises à jour *prévues* et *appliquées* (pour Hermes)
- **Avant** : produire le snapshot des updates disponibles (paquets, versions courante→cible, origine, reboot requis). Déjà amorcé pour `full-upgrade` simulé ; étendre et fiabiliser.
- **Après** : produire un résultat d'exécution avec le **diff réel avant/après** (paquets effectivement modifiés, versions finales, erreurs résiduelles, reboot requis après coup).
@@ -93,11 +103,12 @@ Produire des réponses argumentées (MVP recommandé / alternatives / risques) p
1. **JSON-in-shell vs parsing-in-TS** : `nas-ops` produit le JSON dans le script ; le jalon 1 parse en TS. Quelle stratégie pour la suite ? (cohérence, testabilité, robustesse multi-OS). Trancher et justifier.
2. **Structure des profils OS** : fichiers de templates par profil ? héritage/override ? convention de nommage et d'arborescence sous `templates/`.
-3. **Capture avant/après** : comment obtenir un diff fiable des paquets réellement appliqués (parser `apt-get` ? interroger dpkg ? snapshot dpkg avant/après ?).
-4. **Contrats JSON** : quelles extensions exactes à `UpdateSnapshot` et `ExecutionResult` (`shared/types.ts`) pour Docker, erreurs, scripts custom ? Proposer les types.
-5. **Idempotence & opérations longues** : généraliser l'exécution détachée (`nohup` + exit-code) ? Comment suivre la progression et reprendre ?
-6. **Sécurité Docker `prune` / scripts custom** : où placer la barrière de validation, comment éviter toute fuite de secret vers Hermes/MCP.
-7. **Surface MCP** : quels nouveaux outils/contrats exposer à Hermes pour ces capacités (cf. `list_templates`, `preview_template`, `run_action`…), en gardant la surface minimale.
+3. **Structure des profils machine** : choix manuel à l'ajout vs détection automatique (`systemd-detect-virt`, `/etc/os-release`, `dmidecode`, `lspci`, `/proc/cpuinfo`) ; règles de correction utilisateur.
+4. **Capture avant/après** : comment obtenir un diff fiable des paquets réellement appliqués (parser `apt-get` ? interroger dpkg ? snapshot dpkg avant/après ?).
+5. **Contrats JSON** : quelles extensions exactes à `UpdateSnapshot` et `ExecutionResult` (`shared/types.ts`) pour Docker, erreurs, scripts custom, hardware et métriques ? Proposer les types.
+6. **Idempotence & opérations longues** : généraliser l'exécution détachée (`nohup` + exit-code) ? Comment suivre la progression et reprendre ?
+7. **Sécurité Docker `prune` / scripts custom** : où placer la barrière de validation, comment éviter toute fuite de secret vers Hermes/MCP.
+8. **Surface MCP** : quels nouveaux outils/contrats exposer à Hermes pour ces capacités (cf. `list_templates`, `preview_template`, `run_action`…), en gardant la surface minimale.
---
@@ -110,9 +121,10 @@ Produire des réponses argumentées (MVP recommandé / alternatives / risques) p
3. **Schémas JSON canoniques étendus** (snapshot + résultat) couvrant APT, Docker, erreurs, scripts custom — avec règles de déduplication et de réduction pour Hermes.
4. **Taxonomie des erreurs** + stratégie de gestion et de remédiation.
5. **Modèle des profils OS et des overrides par machine.**
-6. **Modèle des scripts personnalisés** (post-install, install paquets) avec garde-fous.
-7. **Note de sécurité** : ce qui ne doit jamais atteindre Hermes/MCP, validations requises pour les actions destructives.
-8. **Découpage en sous-jalons** implémentables indépendamment (chacun = un cycle spec → plan → implémentation), avec ordre recommandé.
+6. **Modèle des profils machine** : physique, VM, hôte Proxmox, LXC, Raspberry Pi, serveur GPU, workstation.
+7. **Modèle des scripts personnalisés** (post-install, install paquets, hardware, drivers, métriques) avec garde-fous.
+8. **Note de sécurité** : ce qui ne doit jamais atteindre Hermes/MCP, validations requises pour les actions destructives.
+9. **Découpage en sous-jalons** implémentables indépendamment (chacun = un cycle spec → plan → implémentation), avec ordre recommandé.
---
@@ -130,10 +142,100 @@ Produire des réponses argumentées (MVP recommandé / alternatives / risques) p
## 6. Définition de « terminé » pour cette mission
- Tous les axes A→E couverts par un design argumenté.
-- Les 7 questions d'investigation tranchées (MVP/alternatives/risques).
+- Les 8 questions d'investigation tranchées (MVP/alternatives/risques).
- Les livrables de la §4 rédigés et cohérents entre eux.
- Un découpage en sous-jalons priorisé, prêt à passer en `writing-plans`.
- Aucune implémentation de production livrée (cette mission s'arrête au design validé).
- Le fichier `tache2.md` mis à jour avec la section « État d'avancement / Ce qui a été fait » (cf. clôture obligatoire ci-dessus).
> **Étape suivante (hors de cette mission)** : tes livrables seront passés au crible de `validation_tache2.md` (grille de validation + gate). **Aucune phase de développement ne démarre tant que ce gate n'est pas ✅ Accepté.** Conçois donc tes livrables pour qu'ils soient vérifiables contre cette grille (complétude, périmètre respecté, cohérence et intégration avec l'appli existante, non-régression).
+
+---
+
+## 7. Technos à utiliser — checklist
+
+- [ ] Bash POSIX-ish pour templates shell, avec `LC_ALL=C`.
+- [ ] Mustache pour rendu de templates.
+- [ ] TypeScript pour parsing, réduction et contrats JSON.
+- [ ] `apt-get` en scripts, pas `apt` interactif.
+- [ ] `dpkg-query` pour diff avant/après.
+- [ ] Docker CLI + Docker Compose plugin pour stacks.
+- [ ] SSH existant `server/ssh/client.ts`, `sudo -S`, base64 script.
+- [ ] Réduction déterministe avant Hermes/MCP.
+- [ ] Logs bruts archivés séparément des JSON.
+
+## 8. URLs utiles
+
+- `apt-get` manpage : https://manpages.debian.org/apt-get
+- `dpkg-query` manpage : https://manpages.debian.org/dpkg-query
+- `dpkg` manpage : https://manpages.debian.org/dpkg
+- Debian non-free firmware : https://www.debian.org/releases/bookworm/amd64/ch02s02.en.html
+- Proxmox system updates : https://pve.proxmox.com/wiki/System_Software_Updates
+- Raspberry Pi OS software updates : https://www.raspberrypi.com/documentation/usage/terminal/
+- Docker Compose CLI : https://docs.docker.com/reference/cli/docker/compose/
+- Docker Compose pull : https://docs.docker.com/reference/cli/docker/compose/pull/
+- Docker Compose up : https://docs.docker.com/reference/cli/docker/compose/up/
+- Docker image prune : https://docs.docker.com/reference/cli/docker/image/prune/
+- Docker Engine Debian : https://docs.docker.com/engine/install/debian/
+- Mustache : https://mustache.github.io/
+
+## 9. Liens parent/enfant avec les autres tâches
+
+- Parents :
+ - `tache1.9.md` pour stockage et indexation.
+- Enfants :
+ - `tache4.md` pour scripts post-install détaillés.
+ - `tache5.md` pour exécution backend, jobs et API.
+ - `tache3.md` pour affichage des snapshots/actions.
+ - `tache6.md` pour analyse Hermes des JSON.
+ - `tache7.md` pour réduction tokens/nettoyage.
+- Validation : `validation_tache2.md`.
+
+---
+
+## 10. État d'avancement / Ce qui a été fait
+
+> **Statut** : mission de design **terminée**. Aucun code de production écrit (uniquement des `.md` sous `docs/design/tache2/` + cette section). Aucun commit. Prêt à repasser le gate `validation_tache2.md`.
+
+### Livrables produits (chemins)
+
+Tous sous `docs/design/tache2/` :
+
+- `00-synthese.md` — vue d'ensemble, cartographie des livrables, décisions structurantes, alignement `tache1.9.md`, hors-scope.
+- `10-templates-apt.md` — sémantique APT clarifiée, inventaire des templates APT, variables Mustache, pseudo-shell réaliste (update-analyze/upgrade/full-upgrade/autoremove/clean/reboot), profils OS, migration non-régressive du jalon 1.
+- `20-docker.md` — méthode Docker par SSH, inventaire + pseudo-shell des 6 templates `docker/*` (scan/inspect/pull-check/apply/prune/down), flux 1→8, réduction Hermes, insertion webapp.
+- `30-scripts-custom.md` — modèle moteur post-install (manifestes, champs dynamiques, garde-fous), profils attendus, pseudo-shell des templates custom, renvoi catalogue détaillé à la tâche 4.
+- `40-contrats-json.md` — schémas JSON canoniques étendus + **types TypeScript rétro-compatibles** (extensions de `shared/types.ts`), déduplication, réduction déterministe, mapping vers les tables `tache1.9.md`.
+- `50-erreurs.md` — taxonomie des erreurs APT/dpkg/Docker/réseau + codes normalisés + remédiation + interactions humaines + idempotence/reprise.
+- `60-profils-os-machine.md` — modèle profils OS (arborescence + fallback `base`), type machine, overrides par machine, `machine_probe`, proxy apt-cacher-ng (direct/runtime/persistent).
+- `70-securite.md` — frontière Hermes/MCP, actions destructives + validations, surface MCP minimale, traçabilité.
+- `80-sous-jalons.md` — découpage en 10 sous-jalons (SJ-0→SJ-9) priorisé, prêt pour `writing-plans`.
+- `90-questions-investigation.md` — les **8 questions §3 tranchées** (MVP/alternatives/risques).
+- `99-couverture-gate.md` — auto-évaluation case par case de `validation_tache2.md` (§1→§8).
+
+### Décisions prises (résumé)
+
+1. Parsing **hybride à dominante TS** (marqueurs `===SU:XXX===` + TSV `dpkg-query` + `docker --format json`), rétro-compatible jalon 1.
+2. Profils OS = **un fichier complet par profil** sous `templates//`, **fallback `base`** (Debian/Ubuntu inchangés).
+3. Type machine = **choix manuel + `machine_probe`** de correction (jamais auto-appliquée).
+4. Diff réel = **snapshot dpkg before/after**, l'exit code ne suffit jamais.
+5. Extensions de `shared/types.ts` = **unions élargies + blocs optionnels** (`docker`/`errors`/`reboot`/`postInstall`/champs apt), `schemaVersion?`. Payload jalon 1 reste valide.
+6. Opérations longues = `nohup` + exit-code pour les actions applicatives ; **reboot vérifié** via boot_id + délai adaptatif ; refresh synchrone.
+7. Sécurité = barrière `action_requests` côté webapp + nettoyage des secrets ; Hermes propose, ne déclenche jamais.
+8. Surface MCP = **v1 conservée (8 outils)**, capacités nouvelles via `run_action` filtré ; aucune primitive SSH exposée.
+
+### 8 questions d'investigation — tranchées
+
+Q1 JSON-in-shell vs TS · Q2 structure profils OS · Q3 profils machine · Q4 capture avant/après · Q5 extensions JSON · Q6 idempotence/opérations longues · Q7 sécurité prune/scripts · Q8 surface MCP. Détail dans `90-questions-investigation.md`.
+
+### Ce qui reste ouvert
+
+- Exécution `pnpm check/test/build` (non-régression §4) : à lancer par l'orchestrateur (aucun code touché ⇒ attendu vert).
+- Rendu UI fin des snapshots/actions : **tâche 3** (le design pose les contrats/exigences).
+- Conflit délimiteurs Mustache vs Go-templates Docker (`{{ }}`) : choix à figer en implémentation (SJ-4).
+- Catalogue détaillé des scripts post-install : **tâche 4** (mécanisme moteur posé ici).
+- File de jobs persistante / API d'action : **tâche 5**.
+
+### Sous-jalons recommandés (ordre)
+
+SJ-0 socle types/réduction/résolution (bloquant) → SJ-1 APT update/analyse → SJ-2 APT upgrade + diff dpkg → SJ-3 reboot vérifié ; en parallèle SJ-4 Docker scan/inspect → SJ-5 pull-check → SJ-6 apply/prune/down ; transversal SJ-7 profils Proxmox/RPi + proxy persistent ; puis SJ-8 post-install bootstrap/identité → SJ-9 post-install Docker officiel/partages/VM tools. Détail dans `80-sous-jalons.md`.
diff --git a/tache3.md b/tache3.md
new file mode 100644
index 0000000..0e95b59
--- /dev/null
+++ b/tache3.md
@@ -0,0 +1,835 @@
+# Consigne de dev — Design des tuiles machine et paramètres frontend
+
+> **Type** : mission de **design UX/UI + spec frontend** (PAS d'implémentation).
+> **Langue** : français.
+> **Livrable final attendu** : document(s) de design/spec prêts à passer en plan d'implémentation.
+
+---
+
+## 0. Contexte
+
+Le projet `system_update` est une webapp de mise à jour distante de machines Linux, Docker Compose et scripts post-install, pilotée par SSH agentless.
+
+Le jalon actuel affiche des tuiles machine simples : nom, IP, OS, compteur APT, boutons `Refresh`, `Upgrade`, `Reboot`. La prochaine étape UI doit transformer chaque tuile en **cockpit machine compact et extensible**, compatible avec :
+
+- APT update/analyse/upgrade/reboot ;
+- Docker Compose : installation Docker, paramétrage des roots Compose, scan, stacks, upgrade, prune ;
+- Post-install : profils cochables, champs dynamiques, preview, exécution ;
+- Design system Gruvbox seventies.
+
+À lire avant de travailler :
+
+- `CLAUDE.md`
+- `design_system/consigne_design_system.md`
+- `docs/superpowers/specs/2026-06-04-jalon1-tranche-verticale-apt-design.md`
+- `docs/superpowers/specs/2026-06-05-jalon2-polish-design-system-design.md`
+- `validation_tache2.md`
+- `client/src/App.tsx`
+- `client/src/panels/Dashboard.tsx`
+- `client/src/features/machines/MachineTile.tsx`
+- `client/src/components/ui-kit.tsx`
+- `client/src/styles/app.css`
+
+---
+
+## 1. Objectif
+
+Concevoir le design des **tuiles machine extensibles** et des paramètres frontend liés.
+
+La tuile doit rester lisible par défaut, puis s'agrandir automatiquement quand l'utilisateur ouvre les sections Docker ou Post-install.
+
+Par défaut, on voit uniquement le bloc système :
+
+- statut machine ;
+- nom ;
+- IP/port ;
+- OS ;
+- compteur updates ;
+- dernier check ;
+- reboot requis ;
+- actions système en icônes.
+
+Deux sections doivent être repliables :
+
+- **Docker**
+- **Post-install**
+
+Quand une section s'ouvre, la tuile s'agrandit en hauteur. Si le contenu devient important, le design doit prévoir une variante `expanded` qui peut occuper toute la largeur de la grille (`grid-column: 1 / -1`) sans masquer les autres volets.
+
+---
+
+## 2. Contraintes design system
+
+Respect strict de `design_system/consigne_design_system.md` :
+
+- variables CSS uniquement ;
+- composants existants du `ui-kit` ;
+- icônes via `` / ``, jamais emoji/SVG inline ;
+- tooltips obligatoires sur icônes seules ;
+- pas de hover décoratif, seulement pression `.interactive` ;
+- polices Inter / JetBrains Mono / Share Tech Mono ;
+- labels uppercase `.label` ;
+- tuiles `glass`, rayon 10-12px ;
+- UI dense, lisible dark + light ;
+- ne pas utiliser `window.alert` / `confirm`, utiliser ``.
+
+Les boutons texte doivent être réservés aux commandes explicites longues ou primaires. Dès que possible, remplacer par des icônes :
+
+- analyse/refresh : `refresh`
+- upgrade/apply : `download` ou alias à ajouter si besoin
+- reboot/power : `power`
+- paramètres : `cog`
+- rapport/log : `list` ou `terminal`
+- ouvrir/fermer section : `chevD` / `chevR`
+- installer/ajouter : `plus` ou `download`
+- erreur : `alert`
+- dossier/roots compose : `folder`
+
+Si une icône manque, proposer l'ajout d'un alias dans `ICON_MAP` du ui-kit, sans inventer de SVG.
+
+---
+
+## 3. Identité app, favicon et icônes
+
+La webapp doit disposer d'une identité visuelle propre, compatible desktop et smartphone.
+
+Assets à prévoir :
+
+- `favicon.svg` : favicon principal vectoriel ;
+- `favicon.ico` : fallback navigateur ;
+- `apple-touch-icon.png` : icône smartphone/tablette ;
+- `web-app-manifest` ou équivalent futur : icônes `192x192` et `512x512` ;
+- icône monochrome/masque si l'app devient PWA ;
+- icône large éventuelle pour page d'accueil/launcher.
+
+Direction visuelle :
+
+- thème Gruvbox seventies ;
+- symbole simple et lisible en petit format ;
+- évoquer monitoring système + update + machines ;
+- éviter les logos de distributions ou marques propriétaires ;
+- éviter emoji, gradient SaaS, mascotte, illustration complexe ;
+- formes robustes : terminal, serveur, flèche d'update, LED, grille machine.
+
+Un fichier dédié [consigne_icon.md](/home/gilles/Documents/projet/system_update/consigne_icon.md) doit servir de brief à un agent de création SVG.
+
+### Icônes applicatives utiles
+
+Le frontend doit d'abord utiliser Font Awesome via `Icon`/`IconButton`. Les SVG spécifiques ne sont à créer que si Font Awesome ne couvre pas assez bien le besoin ou si l'app a besoin d'un pictogramme propriétaire.
+
+Alias à prévoir ou vérifier dans `ICON_MAP` :
+
+```text
+Navigation/layout
+- app-logo
+- hermes
+- machines
+- settings
+- terminal
+- logs
+- report
+- copy
+- open-external
+- fullscreen
+- collapse
+
+Actions système
+- refresh
+- analyze
+- upgrade
+- full-upgrade
+- reboot
+- stop
+- dry-run
+- approve
+- reject
+
+Machine/profil
+- server
+- vm
+- physical-host
+- proxmox
+- raspberry-pi
+- container
+- gpu
+- cpu
+- memory
+- disk
+- network
+- health
+
+APT/Docker/Post-install
+- package
+- repository
+- firmware
+- driver
+- docker
+- compose-stack
+- image
+- prune
+- script
+- profile
+
+Sécurité/états
+- ok
+- warning
+- error
+- locked
+- secret
+- key
+- shield
+- disconnected
+- running
+```
+
+Les icônes spécifiques SVG candidates :
+
+- `app-logo` : identité de l'application ;
+- `hermes` : si l'agent a une identité visuelle locale distincte ;
+- `proxmox` : pictogramme générique d'hyperviseur, sans reprendre le logo officiel ;
+- `compose-stack` : pile de conteneurs/stacks, plus précis que `server` ;
+- `firmware-driver` : composant matériel + puce ;
+- `machine-probe` : loupe + serveur ;
+- `reboot-verified` : power + check.
+
+---
+
+## 4. Layout global de la webapp
+
+Le design cible reste un dashboard 3 zones avec header et footer fixes.
+
+```text
+┌──────────────────────────────────────────────────────────────────────────────┐
+│ HEADER System Update [search] [scan ssh] [+ machine] [theme] [⚙] │
+├──────────────────┬─────────────────────────────────────┬───────────────────┤
+│ HERMES │ MACHINES │ TERMINAL │
+│ chat clair │ grille de tuiles │ logs/action SSH │
+│ │ │ │
+│ user message │ ┌─────────────────────────────────┐ │ vm_mqtt │
+│ hermes answer │ │ machine tile compact/expanded │ │ live output │
+│ command blocks │ └─────────────────────────────────┘ │ search/filter │
+│ action cards │ ┌─────────────────────────────────┐ │ report/log links │
+│ │ │ machine tile compact/expanded │ │ │
+├──────────────────┴─────────────────────────────────────┴───────────────────┤
+│ FOOTER mode · machines · apt · docker · jobs · cpu · ram · db · hermes · ts │
+└──────────────────────────────────────────────────────────────────────────────┘
+```
+
+Contraintes :
+
+- le volet Hermes gauche ne masque jamais les tuiles ;
+- le terminal droit ne recouvre jamais la section centrale ;
+- les largeurs sont redimensionnables mais bornées ;
+- le centre garde `min-width: 0` et scrolle indépendamment ;
+- sur mobile, ce layout devient des onglets/pages, pas trois colonnes compressées.
+
+---
+
+## 5. Tuile machine — structure cible
+
+### Ajout machine — sélection OS et type
+
+Lors de l'ajout d'une machine, le frontend doit prévoir deux champs distincts :
+
+- **OS** : Debian, Ubuntu, Proxmox VE, Raspberry Pi OS, autre Linux ;
+- **Type de machine** : VM, machine physique, hôte Proxmox, LXC/container, Raspberry Pi, serveur GPU/workstation.
+
+Le formulaire doit permettre :
+
+- choix manuel rapide ;
+- bouton d'auto-détection après test SSH ;
+- affichage du résultat détecté avec possibilité de correction ;
+- explication courte des conséquences, sans texte long : les scripts proposés changent selon le profil ;
+- avertissement si incohérence, par exemple OS Proxmox mais type VM, ou Raspberry Pi OS sans architecture ARM.
+
+Exemple UX :
+
+```text
+Ajouter machine
+┌──────────────────────────────────────────────┐
+│ Nom [ vm_mqtt ] │
+│ Hôte/IP [ 10.0.0.3 ] │
+│ OS [ Debian v ] │
+│ Type machine [ VM v ] │
+│ │
+│ [tester SSH] [détecter OS/hardware] │
+└──────────────────────────────────────────────┘
+```
+
+Après détection, la tuile peut afficher un badge compact :
+
+```text
+debian · vm · amd64 · qemu
+proxmox · physical · zfs
+raspios · raspberry_pi · arm64
+```
+
+### État compact
+
+```text
+┌────────────────────────────────────────────────────────────────────┐
+│ ● vm_mqtt debian 13 · vm · amd64 · qemu │
+│ 10.0.0.3:22 last check 06:42 │
+├────────────────────────────────────────────────────────────────────┤
+│ SYSTEM APT 4 updates Reboot no Warnings 1 │
+│ HEALTH CPU 0.08/4c RAM 26% / 29% │
+│ │
+│ [refresh] analyse [download] upgrade [power] reboot [list] log │
+│ │
+│ ▸ Docker 1 stack update · prune ready │
+│ ▸ Post-install 0 selected · 3 recommended │
+│ ▸ Hardware VM tools ok · no GPU │
+└────────────────────────────────────────────────────────────────────┘
+```
+
+### État complet hôte physique / Proxmox
+
+```text
+┌────────────────────────────────────────────────────────────────────┐
+│ ● proxmox-01 proxmox 9 · physical · amd64 │
+│ 10.0.0.10:22 zfs · gpu detected · last 06:42 │
+├────────────────────────────────────────────────────────────────────┤
+│ SYSTEM APT 12 updates Reboot yes Warnings 2 │
+│ HEALTH CPU 0.42/16c RAM 41% / 68% /tank 72% │
+│ ALERTS repo warning · firmware update available │
+│ │
+│ [refresh] [download] dist-upgrade [power] reboot [list] [cog] │
+│ │
+│ ▾ Docker 3 stacks · 1 update │
+│ ok mqtt up-to-date │
+│ warn frigate image update available │
+│ [refresh] pull-check [download] apply [list] report │
+│ ok paperless up-to-date │
+│ [prune] images │
+│ │
+│ ▾ Post-install 2 recommended │
+│ [ ] firmware tools physical host │
+│ [ ] gpu drivers NVIDIA detected │
+│ [ ] benchmark tools optional │
+│ │
+│ ▾ Hardware │
+│ CPU 16c · RAM 64G · GPU NVIDIA · disks 4 · smart ok │
+└────────────────────────────────────────────────────────────────────┘
+```
+
+### État Docker déplié
+
+```text
+┌────────────────────────────────────────────────────────────────────┐
+│ ● vm_mqtt debian · 10.0.0.3:22 │
+│ SYSTEM Updates: 4 Reboot: no Last check: 06:42 │
+│ [↻] [⇩] [⏻] [▤] │
+│ │
+│ ▾ Docker installed · 3 stacks │
+│ Roots: /home/gilles/docker, /opt/stacks [⚙] [↻] │
+│ │
+│ ✓ homeassistant 1 update available [⇩] │
+│ ✓ mqtt up to date [✓] │
+│ ⚠ paperless pull error [!] │
+│ │
+│ Prune images: 2 old images · 740 MB reclaimable [🧹] │
+│ │
+│ ▸ Post-install 0 profile selected │
+└────────────────────────────────────────────────────────────────────┘
+```
+
+Le rendu final doit utiliser des icônes Font Awesome via le design system, pas les symboles ASCII ci-dessus.
+
+### État Post-install déplié
+
+```text
+┌────────────────────────────────────────────────────────────────────┐
+│ ● debian-new debian · 10.0.2.81:22│
+│ SYSTEM Updates: unknown Reboot: unknown │
+│ [↻] analyse │
+│ │
+│ ▸ Docker not installed [⇩] │
+│ ▾ Post-install │
+│ ☑ Hostname + IP statique risk: net │
+│ Hostname [ debian-docker-01 ] │
+│ Interface [ ens18 ▼ ] │
+│ Address [ 10.0.4.25/24 ] │
+│ Gateway [ 10.0.0.1 ] │
+│ DNS [ 10.0.0.1, 10.0.0.10 ] │
+│ │
+│ ☑ Base tools nano less tmux htop ncdu rsync... │
+│ ☐ Sharing samba nfs avahi wsdd2 │
+│ ☑ Docker officiel user: gilles · dir: /home/gilles/docker │
+│ │
+│ [preview] [run selected] │
+└────────────────────────────────────────────────────────────────────┘
+```
+
+Les cases à cocher doivent être de vrais contrôles du design system (`Toggle` ou checkbox stylée), pas du texte décoratif.
+
+---
+
+## 6. Section système
+
+La section système est toujours visible.
+
+Elle doit afficher :
+
+- statut : `ok`, `updates_available`, `running`, `warning`, `error`, `unknown` ;
+- machine : nom, hostname/IP, port, OS family/version ;
+- APT :
+ - updates prévues ;
+ - dernier `apt_update_analyze` ;
+ - reboot requis ;
+ - erreurs éventuelles ;
+ - lien rapport/log si disponible.
+
+Actions :
+
+- analyser (`apt_update_analyze`) ;
+- upgrade simple ;
+- full/dist-upgrade si disponible ;
+- reboot vérifié ;
+- rapport/log.
+
+Règles :
+
+- `upgrade`, `full/dist-upgrade`, `reboot` demandent confirmation UI ;
+- actions désactivées si une action est déjà en cours ;
+- action d'upgrade désactivée si aucune analyse récente n'existe, ou affichée avec warning explicite ;
+- les erreurs doivent être lisibles directement dans la tuile sans ouvrir le terminal.
+
+---
+
+## 7. Section Docker
+
+La section Docker est repliée par défaut.
+
+Cas à couvrir :
+
+### Docker non installé
+
+Afficher :
+
+- statut `Docker absent` ;
+- action installer Docker officiel ;
+- info courte : "installation via script officiel enregistré".
+
+Action :
+
+- bouton icône + tooltip `Installer Docker officiel`.
+- confirmation obligatoire.
+
+### Docker installé, stacks non configurés
+
+Afficher :
+
+- statut `Docker installé`;
+- bouton paramètres section Docker ;
+- bouton scan ;
+- message : "Aucun root Compose validé".
+
+Paramètres Docker :
+
+- roots Compose par machine ;
+- profondeur de scan ;
+- stacks activés/désactivés ;
+- chemins ignorés ;
+- mode prune prudent/agressif.
+
+### Stacks Compose détectés/configurés
+
+Afficher une liste compacte :
+
+- nom stack ;
+- chemin court ;
+- statut check ;
+- nombre de services ;
+- update dispo ;
+- erreur éventuelle ;
+- action upgrade si update dispo.
+
+Bouton `docker prune` :
+
+- actif uniquement après analyse Docker ;
+- indique l'espace récupérable si connu ;
+- confirmation obligatoire ;
+- mode agressif séparé et clairement marqué comme risqué.
+
+---
+
+## 8. Section Post-install
+
+La section Post-install est repliée par défaut.
+
+Elle contient des profils cochables :
+
+- `bootstrap_root`
+- `identity_network`
+- `base_tools`
+- `network_tools`
+- `dev_git`
+- `sharing`
+- `docker_official`
+- `home_automation`
+- `dev_tools`
+- `embedded_esp_platformio`
+- `terminal_customization`
+- `vm_guest_tools`
+- `storage_health`
+- `media_tools`
+- `security_audit`
+- `security_lab` (high risk)
+- `backup_sync`
+- `monitoring`
+- `network_services`
+
+Quand un profil est coché :
+
+- il déplie ses champs obligatoires ;
+- les champs peuvent être préremplis ;
+- le bouton run reste désactivé tant que les champs requis sont invalides ;
+- une preview du script rendu est disponible ;
+- les actions à risque demandent confirmation.
+
+Exemple Hostname + réseau :
+
+```text
+┌────────────────────────────────────────────────────────────────────┐
+│ POST-INSTALL · Hostname + IP statique │
+│ risk: network_change · reboot required │
+├────────────────────────────────────────────────────────────────────┤
+│ ☑ Activer ce script │
+│ Hostname [ debian-docker-01 ] │
+│ Domaine local [ home ] │
+│ Interface [ ens18 ▼ ] │
+│ Adresse statique [ 10.0.4.25/24 ] │
+│ Passerelle [ 10.0.0.1 ] │
+│ DNS [ 10.0.0.1, 10.0.0.10 ] │
+│ Reconnexion via [ 10.0.4.25 ] │
+│ [preview] [run selected] │
+└────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 9. Paramètres frontend à concevoir
+
+Créer une spec pour les écrans/sections de paramètres suivants :
+
+Les paramètres frontend persistants doivent être sauvegardés côté backend/BDD :
+
+- thème ;
+- densité ;
+- zoom ;
+- taille/densité des tuiles ;
+- sections ouvertes par défaut ;
+- largeur des volets Hermes et terminal ;
+- activation du terminal SSH interactif ;
+- préférences de filtres terminal.
+
+Le navigateur peut garder une copie locale temporaire pour éviter un flash visuel au chargement, mais la source durable doit rester en BDD.
+
+### Vue globale onglet Paramètres
+
+L'onglet paramètres doit rester dense et opérationnel, sans page marketing.
+
+```text
+┌──────────────────────────────────────────────────────────────────────────────┐
+│ HEADER System Update [save] [close] │
+├──────────────────┬───────────────────────────────────────────────────────────┤
+│ SETTINGS NAV │ PARAMETRES │
+│ │ │
+│ > Général │ Général │
+│ Apparence │ Theme [dark v] Density [compact v] Zoom [100%] │
+│ Machines │ Panels Hermes [280px] Terminal [420px] │
+│ Docker │ │
+│ Scripts │ Icônes / application │
+│ Hermes/MCP │ Favicon [preview] Smartphone icon [preview] │
+│ Terminal SSH │ App name [System Update] PWA enabled [off] │
+│ Nettoyage │ │
+│ Sécurité │ Docker │
+│ Découverte │ Default scan depth [4] Prune mode [safe v] │
+│ │ │
+│ │ Scripts d'installation │
+│ │ [Docker officiel] [Node] [Rust] [PlatformIO] [custom +] │
+│ │ │
+│ │ Terminal SSH │
+│ │ Enable interactive SSH [off] Record sessions [off] │
+├──────────────────┴───────────────────────────────────────────────────────────┤
+│ FOOTER settings · unsaved changes 0 · db ok · hermes connected · 06:42 │
+└──────────────────────────────────────────────────────────────────────────────┘
+```
+
+Catégories minimales :
+
+- Général/apparence ;
+- Machines et valeurs par défaut ;
+- Docker ;
+- Scripts d'installation ;
+- Hermes/MCP ;
+- Terminal SSH ;
+- Logs/rapports/nettoyage ;
+- Sécurité/secrets ;
+- Découverte réseau ;
+- Icônes/application.
+
+### Terminal volet droit
+
+Le volet terminal droit doit être amélioré comme un vrai outil d'exploitation, pas seulement une zone xterm brute.
+
+Fonctions à concevoir :
+
+- en-tête clair avec machine sélectionnée : nom, IP, statut, action courante ;
+- séparation visuelle forte quand on change de machine ;
+- replay du buffer récent ;
+- autoscroll activable/désactivable ;
+- bouton pause/reprendre flux ;
+- bouton clear local ;
+- bouton copier sélection ;
+- recherche dans le terminal ;
+- filtre lignes importantes ;
+- mode log brut / mode réduit ;
+- indication WebSocket connecté/déconnecté ;
+- lien rapport/log brut après fin d'exécution ;
+- état "aucune machine sélectionnée" utile ;
+- redimensionnement robuste avec xterm fit ;
+- terminal plein écran ou drawer dédié sur petit écran.
+
+Deux modes doivent être distingués :
+
+1. **Mode exécution suivie** : terminal attaché aux actions lancées par la webapp (`apt`, Docker, post-install, reboot).
+2. **Mode SSH interactif** : ouverture d'un vrai shell SSH vers la machine sélectionnée, avec saisie de commandes par l'utilisateur.
+
+Le mode SSH interactif doit être clairement identifié comme plus risqué :
+
+- bouton d'ouverture explicite ;
+- indication machine/IP/utilisateur en permanence ;
+- confirmation si l'utilisateur ouvre un shell root ou sudo ;
+- journalisation de l'ouverture/fermeture de session ;
+- pas d'envoi automatique des commandes tapées à Hermes ;
+- masquage/censure des secrets dans le replay UI quand c'est possible ;
+- désactivation possible par paramètre global.
+
+Actions en icônes avec tooltips :
+
+- `terminal` : focus terminal ;
+- `pause` / `play` : pause/reprendre ;
+- `search` : chercher ;
+- `close` : clear local ou fermer drawer selon contexte ;
+- `download` : rapport/log ;
+- `alert` : erreurs filtrées.
+
+Le terminal ne doit jamais afficher de secret. Si une sortie contient un motif sensible, le design doit prévoir une étape backend de masquage avant diffusion UI.
+
+### Volet Hermes gauche
+
+Le volet Hermes doit présenter une discussion claire, distincte du terminal :
+
+- bulles ou lignes différenciées `Utilisateur` / `Hermes` / `Système` ;
+- horodatage discret ;
+- état streaming / génération en cours ;
+- bouton copier sur chaque message ;
+- sélection et copier-coller natifs dans le texte ;
+- historique scrollable sans masquer le dashboard ;
+- zone de saisie stable, multi-ligne, avec envoi contrôlé ;
+- rendu lisible des blocs de commande ;
+- bouton copier sur chaque bloc de commande ;
+- actions proposées affichées comme cartes séparées, jamais comme texte ambigu ;
+- lien vers rapports, logs réduits, snapshots ou exécutions quand Hermes les cite.
+
+Les commandes proposées par Hermes doivent être faciles à relire et copier :
+
+```text
+Hermes
+┌────────────────────────────────────┐
+│ Commande proposée │
+│ apt-get update │
+│ apt-get -s dist-upgrade │
+│ [copier] │
+└────────────────────────────────────┘
+```
+
+Une commande copiée depuis Hermes ne doit pas être exécutée automatiquement. Toute action SSH lancée par la webapp passe par les boutons/actions validées.
+
+### Mode smartphone / responsive
+
+Prévoir un brainstorming frontend spécifique pour le mode smartphone avant implémentation. Le layout desktop 3 volets ne peut pas simplement être compressé.
+
+Questions à trancher :
+
+- navigation par tabs ou bottom bar : `Hermes`, `Machines`, `Terminal` ;
+- terminal en drawer plein écran ou page dédiée ;
+- tuile machine en carte verticale ;
+- sections Docker/Post-install repliées par défaut ;
+- actions dangereuses en popup plein écran ;
+- clavier mobile pour champs IP/CIDR/hostname ;
+- taille minimale des touch targets ;
+- comportement du terminal avec clavier virtuel ;
+- conservation de la lisibilité dark/light ;
+- possibilité de masquer Hermes ou terminal pour garder le dashboard lisible.
+
+Proposition MVP mobile :
+
+```text
+┌────────────────────────────┐
+│ System Update [☰] │
+├────────────────────────────┤
+│ Machines │
+│ ┌────────────────────────┐ │
+│ │ ● vm_mqtt │ │
+│ │ APT 4 · Docker 1 │ │
+│ │ [↻] [⇩] [⏻] [▤] │ │
+│ │ ▸ Docker │ │
+│ │ ▸ Post-install │ │
+│ └────────────────────────┘ │
+├────────────────────────────┤
+│ [Hermes] [Machines] [Term] │
+└────────────────────────────┘
+```
+
+Cette partie doit produire une spec dédiée : breakpoints, écrans, composants, interactions tactiles, gestion terminal et validations.
+
+### Paramètres Docker par machine
+
+- roots Compose ;
+- profondeur de scan ;
+- stacks validés ;
+- stacks ignorés ;
+- mode prune ;
+- dernier scan ;
+- erreurs.
+
+### Paramètres scripts d'installation
+
+Catalogue global de scripts réutilisables :
+
+```text
+Paramètres
+└─ Scripts d’installation
+ ├─ Docker officiel Debian
+ ├─ Rust via rustup
+ ├─ Node.js via nvm / NodeSource
+ ├─ Python uv
+ ├─ PlatformIO / ESP
+ ├─ Personnalisation terminal
+ ├─ Script perso domotique
+ └─ ...
+```
+
+Chaque script doit afficher :
+
+- nom ;
+- catégorie ;
+- source ;
+- statut enabled/draft ;
+- variables attendues ;
+- niveau de risque ;
+- preview ;
+- dernière utilisation ;
+- JSON retour attendu.
+
+### Paramètres affichage tuile
+
+- sections ouvertes par défaut ou non ;
+- densité compacte/confort ;
+- affichage des chemins complets ou courts ;
+- seuil de fraîcheur d'une analyse APT/Docker ;
+- comportement expanded full-width.
+
+---
+
+## 10. JSON et intégration API
+
+Même si cette tâche est frontend, le design doit décrire les données nécessaires.
+
+Chaque interrogation/action affichée dans la tuile doit correspondre à un échange JSON :
+
+- snapshot machine ;
+- snapshot OS/profil machine ;
+- snapshot hardware ;
+- snapshot métriques simples ;
+- snapshot APT ;
+- snapshot Docker ;
+- manifeste post-install ;
+- preview template ;
+- résultat d'exécution ;
+- erreurs structurées.
+- messages importants extraits des logs : warnings, dépréciations, évolutions futures, erreurs ;
+- références de rapport/log consultables.
+
+Le frontend ne doit jamais parser du log brut pour décider l'état d'une tuile. Il consomme le JSON canonique produit par la webapp/backend.
+
+Hermes/MCP ne reçoivent que le JSON réduit, jamais de secret.
+
+---
+
+## 11. Livrables attendus
+
+À produire sous `docs/` :
+
+1. Design détaillé des tuiles machine.
+2. États UI : compact, Docker ouvert, Post-install ouvert, erreur, running, machine sans Docker.
+3. ASCII draw final + maquettes textuelles.
+4. Liste des composants design system à utiliser.
+5. Liste des icônes nécessaires et alias à ajouter au ui-kit.
+6. Spécification favicon, icônes smartphone et assets PWA.
+7. Vue globale page web : header, volet Hermes, centre, terminal, footer.
+8. Vue globale onglet paramètres.
+9. Spécification des paramètres frontend.
+10. Contrats JSON nécessaires côté frontend.
+11. Spec UX du volet Hermes : discussion, copier-coller, blocs de commandes, cartes de validation.
+12. Spec UX du terminal droit : exécution suivie vs SSH interactif.
+13. Découpage en sous-jalons d'implémentation.
+
+---
+
+## 12. Définition de terminé
+
+- Le design respecte le design system.
+- La tuile répond aux besoins APT, Docker et Post-install.
+- Les sections s'ouvrent sans masquer la zone centrale ni le terminal.
+- Les actions dangereuses sont clairement identifiées.
+- Les champs dynamiques post-install sont spécifiés.
+- Les vues globales dashboard et paramètres sont spécifiées.
+- Les besoins favicon, icônes smartphone et icônes SVG spécifiques sont listés.
+- Les échanges JSON nécessaires sont listés.
+- Le volet Hermes permet une discussion lisible, des messages copiables et des commandes copiables sans exécution automatique.
+- Le terminal SSH interactif est distingué du terminal de logs d'exécution et reste désactivable.
+- Aucun code de production n'est livré pendant cette mission de design.
+
+---
+
+## 13. Technos à utiliser — checklist
+
+- [ ] React + TypeScript.
+- [ ] Vite pour build frontend.
+- [ ] Design system `Gruvbox seventies`.
+- [ ] CSS variables uniquement, dark/light.
+- [ ] `ui-kit` existant avant création de composants.
+- [ ] Font Awesome via `Icon`/`IconButton`.
+- [ ] SVG custom uniquement pour icônes spécifiques validées dans `consigne_icon.md`.
+- [ ] xterm.js pour terminal web.
+- [ ] WebSocket/SSE pour flux live.
+- [ ] Web App Manifest + favicon/app icons si PWA prévue.
+- [ ] API backend uniquement, pas de parsing log brut côté client.
+
+## 14. URLs utiles
+
+- React TypeScript : https://react.dev/learn/typescript
+- Vite : https://vite.dev/guide/
+- Font Awesome : https://fontawesome.com/docs
+- xterm.js : https://xtermjs.org/
+- MDN Web App Manifest : https://developer.mozilla.org/en-US/docs/Web/Manifest
+- MDN Responsive design : https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Responsive_Design
+- MDN WebSocket : https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
+- MDN Server-sent events : https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
+
+## 15. Liens parent/enfant avec les autres tâches
+
+- Parents :
+ - `tache1.9.md` pour paramètres frontend et état courant.
+ - `tache2.md` pour contrats JSON APT/Docker.
+ - `tache4.md` pour profils/scripts à afficher.
+ - `tache5.md` pour API et WebSocket/SSE.
+- Enfants :
+ - `tache6.md` pour volet Hermes.
+ - `tache7.md` pour métriques/footer/mobile.
+ - `tache8.md` pour reprise partielle en app Rust/GNOME.
+ - `consigne_icon.md` pour création icônes.
+- Validation : `validation_tache3.md`.
diff --git a/tache4.md b/tache4.md
new file mode 100644
index 0000000..7953207
--- /dev/null
+++ b/tache4.md
@@ -0,0 +1,910 @@
+# Consigne de dev — Amélioration des scripts, profils post-install et installateurs
+
+> **Type** : mission d'**investigation + design scripts + contrats JSON** (PAS d'implémentation).
+> **Langue** : français.
+> **Livrable final attendu** : spec prête à passer en plan d'implémentation.
+
+---
+
+## 0. Contexte
+
+Cette tâche regroupe tout ce qui concerne l'amélioration des scripts d'installation, post-install, configuration système, outils, profils réutilisables et installateurs externes.
+
+Principe non négociable :
+
+> Chaque action ou interrogation sur un hôte doit produire un échange JSON clair entre la machine et la webapp.
+
+Les scripts peuvent streamer du log vers le terminal, mais l'état métier consommé par la webapp, Hermes et MCP doit être un JSON canonique.
+
+À lire avant de travailler :
+
+- `CLAUDE.md`
+- `deep-research-report(7).md`
+- `tache2.md`
+- `validation_tache2.md`
+- `templates/apt/*.tpl`
+- `server/ssh/client.ts`
+- `server/templates/render.ts`
+- `shared/types.ts`
+
+---
+
+## 1. Objectif
+
+Concevoir un système de scripts post-install et d'installateurs réutilisables pour préparer des machines Debian/Ubuntu/Proxmox/Raspberry Pi OS après ajout dans la webapp.
+
+Cas concret prioritaire :
+
+- VM Debian 13 installée via netinstall CLI ;
+- IP initiale via DHCP ;
+- connexion SSH sur l'IP DHCP ;
+- élévation root ou sudo ;
+- installation de prérequis ;
+- changement éventuel hostname/domaine `.home` ;
+- configuration IP statique ;
+- reboot vérifié ;
+- installation de groupes de paquets ;
+- installation Docker officiel ;
+- installation d'outils optionnels ;
+- personnalisation terminal.
+
+---
+
+## 2. Modèle général des scripts
+
+Un script ne doit jamais poser de questions interactives pendant l'exécution SSH.
+
+Toute variable nécessaire est demandée par la webapp avant exécution :
+
+- champs texte ;
+- select ;
+- multi-select ;
+- checkbox/toggle ;
+- IP/CIDR ;
+- chemin ;
+- utilisateur ;
+- confirmation de risque.
+
+Chaque script ou profil fournit un manifeste :
+
+```json
+{
+ "id": "identity_network",
+ "label": "Hostname + IP statique",
+ "description": "Configure le hostname, le domaine et l'IP statique de la machine.",
+ "category": "post_install",
+ "risk": "network_change",
+ "requiresConfirmation": true,
+ "fields": [],
+ "defaults": {},
+ "validations": [],
+ "steps": [],
+ "expectedJsonResult": "postInstall"
+}
+```
+
+Cycle recommandé d'un installateur :
+
+```text
+precheck
+→ install
+→ configure
+→ initialize
+→ verify
+→ report JSON
+```
+
+Le JSON de retour doit détailler chaque étape :
+
+```json
+{
+ "action": "install_recipe",
+ "recipeId": "docker_official_debian",
+ "status": "ok",
+ "steps": [
+ { "id": "precheck", "status": "ok" },
+ { "id": "install", "status": "ok" },
+ { "id": "configure", "status": "ok" },
+ { "id": "initialize", "status": "ok" },
+ { "id": "verify", "status": "ok" }
+ ],
+ "requiresRelogin": true,
+ "requiresReboot": true,
+ "errors": []
+}
+```
+
+---
+
+## 3. Profils post-install prioritaires
+
+### `bootstrap_root`
+
+Préparation minimale :
+
+- `sudo`
+- `resolvconf`
+- `ca-certificates`
+- `curl`
+- ajout utilisateur au groupe `sudo`
+- vérification sudo.
+
+### `identity_network`
+
+Configuration identité/réseau :
+
+- hostname ;
+- domaine/search `.home` ;
+- `/etc/hosts` ;
+- `/etc/network/interfaces` ;
+- IP statique `10.0.x.y/22` ;
+- gateway `10.0.0.1` ;
+- DNS `10.0.0.1`, `10.0.0.10` ;
+- reconnexion sur nouvelle IP ;
+- reboot vérifié si nécessaire.
+
+Variables :
+
+```json
+{
+ "newHostname": "debian-docker-01",
+ "domain": "home",
+ "interfaceName": "ens18",
+ "staticAddress": "10.0.4.25/22",
+ "gateway": "10.0.0.1",
+ "dnsNameservers": ["10.0.0.1", "10.0.0.10"],
+ "reconnectHost": "10.0.4.25"
+}
+```
+
+### `base_tools`
+
+Paquets de base, sans `vim` :
+
+- `nano`
+- `less`
+- `bash-completion`
+- `tmux`
+- `screen`
+- `htop`
+- `iotop`
+- `ncdu`
+- `tree`
+- `rsync`
+- `unzip`
+- `zip`
+- `tar`
+
+### `machine_probe`
+
+Détection OS, virtualisation, matériel et capacités.
+
+Ce script doit être proposé juste après l'ajout machine et relançable depuis les paramètres machine.
+
+Commandes/outils possibles :
+
+- `/etc/os-release`
+- `uname -a`
+- `dpkg --print-architecture`
+- `systemd-detect-virt`
+- `hostnamectl`
+- `lscpu`
+- `lsblk`
+- `findmnt`
+- `lspci` via `pciutils`
+- `lsusb` via `usbutils`
+- `dmidecode` seulement si utile et disponible ;
+- `/proc/cpuinfo` pour Raspberry Pi ;
+- `pveversion` et `pvesh` si Proxmox ;
+- état `qemu-guest-agent`/guest tools si VM.
+
+JSON attendu :
+
+```json
+{
+ "action": "machine_probe",
+ "status": "ok",
+ "os": {
+ "family": "debian",
+ "version": "13",
+ "codename": "trixie",
+ "arch": "amd64"
+ },
+ "machine": {
+ "kind": "vm",
+ "virtualization": "qemu",
+ "hypervisor": "kvm",
+ "raspberryPi": false,
+ "proxmoxHost": false
+ },
+ "hardware": {
+ "cpuModel": "Intel...",
+ "cpuCores": 4,
+ "memoryBytes": 4294967296,
+ "gpus": [],
+ "disks": []
+ },
+ "recommendations": [
+ {
+ "profileId": "vm_guest_tools",
+ "reason": "QEMU/KVM détecté"
+ }
+ ]
+}
+```
+
+Le résultat met à jour le profil OS/type machine proposé dans l'UI, mais l'utilisateur doit pouvoir corriger manuellement.
+
+### `machine_metrics_simple`
+
+Remontée légère CPU/RAM/disque pour tuiles, footer et Hermes.
+
+Commandes possibles :
+
+- charge CPU : `/proc/loadavg`, `uptime`, `nproc` ;
+- mémoire : `free -b` ou `/proc/meminfo` ;
+- disque : `df -h` et `df -B1` ;
+- inodes : `df -i` ;
+- uptime : `/proc/uptime` ;
+- température optionnelle : `sensors` si installé, Raspberry Pi via `vcgencmd` si disponible.
+
+JSON attendu :
+
+```json
+{
+ "action": "machine_metrics_simple",
+ "status": "ok",
+ "collectedAt": "ISO",
+ "cpu": {
+ "load1": 0.08,
+ "load5": 0.12,
+ "cores": 4
+ },
+ "memory": {
+ "totalBytes": 4294967296,
+ "usedBytes": 1123456789,
+ "availableBytes": 2987654321
+ },
+ "filesystems": [
+ {
+ "mount": "/",
+ "fstype": "ext4",
+ "sizeBytes": 32100000000,
+ "usedBytes": 9300000000,
+ "usedPercent": 29
+ }
+ ],
+ "warnings": []
+}
+```
+
+Ce script doit être non destructif, rapide, lançable en tâche planifiée, et ne doit pas nécessiter d'installation lourde.
+
+### `apt_repositories`
+
+Analyse et configuration contrôlée des dépôts selon OS.
+
+Besoins :
+
+- Debian : vérifier `main`, `contrib`, `non-free`, `non-free-firmware` selon les profils firmware/drivers ;
+- Ubuntu : vérifier `main`, `universe`, `restricted`, `multiverse` selon besoins drivers ;
+- Proxmox : vérifier dépôts PVE enterprise/no-subscription, dépôts Debian compatibles, warnings si repo absent/incohérent ;
+- Raspberry Pi OS : vérifier dépôts Raspberry Pi OS et Debian associés, sans remplacer par une Debian générique.
+
+Le script doit d'abord produire une analyse, puis proposer une action séparée et validée pour modifier les dépôts.
+
+### `firmware_tools`
+
+Profil machine physique/portable/serveur.
+
+Paquets et outils possibles selon OS :
+
+- `fwupd`
+- `pciutils`
+- `usbutils`
+- `dmidecode`
+- `lshw`
+- `lm-sensors`
+- `smartmontools`
+- firmwares Debian selon matériel détecté.
+
+Règles :
+
+- jamais installer du firmware propriétaire sans profil/repo compatible et validation ;
+- sur VM, proposer seulement si passthrough ou matériel réel détecté ;
+- sur Proxmox bare-metal, tenir compte des recommandations firmware host.
+
+### `gpu_drivers`
+
+Installation/diagnostic GPU par constructeur.
+
+Sous-profils :
+
+- `gpu_nvidia`
+- `gpu_amd`
+- `gpu_intel`
+- `gpu_intel_arc`
+
+Principe :
+
+- première étape obligatoire : `machine_probe` + détection GPU ;
+- deuxième étape : analyse repos/paquets disponibles ;
+- troisième étape : proposition d'installation selon OS.
+
+Contraintes :
+
+- Debian : les drivers/firmwares peuvent nécessiter `contrib`, `non-free`, `non-free-firmware` ;
+- Ubuntu : utiliser `ubuntu-drivers` quand disponible pour recommander les pilotes NVIDIA/GPU ;
+- AMD/Intel : privilégier le stack kernel/Mesa/firmware de la distribution avant scripts externes ;
+- Intel Arc : vérifier kernel/firmware/Mesa suffisamment récents avant installation ;
+- Proxmox : drivers GPU seulement si besoin host/passthrough/transcodage, jamais par défaut ;
+- VM : ne pas proposer sauf GPU passthrough détecté.
+
+### `benchmark_tools`
+
+Outils de test et benchmark, optionnels et jamais installés par défaut.
+
+Catégories :
+
+- CPU : `sysbench`, `stress-ng` ;
+- disque : `fio`, `hdparm` en lecture contrôlée ;
+- réseau : `iperf3` ;
+- monitoring ponctuel : `sysstat` selon disponibilité ;
+- hardware : `hardinfo` seulement si pertinent avec interface graphique, sinon éviter.
+
+Règles :
+
+- les benchmarks peuvent charger la machine : confirmation explicite ;
+- pas de test disque destructif ;
+- rapport JSON avec commande, durée, score, erreurs ;
+- utile surtout pour machine physique, Proxmox host, serveur media ou dev.
+
+### `network_tools`
+
+- `iproute2`
+- `iputils-ping`
+- `dnsutils`
+- `traceroute`
+- `net-tools` optionnel
+- `tcpdump`
+- `nmap`
+- `mtr-tiny`
+- `lsof`
+- `netcat-openbsd`
+
+`nmap` est ici classé comme **outil réseau d'administration** pour découverte locale, diagnostic et inventaire contrôlé. Les usages plus intrusifs ou offensifs relèvent du profil `security_lab`, jamais installé par défaut.
+
+### `dev_git`
+
+- `git`
+- `curl`
+- `wget`
+- `jq`
+- `yq`
+- `gnupg`
+- `lsb-release`
+- `build-essential` optionnel.
+
+### `sharing`
+
+Partage réseau :
+
+- Samba ;
+- NFS ;
+- mDNS/Avahi ;
+- `wsdd2`.
+
+Sous-profils :
+
+- `sharing_samba`
+- `sharing_nfs`
+- `sharing_mdns`
+- `sharing_wsdd2`
+
+### `docker_official`
+
+Installation Docker via documentation officielle Debian :
+
+- ajout clé GPG officielle dans `/etc/apt/keyrings` ;
+- fichier `docker.sources` ;
+- `docker-ce`
+- `docker-ce-cli`
+- `containerd.io`
+- `docker-buildx-plugin`
+- `docker-compose-plugin`
+- `usermod -aG docker `
+- création dossier `/home/gilles/docker` ou variable ;
+- enable/start service ;
+- verify `docker version` + `docker compose version`;
+- reboot ou relogin selon besoin.
+
+Docker doit être modélisé comme **installateur externe officiel**, pas comme simple groupe de paquets Debian.
+
+---
+
+## 4. Profils additionnels à prévoir
+
+### `home_automation`
+
+- `mosquitto`
+- `mosquitto-clients`
+- `bluetooth`
+- `bluez`
+- `avahi-daemon`
+- `dbus`
+- `jq`
+- `curl`
+- `socat`
+- `ser2net`
+- Zigbee2MQTT via script externe optionnel.
+
+### `dev_tools`
+
+Paquets Debian :
+
+- `git`
+- `build-essential`
+- `pkg-config`
+- `cmake`
+- `python3`
+- `python3-venv`
+- `python3-pip`
+- `pipx`
+
+Installateurs externes optionnels :
+
+- Python `uv`
+- Rust via `rustup`
+- Node.js via `nvm` ou NodeSource
+- npm récent si besoin.
+
+### `embedded_esp_platformio`
+
+- `python3-venv`
+- `pipx`
+- PlatformIO via `pipx` ou script validé ;
+- `esptool`
+- `openocd`
+- `avrdude`
+- `dfu-util`
+- `cmake`
+- `ninja-build`
+- `build-essential`
+- ajout utilisateur aux groupes `dialout`, `plugdev`;
+- règles udev ESP/USB si nécessaires ;
+- relogin/reboot requis.
+
+### `dev_ide`
+
+Profil desktop optionnel :
+
+- VS Code ou VSCodium ;
+- CLI `code` ;
+- extensions optionnelles Python, C/C++, PlatformIO, ESP-IDF.
+
+### `storage_health`
+
+- `smartmontools`
+- `nvme-cli`
+- `hdparm`
+- `sdparm`
+- `lsscsi`
+- `sg3-utils`
+- `parted`
+- `gdisk`
+- `fio`
+
+### `media_tools`
+
+- `ffmpeg`
+- `mediainfo`
+- `imagemagick`
+- `mpv` optionnel
+- `vlc` optionnel, surtout desktop
+- `alsa-utils`
+- `pipewire-utils` ou `pulseaudio-utils` selon système.
+
+### `security_audit`
+
+Pour audit légitime :
+
+- `nmap`
+- `whois`
+- `dnsutils`
+- `tcpdump`
+- `tshark`
+- `lynis`
+- `testssl.sh` via script externe optionnel.
+
+### `security_lab`
+
+Profil high-risk, jamais par défaut :
+
+- `nmap`
+- `masscan`
+- `hydra`
+- `john`
+- `hashcat`
+- `gobuster`
+- `dirsearch`
+- `sqlmap`
+- `metasploit-framework`
+- wordlists.
+
+L'UI doit afficher un avertissement clair : usage uniquement sur systèmes autorisés/lab.
+
+Distinction attendue dans la spec :
+
+- `network_tools/nmap` : scan simple et local, ex. port SSH `22`, inventaire de machines autorisées.
+- `security_audit/nmap` : audit défensif, réseau appartenant à l'utilisateur.
+- `security_lab/nmap` : scénarios offensifs/lab, high-risk, confirmation explicite, jamais inclus dans une installation standard.
+
+### `backup_sync`
+
+- `restic`
+- `borgbackup`
+- `rclone`
+- `rsync`
+- `syncthing`
+- `duplicity`
+
+### `monitoring`
+
+- `prometheus-node-exporter`
+- `lm-sensors`
+- `smartmontools`
+- `collectd`
+- Netdata via script externe optionnel.
+
+### `network_services`
+
+- `nginx`
+- `caddy`
+- `certbot`
+- `wireguard-tools`
+- Tailscale via script externe optionnel
+- `openssh-server`
+- `fail2ban`
+
+### `vm_guest_tools`
+
+- `qemu-guest-agent`
+- `open-vm-tools`
+- choix selon hyperviseur.
+
+---
+
+## 5. Scripts de partage — exemples attendus
+
+### Samba
+
+Variables UI :
+
+```json
+{
+ "shareName": "docker-share",
+ "path": "/home/gilles/docker",
+ "validUsers": ["gilles"],
+ "forceGroup": "gilles",
+ "readOnly": false,
+ "browseable": true,
+ "createDirectory": true,
+ "enableMdns": true,
+ "enableWsdd2": true
+}
+```
+
+Retour JSON :
+
+```json
+{
+ "action": "post_install_sharing_samba",
+ "status": "ok",
+ "changed": true,
+ "packagesInstalled": ["samba", "avahi-daemon", "libnss-mdns", "wsdd2"],
+ "filesChanged": ["/etc/samba/smb.conf"],
+ "directoriesCreated": ["/home/gilles/docker"],
+ "services": [
+ { "name": "smbd", "enabled": true, "active": true },
+ { "name": "nmbd", "enabled": true, "active": true },
+ { "name": "avahi-daemon", "enabled": true, "active": true },
+ { "name": "wsdd2", "enabled": true, "active": true }
+ ],
+ "errors": []
+}
+```
+
+### NFS
+
+Variables UI :
+
+```json
+{
+ "exportName": "docker-nfs",
+ "path": "/home/gilles/docker",
+ "allowedNetwork": "10.0.0.0/22",
+ "access": "rw",
+ "syncMode": "sync",
+ "rootSquash": true,
+ "createDirectory": true
+}
+```
+
+Retour JSON :
+
+```json
+{
+ "action": "post_install_sharing_nfs",
+ "status": "ok",
+ "changed": true,
+ "packagesInstalled": ["nfs-kernel-server"],
+ "filesChanged": ["/etc/exports"],
+ "exports": [
+ {
+ "path": "/home/gilles/docker",
+ "allowedNetwork": "10.0.0.0/22",
+ "options": ["rw", "sync", "root_squash"]
+ }
+ ],
+ "services": [
+ { "name": "nfs-kernel-server", "enabled": true, "active": true }
+ ],
+ "errors": []
+}
+```
+
+---
+
+## 6. Installateurs externes réutilisables
+
+Créer une spec pour une section paramètres webapp :
+
+```text
+Paramètres
+└─ Scripts d’installation
+ ├─ Docker officiel Debian
+ ├─ Rust via rustup
+ ├─ Node.js via nvm / NodeSource
+ ├─ Python uv
+ ├─ PlatformIO / ESP
+ ├─ Personnalisation terminal
+ ├─ Zigbee2MQTT
+ └─ Script perso
+```
+
+Chaque installateur doit être :
+
+- versionné sur disque ;
+- rendu via Mustache ;
+- prévisualisable ;
+- validé par formulaire ;
+- journalisé ;
+- traçable dans un rapport ;
+- accompagné d'un JSON résultat ;
+- non interactif ;
+- sans secret en clair.
+
+Règles de sécurité :
+
+- pas de `curl | sh` opaque sans justification ;
+- URL officielle documentée ;
+- checksum/signature si disponible ;
+- confirmation obligatoire ;
+- rollback ou sauvegarde quand un fichier système est modifié ;
+- erreur structurée si une décision manque.
+
+---
+
+## 7. Initialisation complémentaire des outils
+
+Prévoir que certains installateurs nécessitent des commandes après installation.
+
+Chaque recette doit pouvoir déclarer :
+
+- `precheck`
+- `install`
+- `configure`
+- `initialize`
+- `verify`
+- `postNotes`
+
+Exemples :
+
+- Docker : enable service, usermod docker, créer dossier Compose, vérifier `docker compose`.
+- Rust : installer toolchain, vérifier `cargo`, ajouter PATH utilisateur.
+- Node : installer nvm/NodeSource, vérifier `node`, `npm`, `corepack`.
+- PlatformIO : installer via pipx, vérifier `pio`, ajouter user à `dialout`.
+- Samba : écrire partage, `testparm`, restart service, vérifier service actif.
+- NFS : écrire exports, `exportfs -ra`, vérifier exports.
+
+---
+
+## 8. Personnalisation terminal
+
+Créer un profil `terminal_customization`.
+
+Fonctions :
+
+- MOTD ;
+- message d'accueil SSH ;
+- prompt bash ;
+- couleurs/style ;
+- aliases ;
+- affichage hostname ;
+- affichage IP ;
+- affichage branche Git ;
+- bannière maintenance ;
+- configuration par utilisateur.
+
+Variables UI :
+
+```json
+{
+ "targetUser": "gilles",
+ "theme": "gruvbox",
+ "showHostname": true,
+ "showIp": true,
+ "showGitBranch": true,
+ "enableMotd": true,
+ "welcomeMessage": "Bienvenue sur {{hostname}}",
+ "aliases": ["ll", "la", "update", "dps", "dcu"]
+}
+```
+
+Retour JSON :
+
+```json
+{
+ "action": "post_install_terminal_customization",
+ "status": "ok",
+ "changed": true,
+ "targetUser": "gilles",
+ "filesChanged": [
+ "/home/gilles/.bashrc",
+ "/etc/update-motd.d/99-system-update"
+ ],
+ "backupFiles": [
+ "/home/gilles/.bashrc.su-backup-{{backupDate}}"
+ ],
+ "verify": {
+ "bashrcSyntax": "ok",
+ "motdScriptExecutable": true
+ },
+ "errors": []
+}
+```
+
+---
+
+## 9. JSON canonique
+
+Tout script doit produire un résultat structuré.
+
+Champs communs :
+
+```json
+{
+ "action": "post_install_profile",
+ "profileId": "base_tools",
+ "status": "ok",
+ "startedAt": "ISO",
+ "finishedAt": "ISO",
+ "changed": true,
+ "steps": [],
+ "packagesInstalled": [],
+ "filesChanged": [],
+ "services": [],
+ "requiresReboot": false,
+ "requiresRelogin": false,
+ "errors": []
+}
+```
+
+Le log brut reste archivé. Hermes/MCP ne reçoivent que le JSON réduit et les lignes importantes.
+
+---
+
+## 10. Erreurs à prévoir
+
+Taxonomie minimale :
+
+- `missing_required_input`
+- `unsupported_os`
+- `unsupported_architecture`
+- `network_unreachable`
+- `apt_update_failed`
+- `package_install_failed`
+- `external_download_failed`
+- `signature_verification_failed`
+- `service_enable_failed`
+- `service_start_failed`
+- `verify_failed`
+- `network_config_invalid`
+- `reconnect_failed`
+- `user_not_found`
+- `permission_denied`
+- `human_interaction_required`
+- `timeout`
+
+Aucune auto-réparation dangereuse sans validation explicite.
+
+---
+
+## 11. Livrables attendus
+
+À produire sous `docs/` :
+
+1. Catalogue complet des profils post-install.
+2. Manifeste type d'un profil/script.
+3. Modèle de recette `precheck/install/configure/initialize/verify`.
+4. Spec des installateurs externes réutilisables.
+5. Spec des scripts réseau, partage Samba/NFS/wsdd2.
+6. Spec Docker officiel.
+7. Spec détection machine/hardware, métriques simples, firmware, drivers GPU et benchmark.
+8. Spec dev tools, embedded ESP/PlatformIO, terminal customization.
+9. JSON canoniques d'entrée/sortie.
+10. Taxonomie d'erreurs.
+11. Découpage en sous-jalons.
+
+---
+
+## 12. Définition de terminé
+
+- Les profils sont classés et optionnels.
+- Docker est traité comme installateur externe officiel.
+- Les scripts nécessitant des champs UI sont modélisés.
+- Les scripts tiennent compte du couple OS/type machine.
+- La détection hardware et les métriques simples sont prévues.
+- Les drivers/firmware/benchmark restent optionnels et validés explicitement.
+- Les étapes complémentaires d'initialisation sont prévues.
+- Les retours JSON sont spécifiés.
+- Les secrets sont exclus des logs/UI/MCP.
+- Aucun code de production n'est livré pendant cette mission de design.
+
+---
+
+## 13. Technos à utiliser — checklist
+
+- [ ] Bash templates versionnés sur disque.
+- [ ] Mustache pour variables de scripts.
+- [ ] JSON canonique en sortie de chaque script.
+- [ ] `apt-get` non interactif pour paquets Debian/Ubuntu.
+- [ ] Docker Engine dépôt officiel pour Docker.
+- [ ] `systemd`/services pour enable/start/verify quand disponible.
+- [ ] `systemd-detect-virt`, `/etc/os-release`, `lspci`, `lsusb`, `lsblk`, `df`, `free` pour détection.
+- [ ] `qemu-guest-agent` / `open-vm-tools` selon VM.
+- [ ] `smartmontools`, `lm-sensors`, `fwupd` optionnels pour physique.
+- [ ] `ubuntu-drivers` pour recommandations GPU Ubuntu.
+- [ ] `rustup`, `nvm`/NodeSource, `uv`, `pipx`, PlatformIO selon profils dev.
+- [ ] Aucune commande interactive pendant SSH.
+
+## 14. URLs utiles
+
+- Docker Engine Debian : https://docs.docker.com/engine/install/debian/
+- Docker Compose plugin Linux : https://docs.docker.com/compose/install/linux/
+- Debian NetworkConfiguration : https://wiki.debian.org/NetworkConfiguration
+- Debian Handbook network config : https://www.debian.org/doc/manuals/debian-handbook/sect.network-config
+- Samba documentation : https://www.samba.org/samba/docs/
+- Debian NFS server wiki : https://wiki.debian.org/NFSServerSetup
+- Avahi : https://www.avahi.org/
+- Rustup : https://rustup.rs/
+- NodeSource distributions : https://github.com/nodesource/distributions
+- nvm : https://github.com/nvm-sh/nvm
+- uv : https://docs.astral.sh/uv/
+- pipx : https://pipx.pypa.io/
+- PlatformIO : https://docs.platformio.org/
+- Ubuntu NVIDIA drivers : https://ubuntu.com/server/docs/nvidia-drivers-installation
+- fwupd : https://fwupd.org/
+- smartmontools : https://www.smartmontools.org/
+
+## 15. Liens parent/enfant avec les autres tâches
+
+- Parents :
+ - `tache2.md` pour moteur templates et sécurité scripts.
+ - `tache1.9.md` pour stockage profils/recettes/versions.
+- Enfants :
+ - `tache5.md` pour exécution, stockage résultats, API.
+ - `tache3.md` pour formulaires de profils et paramètres.
+ - `tache6.md` pour analyse Hermes des erreurs scripts.
+ - `tache7.md` pour métriques simples, sécurité secrets, nettoyage.
+- Validation : `validation_tache4.md`.
diff --git a/tache5.md b/tache5.md
new file mode 100644
index 0000000..fb777d7
--- /dev/null
+++ b/tache5.md
@@ -0,0 +1,532 @@
+# Consigne de dev — Backend, historique JSON et automatisations
+
+> **Type** : mission d'**investigation + design backend** (PAS d'implémentation).
+> **Langue** : français.
+> **Livrable final attendu** : spec backend prête à passer en plan d'implémentation.
+
+---
+
+## 0. Contexte
+
+La webapp `system_update` exécute des scripts SSH agentless sur des machines Linux et reçoit des sorties normalisées en JSON canonique :
+
+- snapshots APT ;
+- résultats d'exécution APT ;
+- snapshots Docker ;
+- résultats Docker ;
+- profils post-install ;
+- rapports ;
+- erreurs structurées ;
+- état reboot/reconnexion.
+
+Cette tâche vise le backend : **sauvegarde, historisation, automatisations planifiées, icônes/statuts machine, conservation et API interne**.
+
+À lire avant de travailler :
+
+- `CLAUDE.md`
+- `tache1.9.md`
+- `tache2.md`
+- `tache3.md`
+- `tache4.md`
+- `validation_tache2.md`
+- `shared/types.ts`
+- `server/db/schema.ts`
+- `server/services/refresh.ts`
+- `server/services/execute.ts`
+- `server/jobs/worker.ts`
+- `server/ws/outputHub.ts`
+
+---
+
+## 1. Objectif
+
+Concevoir le backend qui stocke et exploite tous les échanges JSON entre machine et webapp.
+
+Le backend doit permettre :
+
+- historiser chaque interrogation/action machine ;
+- relier snapshot, exécution, log brut et rapport Markdown ;
+- afficher l'état actuel des icônes/statuts par machine ;
+- planifier des tâches automatiques, par exemple `update/analyse` de toutes les machines à heure précise ;
+- déclencher les refresh Docker/APT/post-install selon un planning ;
+- gérer erreurs, retries, verrouillage et idempotence ;
+- exposer une API stable pour l'UI, Hermes/MCP et les rapports.
+
+---
+
+## 2. Données à sauvegarder
+
+Chaque échange machine ↔ webapp doit être sauvegardé sous forme structurée.
+
+### Snapshots
+
+- `machine_snapshot`
+- `machine_probe_snapshot`
+- `machine_metrics_snapshot`
+- `apt_update_analyze_snapshot`
+- `docker_scan_snapshot`
+- `docker_pull_check_snapshot`
+- `post_install_manifest_snapshot`
+- `reboot_check_snapshot`
+
+Champs communs :
+
+```json
+{
+ "id": "snap_x",
+ "machineId": "machine_x",
+ "kind": "apt_update_analyze",
+ "createdAt": "ISO",
+ "status": "ok",
+ "payload": {},
+ "importantLines": [],
+ "rawLogRef": "reports/machine_x/snap_x.log"
+}
+```
+
+### Exécutions
+
+- `apt_upgrade`
+- `apt_full_upgrade`
+- `apt_autoremove`
+- `apt_clean`
+- `docker_apply`
+- `docker_prune`
+- `post_install_profile`
+- `reboot_verified`
+
+Champs communs :
+
+```json
+{
+ "executionId": "exec_x",
+ "machineId": "machine_x",
+ "action": "apt_upgrade",
+ "mode": "manual",
+ "startedAt": "ISO",
+ "finishedAt": "ISO",
+ "status": "ok",
+ "payload": {},
+ "rawLogRef": "reports/machine_x/exec_x.log",
+ "reportRef": "reports/machine_x/exec_x.md"
+}
+```
+
+### Logs, rapports et messages importants
+
+Les logs bruts et rapports doivent être accessibles à Hermes, mais par références contrôlées :
+
+- le JSON canonique complet reste en BDD ;
+- le log brut complet reste dans `reports//...log` ou dans un stockage d'artefacts ;
+- le rapport Markdown reste dans `reports//...md` ;
+- la BDD garde `rawLogRef`, `reportRef`, taille, checksum, dates, statut de rétention ;
+- Hermes reçoit par défaut un résumé réduit + des références, pas le log complet.
+
+Le backend doit extraire et stocker les messages importants rencontrés dans les sorties APT/Docker/scripts :
+
+- erreurs bloquantes : `E:`, `dpkg: error`, lock APT, maintainer script en échec ;
+- warnings opérationnels : `W:`, dépôt obsolète, signature, clé GPG, service non redémarré ;
+- messages d'évolution future : annonce de changement majeur Debian/Ubuntu, sécurité paquet, dépréciation de dépôt, changement de politique de paquet ;
+- messages demandant analyse agent : évolution sécurité, migration de version majeure, configuration legacy, composant bientôt non supporté.
+
+Ces messages doivent être stockés comme objets structurés, pas seulement comme lignes de log :
+
+```json
+{
+ "messageId": "msg_x",
+ "machineId": "machine_x",
+ "source": "apt",
+ "category": "future_major_change",
+ "severity": "warning",
+ "packageName": "openssh-server",
+ "message": "résumé nettoyé sans secret",
+ "rawLineRef": "artifact_x#line_381",
+ "snapshotId": "snap_x",
+ "executionId": null,
+ "createdAt": "ISO",
+ "acknowledged": false
+}
+```
+
+Objectif :
+
+- afficher ces warnings dans la tuile machine ;
+- permettre à Hermes de rechercher les évolutions importantes sans relire tous les logs ;
+- garder une trace d'un warning même si le prochain `apt update` ne l'affiche plus ;
+- générer des rapports de veille, par exemple "risques Debian à traiter avant la prochaine version majeure".
+
+### Événements machine
+
+Prévoir une table ou collection d'événements :
+
+- machine ajoutée ;
+- connexion testée ;
+- snapshot créé ;
+- update disponible ;
+- action lancée ;
+- action terminée ;
+- erreur ;
+- reboot demandé ;
+- machine revenue ;
+- rapport créé ;
+- notification envoyée.
+
+Ces événements alimentent :
+
+- timeline machine ;
+- audit ;
+- Hermes ;
+- notifications ;
+- rapports globaux.
+
+---
+
+## 3. État courant machine
+
+Le backend doit dériver un état courant par machine à partir des derniers snapshots/exécutions.
+
+État machine minimal :
+
+```json
+{
+ "machineId": "machine_x",
+ "status": "ok",
+ "apt": {
+ "status": "updates_available",
+ "updatesCount": 4,
+ "lastAnalyzeAt": "ISO",
+ "rebootRequired": false
+ },
+ "docker": {
+ "status": "updates_available",
+ "installed": true,
+ "stacksCount": 3,
+ "updatesCount": 1,
+ "lastScanAt": "ISO",
+ "pruneAvailable": true
+ },
+ "postInstall": {
+ "availableProfiles": 12,
+ "pendingProfiles": 0,
+ "lastRunAt": "ISO"
+ },
+ "profile": {
+ "osFamily": "debian",
+ "osVersion": "13",
+ "osCodename": "trixie",
+ "arch": "amd64",
+ "machineKind": "vm",
+ "virtualization": "qemu",
+ "hardwareProfile": "generic_vm"
+ },
+ "metrics": {
+ "lastCollectedAt": "ISO",
+ "cpuLoad1": 0.08,
+ "memoryUsedPercent": 26,
+ "rootUsedPercent": 29,
+ "diskWarnings": 0
+ },
+ "lastError": null
+}
+```
+
+Cet état sert à mettre à jour les icônes dans les tuiles machine :
+
+- LED machine ;
+- badge APT update ;
+- badge Docker update ;
+- reboot requis ;
+- erreur ;
+- action running ;
+- prune disponible ;
+- warning matériel/driver/repo ;
+- alerte disque/RAM simple.
+
+---
+
+## 4. Automatisations backend
+
+### Besoin prioritaire
+
+Planifier automatiquement :
+
+- `apt_update_analyze` de toutes les machines à heure précise ;
+- `machine_metrics_simple` périodique sur toutes les machines ou les machines sélectionnées ;
+- éventuellement Docker scan/pull-check selon configuration ;
+- mise à jour des icônes si des updates sont disponibles ;
+- notification ou rapport si anomalies.
+
+Exemple :
+
+```json
+{
+ "id": "schedule_daily_update_analyze",
+ "name": "Analyse quotidienne",
+ "enabled": true,
+ "schedule": "0 6 * * *",
+ "timezone": "Europe/Paris",
+ "scope": {
+ "machineIds": "all",
+ "tags": []
+ },
+ "actions": [
+ "apt_update_analyze",
+ "machine_metrics_simple",
+ "docker_scan"
+ ],
+ "concurrency": 2,
+ "notifyOn": ["updates_available", "error"]
+}
+```
+
+### Moteur de planification
+
+MVP recommandé :
+
+- `croner` déjà présent dans le projet ;
+- jobs in-process ;
+- persistance des schedules en DB ;
+- verrou machine pour éviter deux actions simultanées sur la même machine.
+
+Alternative future :
+
+- PostgreSQL + `pg-boss` si besoin de jobs distribués, reprise robuste, retries persistants.
+
+Le design doit trancher MVP / futur.
+
+---
+
+## 5. Verrouillage et idempotence
+
+Règles :
+
+- une machine ne peut exécuter qu'une action à risque à la fois ;
+- un refresh APT ne doit pas courir pendant un upgrade ;
+- Docker scan peut être autorisé si aucune action Docker destructive n'est en cours ;
+- post-install réseau bloque toute autre action machine ;
+- reboot bloque tout jusqu'à reconnexion ou timeout.
+
+Prévoir :
+
+- table `machine_locks` ou statut job courant ;
+- `idempotencyKey` pour éviter les doubles clics ;
+- retries contrôlés ;
+- timeout global ;
+- timeout d'inactivité ;
+- reprise après redémarrage serveur selon le moteur choisi.
+
+---
+
+## 6. API backend attendue
+
+À spécifier :
+
+```text
+GET /api/capabilities
+GET /api/system/status
+GET /api/system/metrics
+
+GET /api/machines
+GET /api/machines/:id/state
+GET /api/machines/:id/hardware
+GET /api/machines/:id/metrics
+GET /api/machines/:id/snapshots
+GET /api/machines/:id/snapshots/:snapshotId
+GET /api/machines/:id/executions
+GET /api/machines/:id/executions/:executionId
+GET /api/machines/:id/events
+GET /api/machines/:id/messages
+POST /api/machines/:id/actions
+
+GET /api/artifacts/:artifactId
+GET /api/artifacts/:artifactId/important-lines
+GET /api/reports
+GET /api/reports/:id
+GET /api/messages
+
+GET /api/schedules
+POST /api/schedules
+PATCH /api/schedules/:id
+POST /api/schedules/:id/run-now
+POST /api/schedules/:id/pause
+POST /api/schedules/:id/resume
+DELETE /api/schedules/:id
+
+GET /api/settings
+PATCH /api/settings
+
+GET /api/events
+WS /api/ws/machines/:id/output
+
+GET /api/search
+```
+
+Clients visés :
+
+- frontend web ;
+- Hermes/MCP via backend ;
+- future app locale Rust/GNOME ;
+- scripts d'administration internes éventuels.
+
+Chaque endpoint doit garantir :
+
+- aucun secret ;
+- JSON stable ;
+- erreurs structurées ;
+- pagination sur historiques longs ;
+- filtres par machine, action, statut, dates.
+- versionnement API.
+
+### Authentification clients API
+
+Prévoir pour la future app locale :
+
+- tokens de clients distincts des credentials machines ;
+- scopes : `read`, `operate`, `admin`, `debug_logs` ;
+- révocation ;
+- rotation ;
+- audit des appels ;
+- stockage local côté app via trousseau système, jamais dans le navigateur.
+
+Exemple capabilities :
+
+```json
+{
+ "apiVersion": "1",
+ "features": {
+ "machines": true,
+ "actions": true,
+ "terminalOutput": true,
+ "interactiveSsh": false,
+ "hermes": true,
+ "settings": true
+ }
+}
+```
+
+---
+
+## 7. Rapports et rétention
+
+Prévoir une politique :
+
+- log brut complet conservé sous `reports/`;
+- JSON canonique conservé en DB ;
+- rapport Markdown lié à l'exécution ;
+- rétention configurable ;
+- purge manuelle ;
+- export global.
+
+Les logs bruts ne sont jamais envoyés à Hermes sans réduction déterministe.
+
+Règles spécifiques Hermes :
+
+- Hermes peut lire les rapports Markdown et les JSON réduits via API/MCP ;
+- Hermes peut demander des extraits ciblés de log par `artifactId`, plage de lignes ou recherche ;
+- accès log complet uniquement si l'utilisateur le demande explicitement ou si le rapport d'erreur l'exige ;
+- toute sortie est masquée côté backend avant exposition ;
+- chaque lecture Hermes est auditée.
+
+Nettoyage :
+
+- purge automatique des vieux logs selon paramètres ;
+- conservation plus longue des logs d'échec et warnings non acquittés ;
+- conservation des rapports épinglés ;
+- dry-run obligatoire pour purge manuelle ;
+- export possible avant suppression ;
+- suppression en BDD et fichiers cohérente, avec vérification des chemins.
+
+---
+
+## 8. Notifications et intégration Hermes
+
+Le backend doit pouvoir notifier :
+
+- UI via WebSocket/SSE ;
+- Hermes via webhook ou MCP selon tâche 6 ;
+- messagerie via Hermes gateway si configuré.
+
+Exemples :
+
+- "3 machines ont des updates APT disponibles" ;
+- "Docker prune possible sur vm_mqtt" ;
+- "Upgrade terminé avec erreur dpkg" ;
+- "Machine revenue après reboot en 74s".
+
+Les notifications doivent contenir uniquement JSON réduit et références de rapport.
+
+---
+
+## 9. Livrables attendus
+
+À produire sous `docs/` :
+
+1. Schéma de données backend.
+2. Modèle d'événements machine.
+3. Modèle d'état courant machine.
+4. Spécification des schedules automatisés.
+5. Règles de verrouillage/idempotence.
+6. API backend.
+7. Modèle des messages importants extraits des logs.
+8. Modèle snapshots hardware/profil machine/métriques simples.
+9. Politique de rétention/logs/rapports/messages.
+10. Intégration avec UI et Hermes.
+11. Découpage en sous-jalons.
+
+---
+
+## 10. Définition de terminé
+
+- Tous les JSON machine ↔ webapp sont historisables.
+- L'état courant des tuiles peut être dérivé sans parser les logs.
+- Les tâches automatiques sont spécifiées.
+- Les métriques simples et le profil OS/type machine sont historisables.
+- Les actions concurrentes sont sécurisées.
+- Les rapports et logs ont une politique claire.
+- Les warnings importants, dépréciations et annonces d'évolution future sont historisés et consultables.
+- Aucun secret ne sort du backend.
+- Aucun code de production n'est livré pendant cette mission de design.
+
+---
+
+## 11. Technos à utiliser — checklist
+
+- [ ] Node.js runtime.
+- [ ] Hono pour API HTTP.
+- [ ] Drizzle ORM + SQLite.
+- [ ] WebSocket ou SSE pour events/live output.
+- [ ] `croner` pour schedules MVP.
+- [ ] Worker in-process au MVP.
+- [ ] Verrous machine persistants.
+- [ ] JSON canonique versionné.
+- [ ] Stockage fichiers pour reports/logs/artifacts.
+- [ ] Docker Compose pour packaging final webserver.
+- [ ] Variables d'environnement pour configuration.
+- [ ] Secrets chiffrés avec master key hors image.
+
+## 12. URLs utiles
+
+- Hono Node.js : https://hono.dev/docs/getting-started/nodejs
+- Hono middleware : https://hono.dev/docs/guides/middleware
+- Drizzle SQLite : https://orm.drizzle.team/docs/get-started-sqlite
+- SQLite WAL : https://www.sqlite.org/wal.html
+- Croner : https://github.com/Hexagon/croner
+- MDN WebSocket : https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
+- MDN Server-sent events : https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
+- Docker Compose : https://docs.docker.com/compose/
+- Compose file reference : https://docs.docker.com/reference/compose-file/
+- Docker volumes : https://docs.docker.com/engine/storage/volumes/
+- Node.js Docker images : https://hub.docker.com/_/node
+
+## 13. Liens parent/enfant avec les autres tâches
+
+- Parents :
+ - `tache1.9.md` pour schéma BDD.
+ - `tache2.md` pour actions/templates/JSON.
+ - `tache4.md` pour scripts post-install.
+- Enfants :
+ - `tache3.md` consomme les API.
+ - `tache6.md` consomme API/MCP.
+ - `tache7.md` ajoute métriques/nettoyage/sécurité.
+ - `tache8.md` consomme API/capabilities.
+- Validation : `validation_tache5.md`.
diff --git a/tache6.md b/tache6.md
new file mode 100644
index 0000000..fb4334b
--- /dev/null
+++ b/tache6.md
@@ -0,0 +1,526 @@
+# Consigne de dev — Hermes, volet gauche, MCP, skills et messagerie
+
+> **Type** : mission d'**investigation + design intégration agent** (PAS d'implémentation).
+> **Langue** : français.
+> **Livrable final attendu** : spec d'intégration Hermes prête à passer en plan d'implémentation.
+
+---
+
+## 0. Contexte
+
+L'utilisateur dispose d'un agent Hermes sur `10.0.0.80`.
+
+Deux usages sont visés :
+
+1. **Depuis la webapp** : le volet gauche permet de discuter avec Hermes, demander une analyse, un rapport, un plan, ou une action proposée.
+2. **Depuis Hermes TUI ou messagerie** : l'utilisateur parle à Hermes ailleurs, puis Hermes utilise un skill et/ou MCP pour analyser une ou plusieurs machines, générer un rapport global, demander/lancer des upgrades validés, analyser les retours et erreurs.
+
+Docs Hermes consultées :
+
+- Documentation générale : https://hermes-agent.nousresearch.com/docs/
+- API Server : https://hermes-agent.nousresearch.com/docs/user-guide/features/api-server/
+- MCP : https://hermes-agent.nousresearch.com/docs/user-guide/features/mcp/
+- Use MCP with Hermes : https://hermes-agent.nousresearch.com/docs/guides/use-mcp-with-hermes
+- Skills System : https://hermes-agent.nousresearch.com/docs/user-guide/features/skills
+- Cron : https://hermes-agent.nousresearch.com/docs/user-guide/features/cron
+- Messaging Gateway : https://hermes-agent.nousresearch.com/docs/user-guide/messaging
+- Webhooks : https://hermes-agent.nousresearch.com/docs/user-guide/messaging/webhooks/
+
+---
+
+## 1. Outils Hermes pertinents à déployer
+
+D'après la documentation Hermes, les briques pertinentes sont :
+
+### API Server Hermes
+
+Hermes expose un serveur HTTP compatible OpenAI. La doc indique :
+
+- activer `API_SERVER_ENABLED=true` ;
+- définir `API_SERVER_KEY` ;
+- lancer `hermes gateway` ;
+- endpoint typique `http://host:8642/v1`.
+
+Usage dans ce projet :
+
+- le volet gauche de la webapp parle au backend `system_update` ;
+- le backend relaie vers Hermes API Server sur `10.0.0.80:8642` ;
+- l'API key Hermes reste côté backend, jamais dans le navigateur ;
+- streaming via `/v1/runs` ou Chat Completions selon support détecté.
+
+### MCP HTTP côté system_update
+
+Hermes sait se connecter à des serveurs MCP HTTP (`url`, `headers`) et filtrer les tools exposés.
+
+Usage dans ce projet :
+
+- `system_update` expose un serveur MCP HTTP interne ;
+- Hermes sur `10.0.0.80` déclare ce MCP dans `~/.hermes/config.yaml` ;
+- seuls les tools sûrs/minimaux sont exposés ;
+- les actions dangereuses créent une demande de validation webapp au lieu de s'exécuter directement.
+
+### Skills Hermes
+
+Hermes charge des skills depuis `~/.hermes/skills/`, avec `SKILL.md`, références, scripts et slash commands.
+
+Usage dans ce projet :
+
+- créer un skill `system-update-ops` ;
+- il apprend à lire les snapshots JSON ;
+- il produit rapports globaux ;
+- il sait demander des refresh ;
+- il sait créer une proposition d'upgrade ;
+- il sait analyser erreurs APT/Docker/post-install ;
+- il ne manipule jamais les secrets.
+
+### Messaging Gateway
+
+Hermes peut être utilisé depuis Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Email, SMS, Home Assistant, Mattermost, Teams, etc.
+
+Usage dans ce projet :
+
+- permettre à l'utilisateur de demander depuis un messager :
+ - "analyse toutes les machines" ;
+ - "fais un rapport global" ;
+ - "prépare les upgrades" ;
+ - "analyse l'échec sur vm_mqtt".
+
+### Webhooks Hermes
+
+Hermes expose un adaptateur webhook avec HMAC, routes et livraison. Il peut fonctionner en mode agent ou `deliver_only`.
+
+Usage dans ce projet :
+
+- envoyer à Hermes une notification quand un job backend termine ;
+- notifier un canal messagerie après schedule automatique ;
+- transmettre un résumé JSON réduit ;
+- utiliser `deliver_only` pour notifications simples sans LLM.
+
+### Cron Hermes
+
+Hermes peut planifier des tâches avec `/cron` ou `hermes cron`, charger des skills et livrer des résultats.
+
+Usage dans ce projet :
+
+- optionnel : planifier côté Hermes des audits périodiques de haut niveau ;
+- recommandé : garder les refresh machine opérationnels côté backend `system_update` (tâche 5), et utiliser Hermes cron pour rapports/analyses/résumés, pas pour exécuter SSH.
+
+---
+
+## 2. Architecture cible
+
+```text
+┌──────────────────────────────┐
+│ Webapp system_update │
+│ ┌──────────┬───────────────┐ │
+│ │ Hermes │ Dashboard │ │
+│ │ chat │ tuiles │ │
+│ └──────────┴───────────────┘ │
+└───────────────┬──────────────┘
+ │ HTTP/SSE
+┌───────────────▼──────────────┐
+│ Backend system_update │
+│ - API machines/actions │
+│ - stockage JSON │
+│ - MCP HTTP server │
+│ - proxy Hermes API │
+└───────┬─────────────────┬────┘
+ │ │
+ │ MCP HTTP │ OpenAI-compatible API
+ │ │
+┌───────▼─────────────────▼────┐
+│ Hermes Agent 10.0.0.80 │
+│ - API Server : :8642 │
+│ - Gateway messaging │
+│ - Skills system-update-ops │
+│ - MCP client system_update │
+└───────────────────────────────┘
+```
+
+Règle forte :
+
+> Hermes ne fait jamais de SSH directement vers les machines. Il passe par les tools MCP/API de `system_update`.
+
+---
+
+## 3. Cas 1 — Volet gauche Hermes dans la webapp
+
+Flux recommandé :
+
+1. Utilisateur écrit dans le volet gauche.
+2. Frontend envoie le message au backend `system_update`.
+3. Backend ajoute un contexte système court :
+ - rôle Hermes ;
+ - interdiction secrets ;
+ - appeler MCP/API pour données machine ;
+ - actions dangereuses = validation webapp.
+4. Backend appelle Hermes API Server sur `10.0.0.80:8642`.
+5. Réponse streamée vers le volet gauche.
+6. Si Hermes propose une action, la webapp l'affiche comme carte de validation, pas comme exécution directe.
+
+Exigences UX pour la discussion :
+
+- séparation claire entre messages utilisateur, messages Hermes et messages système ;
+- copier-coller natif du texte ;
+- bouton copier sur chaque message long ;
+- blocs de commande monospace avec bouton copier ;
+- références cliquables vers machine, snapshot, exécution, rapport ou extrait de log ;
+- action proposée rendue dans une carte validable séparée du texte ;
+- aucun secret dans l'historique envoyé à Hermes ou affiché au navigateur.
+
+Endpoints frontend/backend à concevoir :
+
+```text
+POST /api/hermes/chat
+GET /api/hermes/runs/:runId/events
+POST /api/hermes/runs/:runId/stop
+GET /api/hermes/health
+GET /api/hermes/capabilities
+```
+
+Le backend doit vérifier `/v1/health` et `/v1/capabilities` Hermes.
+
+Configuration :
+
+```text
+HERMES_BASE_URL=http://10.0.0.80:8642/v1
+HERMES_API_KEY=...
+HERMES_SESSION_KEY=system-update-webapp
+```
+
+L'API key n'est jamais exposée au navigateur.
+
+---
+
+## 4. Cas 2 — Hermes TUI ou messagerie contrôle system_update
+
+Depuis Hermes TUI/messagerie, l'utilisateur peut demander :
+
+- analyser une machine ;
+- analyser toutes les machines ;
+- faire un rapport global ;
+- comparer les updates ;
+- proposer un ordre de mise à jour ;
+- demander un upgrade ;
+- analyser le résultat ;
+- expliquer une erreur.
+
+Hermes utilise alors :
+
+- skill `system-update-ops` ;
+- MCP HTTP `system_update`.
+
+Exemple `~/.hermes/config.yaml` :
+
+```yaml
+mcp_servers:
+ system_update:
+ url: "http://10.0.0.X:8787/mcp"
+ headers:
+ Authorization: "Bearer ${SYSTEM_UPDATE_MCP_TOKEN}"
+ tools:
+ include:
+ - list_machines
+ - get_machine_state
+ - get_machine_hardware
+ - get_machine_metrics
+ - get_machine_snapshot
+ - get_all_snapshots
+ - get_machine_execution
+ - get_machine_messages
+ - search_reports
+ - get_report
+ - search_log_artifacts
+ - run_refresh
+ - request_action
+ - get_pending_actions
+ - preview_template
+ - list_templates
+ resources: false
+ prompts: false
+```
+
+Le tool `request_action` ne lance pas directement les actions dangereuses. Il crée une demande :
+
+```json
+{
+ "requestId": "req_x",
+ "machineId": "vm_mqtt",
+ "action": "apt_upgrade",
+ "status": "pending_user_approval",
+ "summary": "4 paquets seront mis à jour",
+ "risk": "medium"
+}
+```
+
+La webapp affiche la demande et l'utilisateur valide.
+
+---
+
+## 5. Tools MCP system_update
+
+Surface minimale à concevoir :
+
+```text
+list_machines
+get_machine_state
+get_machine_hardware
+get_machine_metrics
+get_machine_snapshot
+get_all_snapshots
+get_machine_execution
+get_machine_messages
+search_reports
+get_report
+search_log_artifacts
+run_refresh
+request_action
+get_pending_actions
+preview_template
+list_templates
+```
+
+À éviter au MVP :
+
+- accès log brut complet non filtré ;
+- accès secrets ;
+- actions SSH directes depuis Hermes ;
+- suppression destructive ;
+- modification template sans validation.
+
+Accès logs/rapports autorisé :
+
+- `search_reports` : recherche dans titres/résumés/rapports Markdown ;
+- `get_report` : lecture d'un rapport archivé, sans secret ;
+- `get_machine_messages` : warnings/erreurs/dépréciations extraits des logs ;
+- `search_log_artifacts` : recherche ciblée dans logs bruts avec limites, masquage et pagination ;
+- `get_log_excerpt` (futur, hors liste MVP) : extrait par `artifactId` et plage de lignes, jamais log complet par défaut.
+
+Hermes doit travailler d'abord sur :
+
+1. `machine_state` ;
+2. profil OS/type machine, hardware et métriques simples ;
+3. snapshots JSON réduits ;
+4. messages importants ;
+5. rapports Markdown ;
+6. extraits de logs ciblés seulement si nécessaire.
+
+### Terminal SSH interactif
+
+La webapp peut prévoir un vrai terminal SSH interactif dans le volet droit, mais ce n'est pas un tool Hermes.
+
+Règles :
+
+- l'utilisateur ouvre lui-même une session SSH interactive ;
+- Hermes peut suggérer une commande copiable ;
+- Hermes ne peut pas injecter une commande dans le terminal interactif ;
+- une commande copiée/collée reste une action utilisateur ;
+- ouverture/fermeture de session auditée ;
+- désactivation globale possible ;
+- session non persistée dans Hermes ;
+- logs de session conservés uniquement selon paramètre explicite, avec masquage secrets.
+
+Actions dangereuses :
+
+- `apt_full_upgrade`
+- `apt_dist_upgrade`
+- `docker_prune`
+- `docker_down`
+- `post_install_identity_network`
+- `reboot_verified`
+
+Ces actions doivent passer par validation UI.
+
+---
+
+## 6. Skill Hermes `system-update-ops`
+
+Créer une spec de skill :
+
+```text
+~/.hermes/skills/devops/system-update-ops/
+├─ SKILL.md
+├─ references/
+│ ├─ json-contracts.md
+│ ├─ apt-error-taxonomy.md
+│ ├─ important-messages-taxonomy.md
+│ ├─ docker-compose-workflow.md
+│ ├─ log-access-policy.md
+│ └─ safety-rules.md
+└─ templates/
+ ├─ global-report.md
+ ├─ machine-report.md
+ └─ upgrade-plan.md
+```
+
+Déclencheurs :
+
+- "analyse mes machines"
+- "rapport global system_update"
+- "pourquoi l'upgrade a échoué"
+- "prépare les mises à jour"
+- "propose un ordre d'upgrade"
+
+Procédure du skill :
+
+1. Lister machines via MCP.
+2. Récupérer snapshots récents.
+3. Si snapshots trop anciens, demander `run_refresh`.
+4. Dédupliquer updates APT/Docker.
+5. Récupérer les messages importants non acquittés.
+6. Classer risques, warnings futurs et notices sécurité.
+7. Produire rapport.
+8. Si action demandée, créer demande de validation via `request_action`.
+9. Après exécution, analyser `ExecutionResult`, messages importants et extraits de logs utiles.
+
+---
+
+## 7. Rapports Hermes
+
+Rapport global attendu :
+
+```text
+# Rapport global system_update
+
+## Résumé
+- Machines OK
+- Machines avec updates
+- Machines en erreur
+- Reboots requis
+
+## APT
+- Paquets par machine
+- Doublons
+- Risques
+
+## Docker
+- Stacks avec updates
+- Pull errors
+- Prune possible
+
+## Post-install
+- Profils manquants
+- Actions recommandées
+
+## Plan proposé
+1. Machine A
+2. Machine B
+
+## Actions en attente de validation
+```
+
+Le rapport doit être archivable côté backend et consultable depuis la webapp.
+
+---
+
+## 8. Notifications et webhooks
+
+Le backend `system_update` peut appeler Hermes Webhooks :
+
+- job automatique terminé ;
+- updates détectées ;
+- erreur critique ;
+- machine non revenue après reboot ;
+- rapport global prêt.
+
+Webhook Hermes recommandé :
+
+- HMAC obligatoire ;
+- payload JSON réduit ;
+- mode `deliver_only` pour notifications simples ;
+- mode agent avec skill `system-update-ops` pour synthèse.
+
+Exemple payload :
+
+```json
+{
+ "event": "updates_available",
+ "machineCount": 3,
+ "aptUpdates": 12,
+ "dockerUpdates": 2,
+ "reportRef": "reports/global/..."
+}
+```
+
+---
+
+## 9. Sécurité
+
+Règles non négociables :
+
+- Hermes ne reçoit jamais de mot de passe SSH/sudo/token registry ;
+- Hermes ne fait jamais SSH directement ;
+- Hermes ne reçoit pas les logs bruts complets par défaut ;
+- MCP expose seulement les tools whitelistés ;
+- actions dangereuses = validation UI ;
+- API Hermes appelée par backend seulement ;
+- token MCP avec rotation possible ;
+- audit des demandes Hermes ;
+- prompt injection : les données machine/logs sont traitées comme non fiables.
+
+---
+
+## 10. Livrables attendus
+
+À produire sous `docs/` :
+
+1. Architecture Hermes/webapp/MCP.
+2. Spécification du proxy backend vers Hermes API Server.
+3. Spécification MCP `system_update`.
+4. Spec du skill `system-update-ops`.
+5. Spec des rapports globaux.
+6. Spec notifications/webhooks Hermes.
+7. Sécurité et validations.
+8. Plan de déploiement Hermes sur `10.0.0.80`.
+9. Découpage en sous-jalons.
+
+---
+
+## 11. Définition de terminé
+
+- Les deux cas d'usage sont couverts : volet gauche et TUI/messagerie.
+- Les outils Hermes à déployer sont listés.
+- Le MCP minimal est défini.
+- Le skill Hermes est spécifié.
+- Les actions dangereuses ne peuvent pas être exécutées sans validation webapp.
+- Les secrets restent côté backend.
+- Aucun code de production n'est livré pendant cette mission de design.
+
+---
+
+## 12. Technos à utiliser — checklist
+
+- [ ] Hermes API Server côté agent.
+- [ ] Backend proxy vers Hermes, clé jamais dans navigateur.
+- [ ] MCP HTTP exposé par `system_update`.
+- [ ] Skill Hermes `system-update-ops`.
+- [ ] Webhooks Hermes pour notifications.
+- [ ] Messaging Gateway si canaux externes utilisés.
+- [ ] JSON réduit, références rehydratables.
+- [ ] Audit MCP/API.
+- [ ] Validation UI obligatoire pour actions dangereuses.
+- [ ] Aucun SSH direct depuis Hermes.
+
+## 13. URLs utiles
+
+- Hermes docs : https://hermes-agent.nousresearch.com/docs/
+- Hermes API Server : https://hermes-agent.nousresearch.com/docs/user-guide/features/api-server/
+- Hermes MCP : https://hermes-agent.nousresearch.com/docs/user-guide/features/mcp/
+- Use MCP with Hermes : https://hermes-agent.nousresearch.com/docs/guides/use-mcp-with-hermes
+- Hermes Skills : https://hermes-agent.nousresearch.com/docs/user-guide/features/skills
+- Hermes Messaging : https://hermes-agent.nousresearch.com/docs/user-guide/messaging
+- Hermes Webhooks : https://hermes-agent.nousresearch.com/docs/user-guide/messaging/webhooks/
+- Model Context Protocol : https://modelcontextprotocol.io/
+- JSON Schema : https://json-schema.org/
+
+## 14. Liens parent/enfant avec les autres tâches
+
+- Parents :
+ - `tache5.md` pour API, stockage et action requests.
+ - `tache1.9.md` pour tables Hermes/MCP/audit.
+ - `tache2.md` pour contrats JSON.
+ - `tache7.md` pour réduction tokens.
+- Enfants :
+ - `tache3.md` intègre le volet Hermes web.
+ - `tache8.md` peut exposer une vue Hermes native via backend.
+- Validation : `validation_tache6.md`.
diff --git a/tache7.md b/tache7.md
new file mode 100644
index 0000000..fd2bb30
--- /dev/null
+++ b/tache7.md
@@ -0,0 +1,623 @@
+# Consigne de dev — Optimisation, observabilité, tokens Hermes, nettoyage DB et découverte machines
+
+> **Type** : mission d'**investigation + design optimisation** (PAS d'implémentation).
+> **Langue** : français.
+> **Livrable final attendu** : spec prête à passer en plan d'implémentation.
+
+---
+
+## 0. Contexte
+
+Cette tâche regroupe les optimisations transverses de `system_update` :
+
+- suivi RAM/CPU/base de données dans le footer ;
+- optimisation des tokens échangés avec Hermes sans perte de donnée ;
+- auto-nettoyage de la base et des logs anciens ;
+- découverte de machines disponibles lors de l'ajout, notamment scan du port SSH `22` ;
+- sécurisation des mots de passe, secrets et entrées sensibles.
+
+À lire avant de travailler :
+
+- `CLAUDE.md`
+- `tache5.md` (backend, historique JSON, automatisations)
+- `tache6.md` (Hermes, MCP, skills)
+- `shared/types.ts`
+- `server/db/schema.ts`
+- `server/index.ts`
+- `server/jobs/worker.ts`
+- `client/src/styles/app.css`
+- `design_system/consigne_design_system.md`
+
+Sources web utiles :
+
+- OpenAI prompt caching : https://platform.openai.com/docs/guides/prompt-caching
+- OpenAI latency optimization : https://platform.openai.com/docs/guides/latency-optimization
+- SQLite WAL : https://www.sqlite.org/wal.html
+- SQLite PRAGMA / optimize / wal_checkpoint : https://www.sqlite.org/pragma.html
+- SQLite VACUUM : https://www.sqlite.org/lang_vacuum.html
+- Nmap Reference Guide : https://nmap.org/book/man.html
+- Nmap port scanning options : https://nmap.org/book/port-scanning-options.html
+- MCP sampling / context : https://modelcontextprotocol.io/specification/draft/client/sampling
+
+---
+
+## 1. Objectif
+
+Concevoir une couche d'optimisation et d'observabilité qui permette :
+
+- de voir l'état technique de la webapp dans le footer ;
+- de remonter des métriques simples par machine pour les tuiles et Hermes ;
+- de limiter les tokens envoyés à Hermes tout en conservant toutes les données brutes côté webapp ;
+- de réduire la taille de la base et des logs anciens avec une politique contrôlée ;
+- de découvrir automatiquement des machines candidates sur le réseau local lors de l'ajout ;
+- de renforcer la gestion des mots de passe et secrets côté UI/backend.
+
+---
+
+## 2. Footer observabilité
+
+Le footer doit afficher des métriques compactes et utiles :
+
+```text
+SYSTEM UPDATE · 4 machines · APT 12 · Docker 2 · CPU 8% · RAM 214 MB · DB 42 MB · WAL 3 MB · Jobs 1 · 06:42
+```
+
+Métriques minimales :
+
+- nombre de machines ;
+- updates APT totales ;
+- updates Docker totales ;
+- jobs en cours ;
+- CPU process/backend ;
+- RAM process/backend ;
+- taille DB SQLite ;
+- taille WAL/SHM si SQLite WAL ;
+- dernier nettoyage ;
+- état Hermes : connected/disconnected ;
+- état scheduler : running/paused.
+
+Ces métriques concernent le backend `system_update`, pas le monitoring détaillé des hôtes distants.
+
+### Métriques simples par machine
+
+Prévoir en complément un snapshot `machine_metrics_simple` par machine :
+
+- CPU : load average + nombre de coeurs ;
+- RAM : total/utilisé/disponible ;
+- disque : `df -h` lisible et valeurs en bytes ;
+- inodes ;
+- uptime ;
+- température optionnelle si disponible.
+
+Usage :
+
+- badge/alerte dans la tuile machine ;
+- rapport global Hermes ;
+- détection simple disque plein avant upgrade ;
+- pré-check avant Docker prune ou full-upgrade.
+
+Ce n'est pas un remplaçant à Prometheus/Netdata : seulement une mesure légère, agentless, via SSH.
+
+Le design doit respecter le design system :
+
+- police `Share Tech Mono` ;
+- cellules compactes ;
+- icônes via ui-kit si besoin ;
+- pas de couleurs hors tokens ;
+- tooltips sur métriques peu évidentes.
+
+API à concevoir :
+
+```text
+GET /api/system/status
+GET /api/system/metrics
+GET /api/system/storage
+```
+
+Exemple JSON :
+
+```json
+{
+ "server": {
+ "status": "ok",
+ "uptimeSeconds": 4200,
+ "cpuPercent": 8.2,
+ "memoryRssBytes": 224395264,
+ "memoryHeapUsedBytes": 58200000
+ },
+ "database": {
+ "path": "./data/system-update.db",
+ "sizeBytes": 44040192,
+ "walBytes": 3145728,
+ "shmBytes": 32768,
+ "lastOptimizeAt": "2026-06-05T06:00:00Z"
+ },
+ "jobs": {
+ "running": 1,
+ "queued": 0,
+ "lastScheduleAt": "2026-06-05T06:00:00Z"
+ },
+ "hermes": {
+ "status": "connected",
+ "lastHealthAt": "2026-06-05T06:41:00Z"
+ }
+}
+```
+
+---
+
+## 3. Sécurisation mots de passe et secrets
+
+Objectif :
+
+- éviter toute fuite de mot de passe SSH/sudo/token ;
+- améliorer l'UX de saisie sans diminuer la sécurité ;
+- préparer la transition vers clés SSH et politiques de rotation.
+
+À concevoir :
+
+- stockage chiffré au repos déjà existant à conserver et renforcer ;
+- séparation mot de passe SSH / mot de passe sudo ;
+- option "sudo identique au mot de passe SSH" côté formulaire, sans duplication visible ;
+- bouton afficher/masquer local dans le champ, jamais de log ;
+- validation minimale : champ vide, port, utilisateur ;
+- masquage systématique dans logs, terminal, rapports, erreurs API, Hermes/MCP ;
+- rotation des credentials par machine ;
+- test de connexion après modification ;
+- audit d'accès aux secrets ;
+- jamais de secret dans `UpdateSnapshot`, `ExecutionResult`, rapports Markdown, prompts Hermes, MCP tools ;
+- interdire stockage navigateur/localStorage/sessionStorage ;
+- préparer support clés SSH :
+ - clé privée chiffrée ;
+ - passphrase optionnelle ;
+ - fingerprint ;
+ - validation host key ;
+ - ProxyJump futur ;
+- préparer auth webapp future :
+ - réseau de confiance au MVP ;
+ - reverse proxy/TLS/VPN ;
+ - option comptes utilisateurs/MFA à documenter.
+
+Exemple JSON public machine, sans secret :
+
+```json
+{
+ "machineId": "vm_mqtt",
+ "auth": {
+ "method": "password",
+ "username": "gilles",
+ "hasSudoPassword": true,
+ "lastCredentialTestAt": "ISO",
+ "credentialStatus": "ok"
+ }
+}
+```
+
+Erreurs à prévoir :
+
+- `credential_missing`
+- `credential_decrypt_failed`
+- `ssh_auth_failed`
+- `sudo_auth_failed`
+- `host_key_changed`
+- `secret_redaction_failed`
+
+Le design doit préciser comment masquer les motifs sensibles dans :
+
+- stdout/stderr ;
+- exceptions backend ;
+- terminal live ;
+- rapports ;
+- notifications ;
+- payloads Hermes.
+
+---
+
+## 4. Optimisation tokens Hermes sans perte de données
+
+Principe :
+
+> Ne jamais perdre la donnée. Archiver le brut, stocker le JSON complet, envoyer à Hermes un résumé déterministe compact avec références permettant de recharger le détail via MCP.
+
+La webapp doit conserver :
+
+- log brut complet ;
+- JSON complet ;
+- rapport Markdown ;
+- historique ;
+- diff réel ;
+- erreurs structurées.
+
+Hermes reçoit par défaut :
+
+- snapshot réduit ;
+- lignes importantes ;
+- compteurs ;
+- risques ;
+- références (`snapshotId`, `executionId`, `reportRef`) ;
+- liens MCP pour charger le détail à la demande.
+
+### Méthodes de réduction
+
+Le design doit prévoir :
+
+- reducers déterministes par domaine :
+ - APT ;
+ - Docker ;
+ - post-install ;
+ - reboot ;
+ - erreurs ;
+- déduplication :
+ - APT : `os_family + package + from + to + origin` ;
+ - Docker : `image + fromDigest + toDigest` ;
+ - post-install : `profileId + version + status` ;
+- pagination MCP ;
+- champs courts mais explicites ;
+- tri stable ;
+- IDs/références au lieu de répéter les blocs ;
+- envoi du diff plutôt que snapshot complet quand Hermes connaît déjà l'état précédent ;
+- résumés hiérarchiques :
+ - global ;
+ - machine ;
+ - action ;
+ - erreur ;
+- outils MCP `get_detail` pour recharger ce qui manque.
+
+### Prompt caching
+
+Les docs OpenAI recommandent de mettre le contenu statique au début des prompts pour bénéficier du cache de préfixe.
+
+Design attendu :
+
+- préfixe stable Hermes :
+ - rôle ;
+ - règles sécurité ;
+ - contrat JSON ;
+ - mode opératoire ;
+- données variables à la fin ;
+- ne pas injecter les logs bruts dans le préfixe ;
+- mesurer `cached_tokens` quand le fournisseur le renvoie ;
+- garder les tool schemas stables.
+
+### Exemple payload réduit Hermes
+
+```json
+{
+ "kind": "global_update_summary",
+ "generatedAt": "ISO",
+ "machines": [
+ {
+ "id": "vm_mqtt",
+ "name": "vm_mqtt",
+ "status": "updates_available",
+ "apt": { "count": 4, "rebootRequired": false },
+ "docker": { "updates": 1, "pruneBytes": 734003200 },
+ "refs": {
+ "snapshotId": "snap_123",
+ "latestExecutionId": "exec_456"
+ }
+ }
+ ],
+ "dedup": {
+ "aptGroups": 3,
+ "dockerGroups": 1
+ }
+}
+```
+
+Hermes peut ensuite appeler :
+
+```text
+get_machine_snapshot(machineId, snapshotId)
+get_machine_execution(machineId, executionId)
+get_report(reportRef)
+```
+
+---
+
+## 5. Mesure et budget tokens
+
+Prévoir une table ou collection `hermes_usage` :
+
+- session ;
+- runId ;
+- promptTokens ;
+- cachedTokens ;
+- completionTokens ;
+- totalTokens ;
+- payloadKind ;
+- payloadBytes ;
+- reductionRatio ;
+- model/provider si disponible.
+
+Exemple :
+
+```json
+{
+ "runId": "hermes_run_1",
+ "payloadKind": "global_update_summary",
+ "rawBytes": 940000,
+ "reducedBytes": 18000,
+ "promptTokens": 5200,
+ "cachedTokens": 2100,
+ "completionTokens": 900,
+ "reductionRatio": 0.019
+}
+```
+
+Objectifs :
+
+- savoir combien coûte un rapport global ;
+- détecter les payloads trop gros ;
+- ajuster les reducers ;
+- éviter une dérive silencieuse.
+
+---
+
+## 6. Auto-nettoyage base de données et logs
+
+Objectif :
+
+- conserver l'historique utile ;
+- éviter une base ou un dossier `reports/` qui grossit indéfiniment ;
+- préserver les rapports importants ;
+- permettre export avant purge.
+
+### Politique de rétention
+
+Paramètres à concevoir :
+
+```json
+{
+ "retention": {
+ "rawLogsDays": 30,
+ "snapshotsDays": 90,
+ "executionsDays": 180,
+ "reportsDays": 365,
+ "importantMessagesDays": 730,
+ "keepFailedExecutionsDays": 365,
+ "keepUnacknowledgedWarningsForever": true,
+ "keepPinnedReportsForever": true
+ }
+}
+```
+
+Règles :
+
+- ne jamais supprimer un rapport épinglé ;
+- garder plus longtemps les erreurs ;
+- garder plus longtemps les messages importants extraits des logs ;
+- ne pas supprimer automatiquement les warnings non acquittés sur évolution majeure, sécurité, dépôt obsolète ou dépréciation ;
+- conserver les derniers N snapshots par machine même si anciens ;
+- purge en tâche planifiée ;
+- rapport de purge en JSON ;
+- mode dry-run obligatoire avant suppression manuelle.
+
+Les logs bruts peuvent être supprimés avant les messages importants. Les messages extraits doivent garder :
+
+- source (`apt`, `docker`, `post_install`, `ssh`) ;
+- catégorie ;
+- sévérité ;
+- résumé sans secret ;
+- référence vers le log si encore présent ;
+- statut acquitté/non acquitté ;
+- date de première et dernière observation.
+
+### SQLite maintenance
+
+SQLite WAL est déjà utilisé dans le projet.
+
+À spécifier :
+
+- `PRAGMA optimize` périodique ;
+- `PRAGMA wal_checkpoint(TRUNCATE)` après purge importante ;
+- `VACUUM` seulement en maintenance contrôlée, car il peut être coûteux et nécessite de l'espace temporaire ;
+- suivi `dbBytes`, `walBytes`, `freelistCount` ;
+- politique de sauvegarde avant purge majeure.
+
+Exemple tâche :
+
+```json
+{
+ "action": "database_cleanup",
+ "mode": "dry_run",
+ "deleteCandidates": {
+ "rawLogs": 120,
+ "snapshots": 450,
+ "executions": 12
+ },
+ "estimatedBytesReclaimable": 734003200
+}
+```
+
+---
+
+## 7. Découverte de machines lors de l'ajout
+
+Ajouter dans l'UI "Ajouter une machine" un bouton :
+
+```text
+[scanner le réseau]
+```
+
+But :
+
+- trouver des hôtes avec port SSH ouvert ;
+- afficher une liste de candidats ;
+- préremplir hostname/IP/port ;
+- permettre test de connexion.
+
+### Méthodes de découverte
+
+MVP recommandé :
+
+- scan d'un CIDR configuré : `10.0.0.0/22` ;
+- détecter port `22/tcp` ouvert ;
+- récupérer host key via `ssh-keyscan` ou équivalent ;
+- optionnel : reverse DNS / mDNS ;
+- affichage candidates dans la webapp.
+
+Avec Nmap :
+
+```text
+nmap -p 22 --open 10.0.0.0/22
+```
+
+`nmap` doit être considéré ici comme outil d'administration réseau contrôlé, pas comme outil offensif. Le scan est limité aux CIDR autorisés et à des ports explicitement configurés.
+
+Pour une intégration backend :
+
+- utiliser sortie XML `-oX -` et parser proprement ;
+- ne pas parser du texte humain si une sortie structurée est disponible ;
+- limiter fréquence et concurrence ;
+- timeout ;
+- allowlist réseau ;
+- confirmation utilisateur.
+
+Alternative sans nmap :
+
+- scanner TCP simple côté Node ;
+- plus portable mais moins riche ;
+- suffisant pour port 22 ouvert ;
+- pas de fingerprint OS/service.
+
+Le design doit comparer MVP `nmap` vs TCP scanner maison.
+
+### JSON candidats
+
+```json
+{
+ "scanId": "scan_1",
+ "cidr": "10.0.0.0/22",
+ "startedAt": "ISO",
+ "finishedAt": "ISO",
+ "status": "ok",
+ "candidates": [
+ {
+ "host": "10.0.0.3",
+ "port": 22,
+ "service": "ssh",
+ "hostKeyFingerprint": "SHA256:...",
+ "reverseDns": "vm_mqtt.home",
+ "alreadyKnown": false
+ }
+ ]
+}
+```
+
+### Sécurité découverte réseau
+
+- scan uniquement sur plages autorisées en paramètres ;
+- désactivé par défaut hors réseau local ;
+- journaliser scans ;
+- pas de scan agressif ;
+- pas de détection intrusive ;
+- bouton manuel ou schedule explicitement activé.
+
+---
+
+## 8. Paramètres optimisation
+
+Créer une section paramètres :
+
+```text
+Paramètres
+├─ Observabilité
+│ ├─ afficher CPU/RAM/DB dans footer
+│ ├─ intervalle refresh métriques
+│ └─ seuils warning
+├─ Hermes / tokens
+│ ├─ mode payload compact
+│ ├─ limite tokens par rapport
+│ ├─ reducers actifs
+│ └─ mesurer usage tokens
+├─ Nettoyage
+│ ├─ rétention logs
+│ ├─ rétention snapshots
+│ ├─ rétention warnings importants
+│ ├─ conserver warnings non acquittés
+│ ├─ purge auto
+│ └─ dry-run
+├─ Secrets
+│ ├─ rotation credentials
+│ ├─ masquage logs
+│ ├─ méthode auth password/ssh-key
+│ └─ host key policy
+└─ Découverte réseau
+ ├─ CIDR autorisés
+ ├─ port SSH
+ ├─ méthode nmap/TCP
+ └─ timeout
+```
+
+---
+
+## 9. Livrables attendus
+
+À produire sous `docs/` :
+
+1. Design footer observabilité.
+2. Contrat `/api/system/metrics`.
+3. Stratégie réduction tokens Hermes sans perte de données.
+4. Contrat de mesure usage tokens.
+5. Politique de rétention DB/logs/rapports.
+6. Plan maintenance SQLite.
+7. Design découverte machines SSH.
+8. Comparatif nmap vs scanner TCP.
+9. Paramètres optimisation.
+10. Spec sécurisation mots de passe/secrets.
+11. Politique de conservation des messages importants non acquittés.
+12. Découpage en sous-jalons.
+
+---
+
+## 10. Définition de terminé
+
+- Le footer métriques est spécifié.
+- Les données brutes restent archivées.
+- Hermes reçoit un payload compact mais rehydratable.
+- La mesure tokens est prévue.
+- L'auto-nettoyage DB/logs est sûr et configurable.
+- Les messages importants restent analysables même après purge des logs bruts.
+- La découverte de machines SSH est spécifiée avec garde-fous.
+- Les mots de passe/secrets sont masqués, chiffrés, testables et jamais envoyés à Hermes/MCP.
+- Aucun code de production n'est livré pendant cette mission de design.
+
+---
+
+## 11. Technos à utiliser — checklist
+
+- [ ] SQLite maintenance : WAL, checkpoint, optimize, vacuum contrôlé.
+- [ ] Reducers déterministes avant Hermes.
+- [ ] Mesure usage tokens Hermes.
+- [ ] `nmap` optionnel pour découverte contrôlée.
+- [ ] Scanner TCP maison comme alternative MVP.
+- [ ] `ssh-keyscan` ou équivalent pour fingerprints.
+- [ ] Chiffrement secrets au repos.
+- [ ] Redaction logs/UI/Hermes/MCP.
+- [ ] Schedules de cleanup.
+- [ ] Metrics backend exposées via API.
+- [ ] Docker volumes compatibles purge/logs.
+
+## 12. URLs utiles
+
+- SQLite WAL : https://www.sqlite.org/wal.html
+- SQLite `PRAGMA optimize` : https://www.sqlite.org/pragma.html#pragma_optimize
+- SQLite VACUUM : https://www.sqlite.org/lang_vacuum.html
+- Nmap reference guide : https://nmap.org/book/man.html
+- OpenSSH `ssh-keyscan` : https://man.openbsd.org/ssh-keyscan
+- OWASP Secrets Management Cheat Sheet : https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
+- OWASP Logging Cheat Sheet : https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
+- OpenAI prompt caching : https://platform.openai.com/docs/guides/prompt-caching
+- Docker volumes : https://docs.docker.com/engine/storage/volumes/
+
+## 13. Liens parent/enfant avec les autres tâches
+
+- Parents :
+ - `tache5.md` pour API/jobs/rétention.
+ - `tache1.9.md` pour tables metrics/cleanup/discovery.
+ - `tache6.md` pour Hermes/tokens.
+- Enfants :
+ - `tache3.md` pour footer, smartphone et paramètres.
+ - `tache8.md` pour cache local, tokens client et metrics.
+- Validation : `validation_tache7.md`.
diff --git a/tache8.md b/tache8.md
new file mode 100644
index 0000000..27998e7
--- /dev/null
+++ b/tache8.md
@@ -0,0 +1,374 @@
+# Consigne de dev — App locale Rust/GNOME connectée au serveur
+
+> **Type** : mission de **développement progressif** après validation utilisateur du 2026-06-05.
+> **Langue** : français.
+> **Livrable final attendu** : app locale Rust/GNOME développée par jalons, en commençant par un client API minimal.
+
+---
+
+## 0. Contexte
+
+La webapp `system_update` pilote des machines Linux via un backend central :
+
+- SSH agentless ;
+- snapshots JSON ;
+- exécutions ;
+- logs/rapports ;
+- Hermes/MCP ;
+- terminal ;
+- paramètres ;
+- schedules.
+
+La tâche 8 vise une évolution : créer une **application locale native** en Rust, style GNOME/libadwaita, qui se connecte au serveur `system_update` via API et reprend une partie de l'UI du frontend, sans nécessiter de navigateur web.
+
+Validation utilisateur :
+
+- 2026-06-05 : le démarrage du développement est validé.
+- La première phase crée un scaffold Rust compilable et un client API minimal.
+- L'UI GTK4/libadwaita attend l'installation des bibliothèques système `gtk4` et `libadwaita-1`.
+
+---
+
+## 1. Objectif
+
+Concevoir une application locale :
+
+- développée en Rust ;
+- UI GTK4/libadwaita ou équivalent GNOME moderne ;
+- thème aligné avec le design system Gruvbox seventies ;
+- communication API avec le backend `system_update` ;
+- affichage des machines, tuiles, rapports, logs, paramètres ;
+- exécution des actions via le backend, jamais SSH direct par défaut ;
+- support optionnel Hermes via le backend ;
+- mode desktop local plus confortable qu'un navigateur pour usage quotidien.
+
+---
+
+## 2. Périmètre fonctionnel
+
+### MVP app locale
+
+- configuration URL serveur ;
+- authentification par token/API key ;
+- liste des machines ;
+- vue état courant machine ;
+- tuiles machine compactes ;
+- détail machine ;
+- lancement `apt_update_analyze` ;
+- affichage actions en cours ;
+- lecture rapports ;
+- lecture logs réduits ;
+- notifications desktop simples.
+
+### Fonctions avancées
+
+- terminal de logs live via WebSocket/SSE ;
+- vrai terminal SSH interactif via backend, si activé ;
+- vue Hermes native ;
+- paramètres frontend/app ;
+- scripts post-install ;
+- Docker Compose ;
+- mode offline partiel : cache lecture seule des derniers états ;
+- tray icon ;
+- notifications GNOME ;
+- import/export configuration serveur.
+
+---
+
+## 3. Architecture cible
+
+```text
+┌──────────────────────────────────────────────┐
+│ App locale Rust/GNOME │
+│ - UI libadwaita │
+│ - stockage config local │
+│ - client HTTP/WebSocket │
+│ - cache lecture seule │
+└───────────────────┬──────────────────────────┘
+ │ HTTPS / WS / SSE
+┌───────────────────▼──────────────────────────┐
+│ Backend system_update │
+│ - API REST stable │
+│ - événements live │
+│ - auth clients │
+│ - stockage JSON/logs/rapports │
+│ - orchestration SSH │
+│ - proxy Hermes/MCP │
+└───────────────────┬──────────────────────────┘
+ │ SSH
+┌───────────────────▼──────────────────────────┐
+│ Machines Linux │
+└──────────────────────────────────────────────┘
+```
+
+Règle :
+
+> L'app Rust ne fait pas de SSH direct au MVP. Elle demande au backend de lancer les actions et consomme les JSON, logs et événements.
+
+---
+
+## 4. Stack technique à étudier
+
+### Rust
+
+- `gtk4` ;
+- `libadwaita` ;
+- `glib`/`gio` ;
+- `reqwest` ou client HTTP async compatible ;
+- `tokio` si retenu ;
+- WebSocket via crate adaptée ;
+- `serde`/`serde_json` ;
+- `keyring` pour stocker le token localement ;
+- `directories` pour chemins de config/cache ;
+- `tracing` pour logs app.
+
+Alternative à comparer :
+
+- `relm4` pour architecture UI plus structurée ;
+- `iced` si GTK/libadwaita devient trop contraignant, mais moins GNOME natif.
+
+### Design system GNOME
+
+Réutiliser :
+
+- `design_system/tokens/tokens.gnome.css` ;
+- `design_system/tokens/tokens.json`.
+
+À concevoir :
+
+- mapping tokens CSS web → libadwaita ;
+- widgets équivalents aux tuiles ;
+- couleurs dark/light ;
+- icônes SVG compatibles GTK ;
+- densité proche du frontend.
+
+---
+
+## 5. API backend nécessaire
+
+L'API doit rester commune au frontend web, Hermes/MCP et app locale.
+
+Endpoints à utiliser :
+
+```text
+GET /api/system/status
+GET /api/system/metrics
+
+GET /api/machines
+GET /api/machines/:id/state
+GET /api/machines/:id/hardware
+GET /api/machines/:id/metrics
+GET /api/machines/:id/snapshots
+GET /api/machines/:id/executions
+GET /api/machines/:id/reports
+GET /api/machines/:id/messages
+
+POST /api/machines/:id/actions
+
+GET /api/reports
+GET /api/reports/:id
+GET /api/artifacts/:id/important-lines
+
+GET /api/settings
+PATCH /api/settings
+
+GET /api/events
+GET /api/ws/machines/:id/output
+```
+
+À ajouter ou clarifier côté backend :
+
+- version d'API ;
+- pagination ;
+- erreurs structurées ;
+- auth token dédiée client local ;
+- droits par action ;
+- révocation token ;
+- audit des clients ;
+- limitation de débit ;
+- endpoint capabilities.
+
+Exemple capabilities :
+
+```json
+{
+ "apiVersion": "1",
+ "features": {
+ "machines": true,
+ "actions": true,
+ "terminalOutput": true,
+ "interactiveSsh": false,
+ "hermes": true,
+ "settings": true
+ }
+}
+```
+
+---
+
+## 6. Sécurité
+
+Règles :
+
+- token stocké dans le trousseau local via `keyring`, pas en clair dans un fichier ;
+- HTTPS recommandé hors localhost ;
+- permissions minimales par token ;
+- actions dangereuses confirmées dans l'app ;
+- les secrets SSH/sudo restent uniquement côté backend ;
+- l'app locale ne reçoit jamais les credentials machines ;
+- logs locaux sans secret ;
+- blocage si certificat/host inconnu selon politique choisie.
+
+Profils de token :
+
+- lecture seule ;
+- opérateur : refresh + actions non destructives ;
+- admin : actions dangereuses + paramètres ;
+- debug : accès logs étendus.
+
+---
+
+## 7. UX native proposée
+
+### Layout desktop
+
+```text
+┌────────────────────────────────────────────────────────────────────┐
+│ HeaderBar System Update [search] [refresh] [settings] │
+├───────────────┬────────────────────────────────────────────────────┤
+│ Sidebar │ Machines │
+│ │ ┌────────────────────────────────────────────────┐ │
+│ Overview │ │ ● vm_mqtt debian · vm · APT 4 · Docker 1 │ │
+│ Machines │ │ CPU 0.08 · RAM 26% · / 29% │ │
+│ Reports │ │ [analyze] [upgrade] [reboot] [report] │ │
+│ Hermes │ └────────────────────────────────────────────────┘ │
+│ Settings │ │
+├───────────────┴────────────────────────────────────────────────────┤
+│ StatusBar connected · 4 machines · jobs 1 · hermes ok · 06:42 │
+└────────────────────────────────────────────────────────────────────┘
+```
+
+### Détail machine
+
+```text
+┌────────────────────────────────────────────────────────────────────┐
+│ vm_mqtt debian 13 · vm · qemu │
+├────────────────────────────────────────────────────────────────────┤
+│ Summary APT 4 · reboot no · Docker 1 · warnings 1 │
+│ Health CPU 0.08/4c · RAM 26% · / 29% │
+│ │
+│ Tabs: [System] [Docker] [Post-install] [Hardware] [Logs] [Reports] │
+│ │
+│ System │
+│ packages to update... │
+└────────────────────────────────────────────────────────────────────┘
+```
+
+### Paramètres
+
+```text
+┌────────────────────────────────────────────────────────────────────┐
+│ Settings │
+├───────────────┬────────────────────────────────────────────────────┤
+│ Server │ Server URL [ https://10.0.0.X:8787 ] │
+│ Appearance │ API token [ ************* ] [test] │
+│ Actions │ Theme [ Gruvbox dark ] │
+│ Notifications │ Cache [ enabled ] │
+│ Security │ Notifications [ enabled ] │
+└───────────────┴────────────────────────────────────────────────────┘
+```
+
+---
+
+## 8. Différences avec la webapp
+
+L'app locale ne doit pas être une copie exacte.
+
+À garder :
+
+- logique métier backend ;
+- contrats JSON ;
+- tuiles machine ;
+- design tokens ;
+- rapports/logs ;
+- validations actions.
+
+À adapter :
+
+- navigation GNOME avec HeaderBar/Sidebar ;
+- notifications desktop ;
+- stockage token via trousseau ;
+- raccourcis clavier natifs ;
+- possibilité de tray icon ;
+- fenêtres modales natives.
+
+---
+
+## 9. Livrables attendus
+
+À produire sous `docs/` :
+
+1. Architecture app locale ↔ backend.
+2. Choix stack Rust/GNOME argumenté.
+3. Contrat API nécessaire.
+4. Modèle de sécurité/token.
+5. Mapping design system web → GNOME.
+6. Maquettes ASCII vues principales.
+7. Plan de cache/offline lecture seule.
+8. Découpage en sous-jalons.
+
+---
+
+## 10. Définition de terminé
+
+- L'app locale peut être développée sans modifier la logique SSH.
+- Le backend expose les API nécessaires.
+- Les credentials machines restent côté backend.
+- L'UI native respecte le thème Gruvbox seventies.
+- Les actions dangereuses restent validées.
+- La webapp et l'app Rust partagent les mêmes contrats JSON.
+- Le scaffold Rust est compilable.
+- Le client API peut tester `/api/capabilities`.
+- L'UI GNOME est développée ensuite, sans déplacer la logique SSH hors du backend.
+
+---
+
+## 11. Technos à utiliser — checklist
+
+- [ ] Rust.
+- [ ] GTK4.
+- [ ] libadwaita.
+- [ ] `gtk4-rs` / bindings GNOME.
+- [ ] `serde` / `serde_json` pour contrats JSON.
+- [ ] Client HTTP Rust (`reqwest` ou équivalent).
+- [ ] WebSocket/SSE Rust selon choix backend.
+- [ ] `keyring` pour stockage token local.
+- [ ] `directories` pour config/cache.
+- [ ] `tracing` pour logs app.
+- [ ] Tokens `design_system/tokens/tokens.gnome.css`.
+- [ ] Backend `system_update` comme source de vérité.
+
+## 12. URLs utiles
+
+- Rust : https://www.rust-lang.org/
+- Rust Book : https://doc.rust-lang.org/book/
+- GTK Rust bindings : https://www.gtk.org/docs/language-bindings/rust/
+- gtk4-rs book : https://gtk-rs.org/gtk4-rs/stable/latest/book/
+- libadwaita Rust : https://gtk-rs.org/gtk4-rs/stable/latest/book/libadwaita.html
+- GNOME Human Interface Guidelines : https://developer.gnome.org/hig/
+- reqwest : https://docs.rs/reqwest/
+- serde : https://serde.rs/
+- keyring crate : https://docs.rs/keyring/
+- Flatpak docs : https://docs.flatpak.org/
+
+## 13. Liens parent/enfant avec les autres tâches
+
+- Parents :
+ - `tache5.md` pour API/capabilities/events.
+ - `tache1.9.md` pour `api_clients`.
+ - `tache3.md` pour reprise UX/tuiles.
+ - `tache7.md` pour sécurité token/cache/metrics.
+ - `design_system/tokens/tokens.gnome.css` pour thème natif.
+- Enfants :
+ - aucune tâche enfant immédiate ; c'est une évolution future après webapp serveur stable.
+- Validation : `validation_tache8.md`.
diff --git a/validation_tache1.9.md b/validation_tache1.9.md
new file mode 100644
index 0000000..9bec070
--- /dev/null
+++ b/validation_tache1.9.md
@@ -0,0 +1,103 @@
+# Protocole de validation — Tâche 1.9 (architecture BDD cible)
+
+> **Type** : grille de validation. Utilisée après la mission décrite dans `tache1.9.md`.
+> **But** : vérifier que le schéma de données cible est complet, cohérent avec les tâches 2 à 8, compatible SQLite MVP, et préparé pour les évolutions futures.
+> **Gate obligatoire** : cette validation doit être passée avant toute migration BDD majeure.
+> **Rappel** : la tâche 1.9 est une mission de design architecture BDD, pas d'implémentation.
+
+---
+
+## 0. Quand lancer cette validation
+
+- La mission `tache1.9.md` est annoncée terminée.
+- Les livrables de schéma/architecture sont présents.
+- Les tâches 2 à 8 ont été prises en compte ou explicitement exclues avec justification.
+
+Si un point manque → rejet immédiat.
+
+---
+
+## 1. Discipline & périmètre
+
+- [ ] Aucun code de production modifié pour cette mission.
+- [ ] Aucun schéma/migration appliqué sans plan de migration validé.
+- [ ] Le schéma actuel `machines/snapshots/executions` est respecté comme point de départ.
+- [ ] Les secrets ne sont pas mélangés aux tables publiques.
+- [ ] Le design reste compatible SQLite/Drizzle au MVP.
+
+---
+
+## 2. Complétude du schéma
+
+- [ ] Core : `machines`, credentials, host keys, tags, état courant.
+- [ ] JSON/historique : snapshots, exécutions, événements, rapports, artefacts, messages importants.
+- [ ] APT : paquets prévus, appliqués, erreurs.
+- [ ] Docker : settings, roots Compose, stacks, services, image events.
+- [ ] Post-install/scripts : profils, recettes, versions, état machine, presets.
+- [ ] Jobs/schedules : jobs, schedules, locks, action requests.
+- [ ] Hermes/MCP : sessions, runs, usage tokens, audit MCP.
+- [ ] API clients : tokens hashés, scopes, révocation.
+- [ ] Optimisation : métriques système, cleanup, discovery scans/candidates.
+- [ ] Frontend : `app_settings`, `user_preferences`, `machine_ui_state`.
+- [ ] Hardware/métriques machines : `machine_hardware`, `machine_metrics_latest`.
+
+---
+
+## 3. Cohérence des principes
+
+- [ ] Chaque JSON canonique complet est archivé.
+- [ ] Les colonnes dérivées nécessaires aux filtres/UI sont indexables.
+- [ ] `machine_id` est la clé de rattachement des échanges machine ↔ serveur.
+- [ ] Les logs bruts sont référencés, pas copiés partout en BDD.
+- [ ] Les messages importants survivent à la purge des logs bruts.
+- [ ] La rétention est modélisée sans supprimer les rapports épinglés.
+- [ ] Les actions Hermes sont représentées par demandes validables, pas par exécution directe.
+- [ ] Les paramètres frontend et app locale sont séparés des données métier.
+
+---
+
+## 4. Compatibilité futur Docker Compose webserver
+
+- [ ] Le schéma prévoit volumes persistants : DB, reports/logs, artifacts.
+- [ ] Le schéma ne dépend pas d'un chemin local non portable.
+- [ ] Les chemins de fichiers sont stockés comme références maîtrisées.
+- [ ] Les secrets master key/API tokens restent hors image Docker.
+- [ ] Le modèle reste utilisable dans un container backend unique au MVP.
+
+---
+
+## 5. Migration progressive
+
+- [ ] Les phases de migration sont ordonnées et réalistes.
+- [ ] Chaque phase peut être testée indépendamment.
+- [ ] La migration depuis le schéma actuel est décrite.
+- [ ] Les index principaux sont listés.
+- [ ] Le passage futur PostgreSQL est anticipé sans imposer PostgreSQL au MVP.
+
+---
+
+## 6. Verdict
+
+- **✅ Accepté** : schéma complet, cohérent, migrable.
+- **🟡 Accepté avec réserves** : ajustements mineurs à faire.
+- **❌ Rejeté** : modèle incomplet, secrets mal placés, migration floue ou incompatible MVP.
+
+---
+
+## 7. Notes de validation
+
+**Revue du 2026-06-05 (orchestrateur).**
+
+**Verdict : ✅ Accepté.**
+
+Le schéma cible (`tache1.9.md`) couvre l'ensemble des besoins des tâches 2 à 8, reste compatible MVP SQLite/Drizzle, isole les secrets (`machine_credentials`), conserve les JSON complets + colonnes dérivées indexées, et fournit un plan de migration en 9 phases depuis le schéma actuel.
+
+- §1 Discipline & périmètre : ✓ (design only, schéma `machines/snapshots/executions` conservé comme base, secrets isolés, SQLite/Drizzle).
+- §2 Complétude : ✓ (tous les groupes de tables présents : core, JSON/historique, APT planned/applied/errors, Docker, post-install, jobs/schedules/locks/action_requests, Hermes/MCP, api_clients, optim/cleanup/discovery, frontend app_settings/user_preferences/machine_ui_state, hardware/metrics).
+- §3 Principes : ✓ (JSON complets + dérivés, `machine_id` clé, logs référencés, `important_messages` survit à la purge, rétention avec `pinned`, actions Hermes via `action_requests`).
+- §4 Docker Compose : ✓ (volumes DB/reports/artifacts, chemins comme références, secrets hors image).
+- §5 Migration : ✓ (phases 1→9 ordonnées et testables indépendamment, index listés §13, PostgreSQL anticipé sans l'imposer).
+
+**Réserve mineure corrigée pendant la revue** : doublon `créer discovery_candidates` dans la §14 Phase 9 (appartient à Phase 8) — supprimé.
+
+**Cohérence avec l'existant** : le WIP non commité ajoute déjà le type `ServerCapabilities` + endpoint `/api/capabilities` (relève de la tâche 5) et la table `api_clients` prévue ici sert la tâche 8 — pas de conflit.
diff --git a/validation_tache2.md b/validation_tache2.md
index 133ac74..f019e8e 100644
--- a/validation_tache2.md
+++ b/validation_tache2.md
@@ -48,7 +48,7 @@ Confronter au § « Livrables attendus » et aux 5 axes de `tache2.md` :
- [ ] Découpage en sous-jalons priorisé.
**Questions d'investigation (§3) :**
-- [ ] Les 7 questions sont tranchées, chacune avec MVP recommandé / alternatives / risques.
+- [ ] Les 8 questions sont tranchées, chacune avec MVP recommandé / alternatives / risques.
> Tout livrable ou question manquant → **renvoi pour complément**.
@@ -89,5 +89,539 @@ Consigner le verdict (date, blocs en échec, actions de suite) en bas de ce fich
---
-## 6. Notes de validation (à remplir au moment de la revue)
-> _(Section laissée vide ; à compléter lors de la validation effective : date, verdict, points relevés, décisions.)_
+## 6. Focus de validation — scripts Docker Compose et intégration webapp
+
+Cette section fixe la proposition attendue pour le volet Docker Compose de la tâche 2. Elle sert de point de contrôle supplémentaire au moment de valider les livrables de design : l'agent doit expliquer comment ces scripts s'insèrent dans l'application existante, sans créer un moteur parallèle.
+
+### Méthode Docker Compose retenue
+
+- [ ] Le design retient une gestion **par SSH sur la machine cible**, en réutilisant la couche d'exécution existante (`server/ssh/client.ts`) et les templates versionnés sur disque. La variante `docker context` over SSH peut être citée comme alternative opérateur, mais ne doit pas devenir le moteur MVP de la webapp.
+- [ ] Les stacks Compose sont découverts depuis des **racines déclarées par machine** (`composeRoots`, ex. `/opt/stacks`, `/srv/docker`) avec scan limité en profondeur, puis validés dans l'UI avant toute action.
+- [ ] La détection automatique par labels Compose (`com.docker.compose.project`, `com.docker.compose.service`, et si présent `com.docker.compose.project.working_dir`) reste un complément pour retrouver les stacks actifs, pas l'unique source de vérité.
+- [ ] Un stack seulement détecté reste en statut `candidate`; les actions `pull`, `up`, `down` et `prune` ne sont autorisées que sur un stack `enabled`/validé par l'utilisateur.
+
+### Scripts/templates Docker attendus
+
+Le design doit proposer au minimum ces templates, avec pseudo-shell réaliste, marqueurs `===SU:DOCKER_*===`, `LC_ALL=C`, sortie parsable et log brut archivé :
+
+- [ ] `templates/docker/scan-compose.sh.tpl` : scanne les racines déclarées, trouve `compose.yaml`, `compose.yml`, `docker-compose.yaml`, `docker-compose.yml`, ignore `.git`, `node_modules`, `backup`, `old`, `archive`, puis valide chaque candidat avec `docker compose config --quiet`.
+- [ ] `templates/docker/inspect-compose.sh.tpl` : produit l'état actuel sans appliquer de changement : `docker compose config --images`, `docker compose ps --format json`, `docker compose images --format json`, puis `docker image inspect` pour les images utilisées.
+- [ ] `templates/docker/pull-check.sh.tpl` : exécute `docker compose pull --policy always --ignore-buildable` pour récupérer les images candidates sans démarrer de conteneurs, puis compare image ID/digest/labels avant-après. Cette action est non applicative mais écrit sur le disque Docker; elle ne doit donc pas être confondue avec un scan purement passif.
+- [ ] `templates/docker/apply-compose.sh.tpl` : applique après validation utilisateur avec `docker compose up -d --remove-orphans`, puis recapture `ps/images/inspect` pour vérifier les conteneurs recréés et les erreurs.
+- [ ] `templates/docker/prune-images.sh.tpl` : nettoyage prudent par défaut avec `docker image prune -f`; mode agressif `docker image prune -a -f --filter "until=168h"` seulement avec validation explicite côté webapp.
+- [ ] `templates/docker/down-compose.sh.tpl` : action séparée et destructive; `docker compose down` ne doit pas faire partie du chemin normal de mise à jour. Toute option `--volumes` ou `--rmi` doit être interdite au MVP ou protégée par une validation forte distincte.
+
+### Flux de mise à jour Docker à valider
+
+Le design doit formaliser ce flux, compatible avec la documentation Docker :
+
+1. `docker_scan` : découverte des stacks candidats depuis les racines déclarées + labels Compose actifs.
+2. `docker_inspect_current` : capture de l'état actuel des services, conteneurs et images.
+3. `docker_pull_check` : téléchargement des images candidates sans démarrage de conteneurs.
+4. Comparaison déterministe : image ref, image ID, repo digest, labels OCI (`org.opencontainers.image.version`, `revision`, `source`, `created`) si présents.
+5. Proposition UI/Hermes : liste des stacks/services avec update disponible, erreurs de pull, inconnues.
+6. `docker_apply` après validation utilisateur : `docker compose up -d --remove-orphans`.
+7. Vérification après application : conteneurs recréés, état `running/exited`, health si disponible, erreurs.
+8. `docker_prune_images` après succès ou sur action séparée : images supprimées, espace récupéré, erreurs.
+
+Points Docker à vérifier dans les livrables :
+
+- [ ] Le design explique que `docker compose pull` télécharge les images mais ne démarre pas les conteneurs; c'est donc un bon pré-check applicatif, mais pas un scan sans effet.
+- [ ] Le design explique que `docker compose up -d` recrée les conteneurs quand l'image ou la configuration a changé, en préservant les volumes montés; `down` n'est donc pas nécessaire pour une mise à jour normale.
+- [ ] Le design distingue `docker image prune -f` (dangling images) de `docker image prune -a` (toutes les images non référencées par un conteneur), et classe `-a` comme action destructive.
+
+Sources Docker de référence à citer dans la spec :
+
+- `docker compose pull` : https://docs.docker.com/reference/cli/docker/compose/pull/
+- `docker compose up` : https://docs.docker.com/reference/cli/docker/compose/up/
+- `docker compose config` : https://docs.docker.com/reference/cli/docker/compose/config/
+- `docker compose ps` : https://docs.docker.com/reference/cli/docker/compose/ps/
+- `docker compose images` : https://docs.docker.com/reference/cli/docker/compose/images/
+- `docker compose down` : https://docs.docker.com/reference/cli/docker/compose/down/
+- `docker image inspect` : https://docs.docker.com/reference/cli/docker/image/inspect/
+- `docker image prune` : https://docs.docker.com/reference/cli/docker/image/prune/
+
+### JSON canonique Docker attendu
+
+- [ ] `UpdateSnapshot` reste rétrocompatible et peut recevoir un bloc optionnel `docker`.
+- [ ] Le bloc snapshot Docker contient au minimum : stacks déclarés, stacks candidats, services, image ref actuelle, image ID actuelle, digest actuel si disponible, labels de version si disponibles, image ID/digest candidat après pull, statut `up_to_date|updates_available|warning|error`.
+- [ ] `ExecutionResult` reste rétrocompatible et peut recevoir un bloc optionnel `docker`, avec résultats `pull`, `up`, `prune`, erreurs, conteneurs recréés, images supprimées et octets récupérés.
+- [ ] Les erreurs sont structurées : `docker_not_installed`, `compose_not_found`, `compose_config_invalid`, `registry_auth_failed`, `pull_failed`, `image_inspect_failed`, `up_failed`, `container_unhealthy`, `prune_failed`, `disk_space_low`.
+- [ ] La réduction Hermes ne transmet que le JSON canonique + lignes importantes (`Pulling`, `Digest`, `Status`, `Downloaded newer image`, `Recreating`, `Started`, `Error`, `deleted`, `Total reclaimed space`). Le log brut complet reste archivé.
+
+### Insertion dans la webapp existante
+
+Le design doit montrer comment Docker s'emboîte dans les surfaces déjà présentes :
+
+- [ ] **Config machine** : ajouter des champs de configuration Docker par machine dans le modèle futur (`dockerEnabled`, `composeRoots`, `composeScanDepth`, `composeStacks[]`), sans casser `MachineView` existant; les nouveaux champs doivent être optionnels ou livrés dans un endpoint dédié.
+- [ ] **Refresh/snapshot** : étendre le refresh machine pour pouvoir produire un snapshot combiné `apt` + `docker`, ou un refresh Docker séparé si l'équipe choisit d'éviter que `docker_pull_check` soit lancé automatiquement.
+- [ ] **Actions** : étendre progressivement `ActionType` avec des actions explicites (`docker_scan`, `docker_pull_check`, `docker_compose_apply`, `docker_prune_images`, `docker_compose_down`) en conservant le filtrage d'autorisation côté route `POST /api/machines/:id/actions`.
+- [ ] **Executions/rapports** : réutiliser la table `executions`, le WebSocket terminal, `rawLogPath`, `reportPath` et le statut `ok|warning|error`; ne pas créer un second système de jobs Docker.
+- [ ] **UI machine** : afficher un compteur Docker séparé du compteur APT (ex. stacks avec updates), proposer une vue détail par stack/service, puis boutons d'action validés (`Pull/check`, `Appliquer`, `Prune`, `Down`).
+- [ ] **Validation utilisateur** : `docker_compose_apply`, `docker_prune_images` agressif et `docker_compose_down` doivent passer par une confirmation UI explicite; Hermes peut proposer mais ne déclenche jamais directement ces actions.
+- [ ] **Secrets** : les credentials registry éventuellement présents sur la machine (`~/.docker/config.json`, helper, tokens) ne sont jamais lus ni renvoyés; les erreurs doivent être nettoyées avant UI/MCP si elles exposent une URL sensible.
+
+---
+
+## 7. Focus de validation — APT update/analyse, upgrade et reboot vérifié
+
+Cette section fixe la proposition attendue pour la suite APT de la tâche 2. Elle doit valider que le design répond au besoin opérateur : voir la liste des paquets avant exécution, prouver ce qui a réellement changé après exécution, gérer les cas non interactifs, puis vérifier qu'un reboot a vraiment abouti.
+
+### `update + analyse` avant upgrade
+
+- [ ] Le design introduit une action de refresh/analyse explicite, nommable `apt_update_analyze`, distincte des upgrades manuels destructifs.
+- [ ] Cette action exécute au minimum `apt-get update`, puis une simulation `apt-get -s upgrade` et une simulation `apt-get -s dist-upgrade` ou équivalent profil OS.
+- [ ] Le snapshot produit liste les paquets qui **seraient** mis à jour avant validation utilisateur : nom, version actuelle, version cible, origine, architecture si disponible.
+- [ ] Le snapshot distingue `upgrade` et `full/dist-upgrade` : paquets mis à jour, paquets nouvellement installés, paquets qui seraient supprimés, paquets retenus/held.
+- [ ] Le design explique que les simulations APT sont parsées via les lignes documentées `Inst`, `Conf`, `Remv`, tout en gardant le log brut archivé.
+- [ ] Le statut snapshot APT distingue `ok`, `updates_available`, `warning`, `error`, avec `warning` si `full/dist-upgrade` implique des suppressions ou des paquets retenus.
+
+Sources APT à citer dans la spec :
+
+- `apt-get` simulation et commandes : https://manpages.debian.org/apt-get
+- `dpkg` conffiles/options : https://manpages.debian.org/dpkg
+- `apt-listchanges` non-interactive : https://manpages.debian.org/bookworm/apt-listchanges/apt-listchanges.1.en.html
+- `needrestart` non-interactive/list mode : https://manpages.debian.org/bookworm/needrestart/needrestart.1.en.html
+
+### Profils OS et type de machine
+
+- [ ] Le design distingue `os_family` et `machine_kind` lors de l'ajout machine.
+- [ ] L'UI propose un choix manuel : Debian, Ubuntu, Proxmox VE, Raspberry Pi OS, autre Linux.
+- [ ] L'UI propose un choix manuel du type : VM, machine physique, hôte Proxmox, LXC/container, Raspberry Pi, serveur GPU/workstation.
+- [ ] Une action `machine_probe` ou équivalent peut détecter/corriger : `/etc/os-release`, architecture, virtualisation, Proxmox, Raspberry Pi, GPU, disques, mémoire.
+- [ ] Les scripts proposés dépendent du couple OS/type machine : firmware/driver sur physique, guest tools sur VM, profil Proxmox sur hôte PVE, profil Raspberry Pi OS sur Pi.
+- [ ] Debian avec firmware/drivers propriétaires vérifie explicitement `contrib`, `non-free`, `non-free-firmware`.
+- [ ] Proxmox est traité comme profil dédié et pas comme Debian générique.
+- [ ] Les scripts hardware/drivers/benchmark ne sont jamais installés par défaut et exigent validation.
+
+### Scripts/templates APT attendus
+
+Le design doit proposer au minimum ces templates, en conservant `LC_ALL=C`, `DEBIAN_FRONTEND=noninteractive`, le proxy APT optionnel, les marqueurs `===SU:APT_*===`, et `===SU:EXIT=N===` :
+
+- [ ] `templates/apt/update-analyze.sh.tpl` : update des index + simulations `upgrade` et `dist/full-upgrade` + reboot-check.
+- [ ] `templates/apt/upgrade.sh.tpl` : applique l'upgrade simple, sans suppressions volontaires.
+- [ ] `templates/apt/full-upgrade.sh.tpl` ou `dist-upgrade.sh.tpl` : applique le profil complet; le design clarifie l'alias UI `full-upgrade` vs commande système `apt-get dist-upgrade`/`apt full-upgrade`.
+- [ ] `templates/apt/autoremove.sh.tpl` : retire les dépendances inutiles, avec prévisualisation ou confirmation explicite si des paquets seront supprimés.
+- [ ] `templates/apt/clean.sh.tpl` : vide le cache des paquets téléchargés; action séparée.
+- [ ] `templates/apt/reboot-check.sh.tpl` : vérifie `/run/reboot-required`, `/var/run/reboot-required` et les paquets listés si `reboot-required.pkgs` existe.
+- [ ] `templates/apt/reboot.sh.tpl` : planifie le reboot et émet un marqueur de sortie parsable avant coupure SSH.
+
+### Upgrade non interactif et interactions humaines
+
+- [ ] Les upgrades réels utilisent une politique non interactive explicite : `DEBIAN_FRONTEND=noninteractive`, `apt-get -y`, options dpkg défensives `--force-confdef` + `--force-confold`.
+- [ ] Le design justifie la politique par défaut : conserver les fichiers de configuration locaux quand dpkg ne peut pas résoudre automatiquement, afin d'éviter d'écraser une configuration distante.
+- [ ] Les prompts potentiels (`conffile`, `debconf`, `apt-listchanges`, `needrestart`, service restart, maintainer script) sont traités comme risques de blocage à détecter, pas comme dialogues interactifs à exposer dans le terminal.
+- [ ] Le design prévoit un timeout d'inactivité et/ou un timeout global pour classer l'exécution en erreur contrôlée si une action reste bloquée.
+- [ ] Une erreur structurée `human_interaction_required` ou équivalent est prévue, avec lignes importantes et recommandation de reprise manuelle; aucune auto-réparation dangereuse n'est lancée sans validation.
+
+### Diff réel avant/après
+
+- [ ] Le design ne se contente pas de l'exit code APT pour déclarer une réussite.
+- [ ] Avant et après chaque action APT réelle, le template capture l'état dpkg via une commande stable du type `dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n'`.
+- [ ] Le backend ou le contrat JSON compare l'état avant/après pour produire : paquets mis à jour, installés, supprimés, inchangés malgré simulation, versions finales et anomalies.
+- [ ] `ExecutionResult` reste rétrocompatible et reçoit un bloc optionnel `apt` détaillant `planned`, `applied`, `installed`, `removed`, `held`, `errors`, `rebootRequiredAfterRun`.
+- [ ] Le rapport Markdown résume le diff réel et garde une référence au log brut, sans l'inliner entièrement.
+
+### Reboot vérifié et délai adaptatif
+
+- [ ] Le design introduit un reboot vérifié : lire un identifiant de boot avant reboot (`/proc/sys/kernel/random/boot_id`), déclencher le reboot, attendre la coupure SSH, retenter la connexion, puis relire le `boot_id`.
+- [ ] Le reboot est considéré `ok` uniquement si la machine redevient joignable et si le `boot_id` a changé.
+- [ ] Le résultat reboot contient au minimum : `beforeBootId`, `afterBootId`, `requestedAt`, `sshWentDownAt`, `sshCameBackAt`, `waitedSeconds`, `status`, `errors`.
+- [ ] Le design prévoit un délai adaptatif par machine (`lastRebootDurationSeconds`, `nextRecommendedWaitSeconds`) mis à jour après chaque reboot réussi, avec marge de sécurité.
+- [ ] En cas d'échec, le statut distingue au moins : `reboot_command_failed`, `ssh_never_went_down`, `machine_did_not_return`, `boot_id_unchanged`, `timeout`.
+- [ ] Le reboot reste une action destructrice soumise à validation explicite côté webapp; Hermes peut recommander mais ne déclenche jamais directement.
+
+### Insertion dans la webapp existante
+
+- [ ] `apt_update_analyze` alimente le snapshot existant et la tuile machine avec la liste des paquets prévus avant upgrade.
+- [ ] Les actions `apt_upgrade`, `apt_full_upgrade`/`apt_dist_upgrade`, `apt_autoremove`, `apt_clean` et `reboot_verified` passent par la même route d'action et la même table `executions` que le jalon 1.
+- [ ] L'UI doit afficher avant exécution : liste des paquets, suppressions éventuelles, paquets retenus, reboot déjà requis, et niveau de risque.
+- [ ] L'UI doit afficher après exécution : réussite/erreur, diff réel, reboot requis après coup, lien rapport, lien log brut.
+- [ ] Les actions qui peuvent supprimer des paquets ou redémarrer la machine (`dist/full-upgrade`, `autoremove`, `reboot_verified`) exigent une confirmation UI explicite.
+- [ ] Les nouveaux champs JSON et actions sont ajoutés de manière rétrocompatible pour que les flux jalon 1 (`refresh`, `apt_full_upgrade`, `reboot`) restent valides pendant la migration.
+
+---
+
+## 8. Focus de validation — scripts post-install et profils personnalisés
+
+Cette section fixe la proposition attendue pour les scripts post-install de la tâche 2. Elle doit valider que le design couvre le cas concret d'une VM Debian 13 installée en netinstall CLI, joignable d'abord via DHCP, puis préparée à distance via SSH avec changement éventuel d'identité, réseau statique, paquets de base, partages réseau et Docker officiel.
+
+### Principe UX et absence d'interactivité SSH
+
+- [ ] Le design interdit les questions interactives au milieu d'un script SSH. Toute question nécessaire est transformée en champ de formulaire dans la webapp avant exécution.
+- [ ] Les scripts post-install sont présentés sous forme de profils cochables; cocher un profil déplie automatiquement ses champs obligatoires.
+- [ ] Chaque profil fournit un manifeste décrivant `id`, `label`, `description`, `fields`, valeurs par défaut, validations, prévisualisation, niveau de risque et confirmations requises.
+- [ ] Le bouton d'exécution reste désactivé tant que les champs requis du ou des profils cochés ne sont pas valides.
+- [ ] La webapp propose une preview du template rendu avant exécution, avec masquage des secrets et signalement des changements réseau/reboot.
+- [ ] Les scripts s'exécutent en mode non interactif; s'ils détectent une situation nécessitant une décision non fournie, ils échouent avec une erreur structurée au lieu de bloquer.
+
+### Profils post-install attendus
+
+Le design doit proposer au minimum ces profils composables :
+
+- [ ] `bootstrap_root` : première préparation après connexion DHCP et élévation via `su -` ou contexte root; installe `sudo`, `resolvconf`, `ca-certificates`, `curl`; ajoute l'utilisateur opérateur au groupe `sudo`; vérifie `sudo`.
+- [ ] `identity_network` : modifie si besoin le hostname, le domaine/search local `.home`, `/etc/hosts`, puis configure l'IP statique dans `/etc/network/interfaces`.
+- [ ] `base_tools` : outils de base sans `vim`; prévoir notamment `nano`, `less`, `bash-completion`, `tmux`, `screen`, `htop`, `iotop`, `ncdu`, `tree`, `rsync`, `unzip`, `zip`, `tar`.
+- [ ] `network_tools` : `iproute2`, `iputils-ping`, `dnsutils`, `traceroute`, `net-tools` optionnel, `tcpdump`, `nmap`, `mtr-tiny`, `lsof`, `netcat-openbsd`.
+- [ ] `dev_git` : `git`, `curl`, `wget`, `jq`, `yq`, `gnupg`, `lsb-release`; `build-essential` seulement optionnel.
+- [ ] `sharing` : profils de partage réseau avec `samba`, `nfs-kernel-server`, `avahi-daemon`, `libnss-mdns`, configurables séparément si besoin.
+- [ ] `docker_official` : installation Docker Engine depuis le dépôt officiel Docker Debian, avec `docker-ce`, `docker-ce-cli`, `containerd.io`, `docker-buildx-plugin`, `docker-compose-plugin`, ajout utilisateur au groupe `docker`, création du dossier Compose dans le home, puis reboot/reconnexion si nécessaire.
+- [ ] `vm_guest_tools` : paquet adapté à l'hyperviseur (`qemu-guest-agent` ou `open-vm-tools`) choisi par l'utilisateur.
+- [ ] Profils optionnels à prévoir mais non installés par défaut : `security_basic`, `backup_tools`, `monitoring`, `mail_notify`, `time_sync`, `storage_tools`.
+
+### Champs dynamiques attendus dans l'interface
+
+Le design doit préciser les champs générés lorsqu'un profil est coché :
+
+- [ ] Pour `identity_network` : `newHostname`, `domain/search`, `interfaceName`, `staticAddress` au format CIDR (ex. `10.0.x.y/22`), `gateway` (défaut `10.0.0.1`), `dnsNameservers` (défaut `10.0.0.1`, `10.0.0.10`), `reconnectHost`.
+- [ ] Pour `docker_official` : `dockerUser` (ex. `gilles`), `dockerHomeDir` ou `composeRoot` (ex. `/home/gilles/docker`), `installComposePlugin`, `rebootAfterInstall`.
+- [ ] Pour `sharing` : choix séparé Samba/NFS/mDNS, noms de partages éventuels, chemins autorisés, utilisateurs/groupes si nécessaire.
+- [ ] Pour `vm_guest_tools` : type d'hyperviseur ou paquet cible.
+- [ ] Les champs peuvent être préremplis depuis la machine (`machine.name`, IP DHCP, interface primaire détectée, utilisateur SSH), mais doivent rester modifiables avant validation.
+
+Exemple de manifeste attendu dans la spec :
+
+```json
+{
+ "id": "identity_network",
+ "label": "Hostname + IP statique",
+ "requiresConfirmation": true,
+ "risk": "network_change",
+ "fields": [
+ { "name": "newHostname", "type": "hostname", "required": true },
+ { "name": "domain", "type": "string", "required": true, "default": "home" },
+ { "name": "interfaceName", "type": "select", "required": true, "defaultFrom": "detected.primaryInterface" },
+ { "name": "staticAddress", "type": "ipv4_cidr", "required": true },
+ { "name": "gateway", "type": "ipv4", "required": true, "default": "10.0.0.1" },
+ { "name": "dnsNameservers", "type": "ipv4_list", "required": true, "default": ["10.0.0.1", "10.0.0.10"] },
+ { "name": "reconnectHost", "type": "ipv4", "required": true, "defaultFrom": "staticAddress.ip" }
+ ]
+}
+```
+
+### Scripts/templates custom attendus
+
+- [ ] `templates/custom/bootstrap-root.sh.tpl` : prépare sudo et les prérequis minimaux depuis un contexte root ou sudo.
+- [ ] `templates/custom/identity-network.sh.tpl` : écrit hostname, hosts et réseau statique; sauvegarde les fichiers modifiés; signale explicitement le changement d'adresse.
+- [ ] `templates/custom/install-package-groups.sh.tpl` : installe les groupes de paquets sélectionnés, sans `vim` par défaut.
+- [ ] `templates/custom/docker-official-debian.sh.tpl` : suit la documentation officielle Docker Debian pour le dépôt APT, la clé GPG dans `/etc/apt/keyrings`, `docker.sources`, puis les paquets Docker/Compose.
+- [ ] `templates/custom/sharing.sh.tpl` : installe Samba/NFS/mDNS selon les choix, sans exposer de configuration dangereuse par défaut.
+- [ ] `templates/custom/vm-guest-tools.sh.tpl` : installe l'agent invité choisi.
+
+Sources à citer dans la spec :
+
+- Docker Debian officiel : https://docs.docker.com/engine/install/debian/
+- Docker Compose plugin : https://docs.docker.com/compose/install/linux/
+- Debian network configuration : https://wiki.debian.org/NetworkConfiguration
+- Debian Handbook network configuration : https://www.debian.org/doc/manuals/debian-handbook/sect.network-config
+- Debian package `resolvconf` : https://packages.debian.org/stable/net/resolvconf
+
+### Réseau, reboot et reconnexion
+
+- [ ] Le design traite `identity_network` comme une action à risque : confirmation UI explicite, preview obligatoire, sauvegarde de `/etc/network/interfaces` et `/etc/hosts`.
+- [ ] Lorsqu'une IP statique est appliquée, le résultat JSON contient l'ancien endpoint DHCP, le nouveau endpoint, et l'indication que la reconnexion doit se faire sur `reconnectHost`.
+- [ ] Le script ne doit pas couper la connexion sans que la webapp ait planifié la stratégie de reconnexion; si reboot requis, utiliser le mécanisme `reboot_verified`.
+- [ ] Après reboot ou redémarrage réseau, la webapp vérifie la reconnexion sur la nouvelle IP/hostname et met à jour la machine si le retour est confirmé.
+- [ ] En cas d'échec, les erreurs distinguent `network_config_invalid`, `interface_not_found`, `dns_config_failed`, `reconnect_failed`, `hostname_failed`, `sudo_setup_failed`.
+
+### JSON canonique post-install attendu
+
+- [ ] `ExecutionResult` reste rétrocompatible et peut recevoir un bloc optionnel `custom` ou `postInstall`.
+- [ ] Le résultat liste les profils exécutés, les variables non sensibles utilisées, les fichiers modifiés, les paquets installés, les services activés/démarrés, les reboots demandés et les erreurs.
+- [ ] Les secrets ou tokens ne sont jamais inclus dans les variables sérialisées, les logs UI, les rapports ni le MCP.
+- [ ] Les modifications réseau et Docker sont clairement marquées dans le rapport Markdown avec les prochaines actions attendues, par exemple reconnexion, logout/login pour groupe Docker, ou reboot.
+
+### Insertion dans la webapp existante
+
+- [ ] Les scripts post-install passent par la même mécanique que les autres actions : templates versionnés, preview, exécution SSH, WebSocket terminal, `executions`, rapport Markdown et log brut.
+- [ ] La configuration des profils peut être stockée par machine ou lancée comme assistant ponctuel de provisioning, mais le design doit préciser où sont conservées les valeurs réutilisables.
+- [ ] Hermes peut aider à proposer des profils ou expliquer un échec, mais ne reçoit que le JSON réduit et ne déclenche jamais les actions à risque sans validation webapp.
+- [ ] Les profils post-install sont découpés en sous-jalons indépendants : bootstrap/sudo, identité+réseau, paquets de base, Docker officiel, partage réseau, outils VM/monitoring.
+
+---
+
+## 9. Notes de validation (à remplir au moment de la revue)
+
+> **Date** : 2026-06-05
+> **Signé** : (orchestrateur, revue indépendante post-design)
+
+---
+
+> **Note historique** : une première validation (2026-06-05, délégation initiale) avait conclu au rejet faute de livrables. Les livrables ont depuis été produits. La présente revue est **indépendante** — elle ne s'appuie pas sur l'auto-évaluation `99-couverture-gate.md` mais sur la lecture directe de chaque livrable.
+
+---
+
+### Verdict global
+
+🟡 **Accepté avec réserves mineures** — les livrables sont complets, cohérents avec l'existant et couvrent l'intégralité des cases du gate. Deux réserves mineures non bloquantes sont détaillées ci-dessous (renommage de fichier à tracer dans SJ-0 ; état non committé de `shared/types.ts` à surveiller). Aucune incohérence majeure, aucune rupture de rétro-compatibilité, aucun manque réel. La mission peut progresser vers `writing-plans` pour SJ-0 dès confirmation de la non-régression (`pnpm check/test/build`).
+
+---
+
+### Vérification §0 — Conditions préalables
+
+- **Section « État d'avancement / Ce qui a été fait »** dans `tache2.md` : **PRÉSENTE** (§10, ll. 196-241). Contient les chemins des 11 livrables, les 8 décisions, les questions tranchées, ce qui reste ouvert, les sous-jalons recommandés. Condition satisfaite.
+- **Livrables de design présents sous `docs/`** : **11 fichiers présents** sous `docs/design/tache2/` (00→99). Condition satisfaite.
+- **`tache2.md` annoncé terminé** : oui (§10 statut = « mission de design terminée »). Condition satisfaite.
+
+**Conclusion §0 : toutes les conditions préalables sont remplies. La validation peut s'ouvrir.**
+
+---
+
+### Vérification §1 — Discipline & périmètre
+
+- **Fichiers `docs/design/tache2/**` et `tache2.md`** : seuls fichiers nouveaux liés à la tâche 2. **OK.**
+- **Jalon 1 intact** : `templates/apt/check.sh.tpl`, `full-upgrade.sh.tpl`, `reboot.sh.tpl`, `server/services/aptParse.ts`, `server/templates/aptReduce.ts`, `server/templates/render.ts`, `server/ssh/client.ts` — **aucun touché**. **OK.**
+- **Dépôts de référence** : `linux-update-dashboard/` et `nas-ops/` non copiés (cités en inspiration, pseudo-shell réécrit). **OK.**
+- **Hors-scope listés comme suggestions uniquement** : `00-synthese.md §6` liste explicitement les renvois vers tâches 3/4/5/6/7. **OK.**
+- **`shared/types.ts` modifié** : le diff montre l'ajout de l'interface `ServerCapabilities` (32 lignes). L'auto-évaluation affirme que l'agent tâche 2 n'a pas touché ce fichier. La vérification du diff confirme que cette modification (endpoint `/capabilities`, gestion d'erreur Hono, import `capabilities.ts`) est cohérente avec le chantier jalon 2 en cours (polish design system) et non avec la tâche 2. **Il s'agit d'une modification du contexte de travail partagé, pas d'une violation de la tâche 2.** Toutefois, le fichier `shared/types.ts` étant dans un état non committé différent de l'état jalon 1, la vérification de rétro-compatibilité (§3) doit se faire sur l'état **actuel** de ce fichier (qui inclut déjà `ServerCapabilities` mais conserve intégralement `AptPackage`, `UpdateSnapshot`, `ExecutionResult`, `MachineView`). Pas d'incidence sur le design de la tâche 2. **Réserve de contexte, non bloquante.**
+
+**Conclusion §1 : aucune violation de périmètre imputable à la tâche 2. La modification de `shared/types.ts` provient du jalon 2 concurrent.**
+
+---
+
+### Vérification §2 — Complétude des livrables
+
+**Axes :**
+
+| Axe | Couvert ? | Livrable principal | Vérification |
+|---|---|---|---|
+| A — Templates APT + profils OS + proxy | **OUI** | `10-templates-apt.md`, `60-profils-os-machine.md` | 7 templates inventoriés avec marqueurs, pseudo-shell pour 5, profils OS Debian/Ubuntu/Proxmox/RPi, proxy direct/runtime/persistent. |
+| B — Capture prévu/appliqué, Hermes | **OUI** | `40-contrats-json.md` | Snapshot dpkg before/after, `AptExecutionResult.planned`/`applied`, déduplication, réducteur étendu. |
+| C — Taxonomie des erreurs | **OUI** | `50-erreurs.md` | 11 codes APT, 10 codes Docker, 6 codes réseau/post-install, stratégie de remédiation non auto. |
+| D — Docker scan/pull/up/down/prune | **OUI** | `20-docker.md` | 6 templates avec pseudo-shell, flux 8 étapes, réduction Hermes, insertion webapp. |
+| E — Scripts custom + overrides + garde-fous | **OUI** | `30-scripts-custom.md` | 9 profils post-install, manifestes, champs dynamiques, pseudo-shell 6 templates, garde-fous. |
+
+**Livrables §4 :**
+
+| # | Livrable | Présent | Fichier |
+|---|---|---|---|
+| 1 | Inventaire des templates | **OUI** | `10` §2, `20` §2, `30` §2 |
+| 2 | Contenu proposé (pseudo-shell, `===SU:XXX===`) | **OUI** | `10` §4, `20` §4, `30` §4 |
+| 3 | Schémas JSON canoniques étendus | **OUI** | `40` complet |
+| 4 | Taxonomie des erreurs | **OUI** | `50` |
+| 5 | Modèle profils OS + overrides | **OUI** | `60` §1-§4 |
+| 6 | Modèle profils machine | **OUI** | `60` §5-§6 |
+| 7 | Modèle scripts personnalisés | **OUI** | `30` |
+| 8 | Note de sécurité | **OUI** | `70` |
+| 9 | Découpage en sous-jalons priorisé | **OUI** | `80` (SJ-0→SJ-9) |
+
+**8 questions d'investigation (§3) :** toutes tranchées dans `90-questions-investigation.md` avec MVP / alternatives / risques. Vérification directe : Q1 (parsing hybride TS dominant), Q2 (un fichier complet par profil + fallback base), Q3 (manuel + machine_probe), Q4 (dpkg-query before/after), Q5 (extensions additives types TS), Q6 (nohup + exit-code + reboot_verified), Q7 (barrière action_requests + nettoyage secrets), Q8 (surface MCP v1 conservée). **Toutes tranchées.**
+
+**Conclusion §2 : complétude intégrale. Aucun livrable manquant, aucune question non tranchée.**
+
+---
+
+### Vérification §3 — Cohérence & intégration avec l'application existante
+
+**Types JSON — rétro-compatibilité (`shared/types.ts`) :**
+
+Vérification directe de l'état actuel de `shared/types.ts` (ll. 1-89) :
+- `OsFamily = "debian" | "ubuntu" | "unknown"` → le design propose `"proxmox" | "raspbian"` en plus. Additif, aucune rupture.
+- `AptProxyMode = "direct" | "runtime"` → ajout `"persistent"`. Additif.
+- `ActionType = "apt_full_upgrade" | "reboot"` → ajout de 12 valeurs. Additif.
+- `UpdateSnapshot.apt` : bloc requis conservé tel quel ; les ajouts (`status?`, `held?`, `removed?`, etc.) sont tous optionnels. Un payload jalon 1 reste valide.
+- `ExecutionResult` : `mode: "manual"` élargi en union — **réserve mineure** : le champ `mode` passe de littéral `"manual"` à `"manual" | "scheduled" | "hermes_requested"`. TypeScript accepte l'union élargie sans rupture (un payload avec `mode: "manual"` reste valide). Non bloquant mais à noter pour la migration progressive.
+- `MachineView` : non touché par le design. **OK.**
+- `ServerCapabilities` (ajout jalon 2) : non conflictuel avec les extensions tâche 2.
+
+**Convention `===SU:XXX===` :**
+
+Vérification directe sur `check.sh.tpl` (marqueurs actuels : `===SU:UPDATE===`, `===SU:SIMULATE===`, `===SU:REBOOT===`, `===SU:END===`) et les pseudo-shells proposés :
+- `update-analyze.sh.tpl` utilise `===SU:APT_UPDATE===`, `===SU:APT_SIM_UPGRADE===`, `===SU:APT_SIM_DISTUPGRADE===`, etc. — nouveaux noms, cohérents avec la convention de nommage.
+- `10-templates-apt.md §6` précise explicitement le plan de migration : `check.sh.tpl` conservé, `update-analyze.sh.tpl` introduit comme successeur enrichi, bascule dans un sous-jalon dédié (SJ-1). **OK.**
+- `full-upgrade.sh.tpl` existant utilise `===SU:UPGRADE===`/`===SU:REBOOT===`/`===SU:EXIT===` ; le design ajoute `===SU:DPKG_BEFORE===` et `===SU:DPKG_AFTER===` sans supprimer les marqueurs existants. Extension additive. **OK.**
+- `reboot.sh.tpl` existant utilise `===SU:REBOOT_NOW===` ; le design propose `===SU:BOOT_ID_BEFORE===` + `===SU:REBOOT_NOW===`. Ici une **réserve mineure** : le `reboot.sh.tpl` proposé (`10-templates-apt.md §4.5`) émet uniquement `===SU:BOOT_ID_BEFORE===` et `===SU:REBOOT_NOW===`, mais n'émet pas de `===SU:EXIT=N===` final, contrairement à la convention établie dans les autres templates. En pratique le script se termine par `echo "reboot planifié"` sans marqueur de sortie — à corriger en implémentation (SJ-3). Signalé comme coquille de cohérence.
+
+**Parsing — stratégie explicite et justifiée :**
+
+Décision Q1 (`90` §Q1) : hybride à dominante TS. Cohérent avec `aptParse.ts` et `aptReduce.ts` existants. La stratégie d'extension (`reduceLines.ts` additif, sans casser `reduceAptLines`) est explicite. **OK.**
+
+**Couche SSH (`server/ssh/client.ts`) :**
+
+Le design réutilise exclusivement `runScriptSudo` / `runPlain` partout (cité en `00-synthese.md §4`, `90` Q6, `20` §1, `30` §6). Aucun nouveau mécanisme SSH. **OK.**
+
+**Frontière Hermes/MCP :**
+
+- Aucun secret (mot de passe, token, credential registry) ne transite vers Hermes/MCP : `70` §1, `30` §5, `20` §6. Mécanisme de nettoyage déterministe décrit. **OK.**
+- Surface MCP minimale : 8 outils v1 conservés, nouvelles capacités via `run_action` filtré. Aucune nouvelle primitive SSH. **OK.**
+- `run_action` sur action destructive non validée → `action_request` en attente, pas une exécution. **OK.**
+
+**Sécurité — actions destructives :**
+
+Tableau complet dans `70` §2 (8 actions avec niveau de validation). `--volumes`/`--rmi` sur `down` interdits au MVP. Hermes propose, ne déclenche jamais. **OK.**
+
+**Profils OS — non-invalidation Debian/Ubuntu :**
+
+Mécanisme de fallback `templates/apt/*` (profil `base`) : les Debian/Ubuntu sans dossier dédié utilisent les templates existants. Non-régression jalon 1 assurée par convention de chemin. **OK.**
+
+**Sous-jalons — implémentabilité indépendante :**
+
+`80-sous-jalons.md` : SJ-0→SJ-9, dépendances explicites, chacun décrit en termes de contenu + testabilité + risque + priorité. SJ-0 marqué comme bloquant pour tous les autres. **OK.**
+
+**Alignement `tache1.9.md` :**
+
+`00-synthese.md §5` mappe chaque bloc JSON sur les tables prévues par `tache1.9.md` (`snapshots`, `executions`, `apt_planned_packages`, `apt_applied_packages`, `docker_compose_stacks`, `docker_stack_services`, `docker_image_events`, `apt_errors`, `important_messages`, `install_profiles`, `install_recipes`, `install_recipe_versions`, `machine_profile_state`, `script_variables_presets`, `docker_settings`, `docker_compose_roots`, `machine_hardware`). Aucune table nouvelle non prévue. **OK.**
+
+**Conclusion §3 : cohérence globale confirmée. Deux points mineurs identifiés (mode union + marqueur EXIT manquant sur reboot.sh.tpl), non bloquants pour le design.**
+
+---
+
+### Vérification §4 — Non-régression
+
+L'agent tâche 2 n'a modifié aucun fichier de production (templates, server, client, shared, configs). La non-régression formelle (`pnpm check/test/build`) est à exécuter par l'orchestrateur — aucun code n'ayant été touché par la mission de design, elle est attendue verte. **À vérifier avant SJ-0.**
+
+---
+
+### Vérification §6 — Focus Docker Compose (grille détaillée)
+
+Vérification directe sur `20-docker.md` et `40-contrats-json.md` :
+
+| Case §6 | Couvert ? | Détail |
+|---|---|---|
+| Gestion par SSH, couche existante ; `docker context` = alternative | **OUI** | `20` §1, explicit |
+| Stacks depuis `composeRoots`, scan limité (`composeScanDepth`), validation UI | **OUI** | `20` §1 et §4.1 (pseudo-shell scan) |
+| Détection labels en complément (`com.docker.compose.project`, `.service`, `.working_dir`) | **OUI** | `20` §1 et §4.1 (section `DOCKER_LABELS`) |
+| Stack détecté = `candidate` ; actions sur `enabled` seulement | **OUI** | `20` §1 et §2 tableau |
+| `scan-compose.sh.tpl` (fichiers compose, ignore .git/node_modules/backup/old/archive, `config --quiet`) | **OUI** | `20` §4.1, pseudo-shell complet |
+| `inspect-compose.sh.tpl` (`config --images`, `ps --format json`, `images --format json`, `image inspect`) | **OUI** | `20` §4.2 |
+| `pull-check.sh.tpl` (`--policy always --ignore-buildable`, compare ID/digest/labels, non passif) | **OUI** | `20` §4.3 ; non-passivité explicitée dans §1 tableau et §3 |
+| `apply-compose.sh.tpl` (`up -d --remove-orphans`, recapture ps/images/inspect) | **OUI** | `20` §4.4 |
+| `prune-images.sh.tpl` (safe `-f` ; agressif `-a -f --filter until=168h` validé) | **OUI** | `20` §4.5 ; Mustache `{{#aggressive}}` |
+| `down-compose.sh.tpl` (séparé/destructif ; `--volumes`/`--rmi` interdits) | **OUI** | `20` §4.6 ; commentaire explicite dans pseudo-shell |
+| Flux 1→8 formalisé | **OUI** | `20` §3 (8 étapes numérotées) |
+| `pull` télécharge sans démarrer les conteneurs | **OUI** | `20` §3 (point clé n°1) |
+| `up -d` recrée si changement, préserve volumes, `down` inutile | **OUI** | `20` §3 (point clé n°2) |
+| `prune -f` vs `-a` (destructif) distingués | **OUI** | `20` §2 tableau + §3 (point clé n°3) |
+| Sources Docker citées (8 URLs) | **OUI** | `20` §1, toutes les 8 URLs listées |
+| `UpdateSnapshot.docker` rétrocompatible (bloc optionnel) | **OUI** | `40` §3 ; `docker?` optionnel sur `UpdateSnapshot` |
+| Bloc snapshot Docker minimal (stacks déclarés/candidats/services/IDs/digest/labels/candidat/statut) | **OUI** | `40` §3, interfaces `DockerSnapshotStack` et `DockerSnapshotService` |
+| `ExecutionResult.docker` (pull/up/prune/erreurs/recréés/supprimés/octets) | **OUI** | `40` §4, `DockerExecutionResult` |
+| Erreurs Docker structurées (10 codes) | **OUI** | `50` §3, tableau des 10 codes |
+| Réduction Hermes lignes Docker + log brut archivé | **OUI** | `20` §5, `40` §7 |
+| Config machine `dockerEnabled`/`composeRoots`/`composeScanDepth`/`composeStacks[]`, `MachineView` intact | **OUI** | `20` §6 ; champs optionnels ou endpoint dédié |
+| Refresh combiné apt+docker ou Docker séparé | **OUI** | `20` §6 (pull-check séparé recommandé) |
+| `ActionType` étendu (docker_*) + filtrage autorisation route | **OUI** | `40` §2 et `20` §6 |
+| Réutilise `executions`/WS/`rawLogPath`/`reportPath`/statut, pas de second système | **OUI** | `20` §1 et §6 |
+| UI compteur Docker séparé + détail + boutons validés | **OUI** | `20` §6 |
+| Validation UI apply/prune agressif/down ; Hermes ne déclenche pas | **OUI** | `20` §6, `70` §2 |
+| Secrets registry jamais lus ; erreurs nettoyées | **OUI** | `20` §6, `70` §1 (filtre regex sensible) |
+
+**Conclusion §6 : toutes les cases Docker confirmées par lecture directe des livrables. Aucun manque.**
+
+---
+
+### Vérification §7 — Focus APT / analyse / reboot vérifié
+
+| Case §7 | Couvert ? | Détail |
+|---|---|---|
+| `apt_update_analyze` distinct des upgrades destructifs | **OUI** | `10` §2 tableau (type snapshot, non destructif) |
+| `apt-get update` + `-s upgrade` + `-s dist-upgrade` | **OUI** | `10` §4.1 pseudo-shell |
+| Snapshot liste paquets prévus (nom/cur/cible/origine/arch) | **OUI** | `40` §3 `AptSnapshotDetail` ; arch en option |
+| Distingue upgrade vs dist-upgrade (maj/install/remove/held) | **OUI** | `10` §4.1 parsing + `40` §3 (`upgradeCount`, `distUpgradeCount`, `removed`, `held`) |
+| Simulations parsées via `Inst`/`Conf`/`Remv`, log brut archivé | **OUI** | `10` §1 et §4.1 |
+| Statut `ok/updates_available/warning/error` ; warning si remove/held | **OUI** | `10` §4.1 fin de section |
+| Sources APT citées (4 URLs) | **OUI** | `10` §1, toutes présentes |
+| Distingue `os_family` et `machine_kind` | **OUI** | `60` §1 |
+| Choix manuel OS (5 valeurs) | **OUI** | `60` §6 |
+| Choix manuel type machine (6 valeurs) | **OUI** | `60` §6 |
+| `machine_probe` détecte/corrige (jamais auto) | **OUI** | `60` §6 + pseudo-shell probe |
+| Scripts dépendent du couple OS/type | **OUI** | `60` §5 tableau |
+| Debian vérifie contrib/non-free/non-free-firmware | **OUI** | `60` §4 + `10` §5 |
+| Proxmox = profil dédié | **OUI** | `60` §4 et §2 (dossier `templates/proxmox/`) |
+| Scripts hardware/drivers/benchmark jamais par défaut | **OUI** | `60` §5 note + `30` §2 note |
+| Templates APT attendus (7) | **OUI** | `10` §2 tableau (tous 7 listés avec pseudo-shell ou référence) |
+| Politique non interactive (`DEBIAN_FRONTEND=noninteractive`, `-y`, confdef/confold) | **OUI** | `10` §4.2 et `50` §2 |
+| Justification confdef/confold | **OUI** | `10` §4.2 (conserver config locale) |
+| Prompts = risques de blocage, pas dialogues | **OUI** | `50` §2 |
+| Timeout inactivité/global → `human_interaction_required` | **OUI** | `50` §2 |
+| `human_interaction_required` prévu (pas d'auto-réparation) | **OUI** | `50` §2 + `50` §1 (principe) |
+| Pas seulement exit code | **OUI** | `50` §1 (principe n°1) + `10` §4.2 |
+| `dpkg-query -W -f=...` before/after | **OUI** | `10` §4.2, `40` §3 Q4 |
+| Diff backend (maj/install/remove/inchangé/versions/anomalies) | **OUI** | `40` §4 `AptExecutionResult` |
+| `ExecutionResult.apt` (planned/applied/installed/removed/held/errors/rebootRequiredAfterRun) | **OUI** | `40` §4 `AptExecutionResult` |
+| Rapport Markdown résume diff + réf log | **OUI** | `70` §4, `40` §8 |
+| Reboot vérifié (boot_id avant/après, attente, reconnexion) | **OUI** | `10` §4.5 (pseudo-shell + orchestration backend) |
+| Reboot ok si machine revient ET boot_id changé | **OUI** | `10` §4.5 (expliqué en clair) |
+| `RebootResult` (beforeBootId…status/errors) | **OUI** | `40` §4 interface `RebootResult` (8 champs requis + optionnels) |
+| Délai adaptatif par machine (`lastRebootDurationSeconds`, `nextRecommendedWaitSeconds`) | **OUI** | `10` §4.5 + `40` §4 `RebootResult` |
+| Statuts d'échec reboot distingués (5 valeurs) | **OUI** | `40` §4 union `RebootResult.status` |
+| Reboot = validation UI ; Hermes ne déclenche pas | **OUI** | `70` §2 tableau |
+| `apt_update_analyze` alimente snapshot + tuile | **OUI** | `10` §6 |
+| Actions via même route + table `executions` | **OUI** | `20` §6, `10` §6 |
+| UI avant exécution (paquets/suppressions/held/reboot/risque) | **OUI** | `70` §2 + `40` §3 (données disponibles) ; rendu JSX renvoyé à tâche 3 |
+| UI après exécution (réussite/diff/reboot/rapport/log) | **OUI** | `70` §4 ; rendu JSX renvoyé à tâche 3 |
+| Confirmation UI pour dist/full/autoremove/reboot | **OUI** | `70` §2 tableau |
+| Nouveaux champs/actions rétrocompatibles | **OUI** | `40` §1 règles d'extension |
+
+**Conclusion §7 : toutes les cases APT/reboot confirmées par lecture directe. Aucun manque.**
+
+---
+
+### Vérification §8 — Focus scripts post-install
+
+| Case §8 | Couvert ? | Détail |
+|---|---|---|
+| Interdit questions interactives SSH → champs formulaire | **OUI** | `30` §1 (interdiction stricte) |
+| Profils cochables dépliant leurs champs | **OUI** | `30` §1 |
+| Manifeste (id/label/description/fields/défauts/validations/preview/risk/confirmations) | **OUI** | `30` §1 + §3 (exemple JSON complet) |
+| Bouton désactivé si champs invalides | **OUI** | `30` §1 |
+| Preview avec masquage secrets + signalement réseau/reboot | **OUI** | `30` §1, `70` §1 |
+| Échec structuré si décision manquante | **OUI** | `30` §1 et §4 |
+| 8 profils attendus (bootstrap_root → vm_guest_tools + optionnels) | **OUI** | `30` §2 tableau (9 profils principaux + optionnels listés) |
+| Champs `identity_network` (7 champs) | **OUI** | `30` §3 + exemple manifeste JSON |
+| Champs `docker_official` (4 champs) | **OUI** | `30` §3 |
+| Champs `sharing` | **OUI** | `30` §3 |
+| Champs `vm_guest_tools` | **OUI** | `30` §3 |
+| Champs préremplis depuis machine, modifiables | **OUI** | `30` §3 (dernière phrase) |
+| Exemple de manifeste JSON | **OUI** | `30` §3 (manifeste `identity_network` complet, identique au gate) |
+| Templates custom attendus (6) | **OUI** | `30` §4 (pseudo-shell pour bootstrap, identity, install-pkg-groups, docker-official ; sharing et vm-guest-tools en prose avec référence tâche 4) |
+| Sources citées (5 URLs) | **OUI** | `30` §4 fin |
+| `identity_network` à risque (confirmation/preview/sauvegardes) | **OUI** | `30` §4.2, `70` §2 |
+| Résultat JSON old/new endpoint + reconnectHost | **OUI** | `40` §4 `PostInstallResult.networkChange` |
+| Pas de coupure sans stratégie reconnexion ; reboot via reboot_verified | **OUI** | `30` §4.2 |
+| Webapp vérifie reconnexion + met à jour machine | **OUI** | `30` §4.2 |
+| Erreurs réseau distinguées (6 codes) | **OUI** | `50` §4 |
+| `ExecutionResult.postInstall` rétrocompatible | **OUI** | `40` §4 `PostInstallResult`, bloc optionnel |
+| Résultat liste profils/variables non sensibles/fichiers/paquets/services/reboots/erreurs | **OUI** | `40` §4 `PostInstallResult` (7 champs) |
+| Secrets jamais inclus | **OUI** | `30` §5, `70` §1 |
+| Changements réseau/Docker marqués dans rapport Markdown | **OUI** | `30` §5, `70` §4 |
+| Même mécanique (templates/preview/SSH/WS/executions/rapport/log) | **OUI** | `30` §6 |
+| Valeurs réutilisables stockées (où) | **OUI** | `30` §6 (`script_variables_presets`, `machine_profile_state`) |
+| Hermes propose/explique, JSON réduit, pas de déclenchement risqué | **OUI** | `30` §6, `70` §2/§3 |
+| Profils découpés en sous-jalons indépendants | **OUI** | `80` SJ-8 et SJ-9 |
+
+**Conclusion §8 : toutes les cases post-install confirmées par lecture directe. Aucun manque.**
+
+---
+
+### Réserves (non bloquantes pour le design)
+
+**Réserve 1 — Absence de `===SU:EXIT=N===` dans `reboot.sh.tpl` : comportement intentionnel, cohérent avec le jalon 1.**
+Le pseudo-shell de `10-templates-apt.md §4.5` ne se termine pas par `===SU:EXIT=N===`. Cela est conforme au `reboot.sh.tpl` existant en prod (jalon 1, vérifié), qui n'en contient pas non plus : le reboot est planifié en arrière-plan (`nohup sleep 2; reboot`) et la connexion SSH se ferme proprement avant qu'un EXIT puisse être émis de manière fiable. Ce comportement est donc intentionnel et documenté en prose dans §4.5. **Ce n'est pas une coquille.** Réserve levée.
+
+**Réserve 2 — Réduction Hermes (`aptReduce.ts`) : renommage suggéré `reduceLines.ts` à confirmer.**
+Le design propose de renommer `aptReduce.ts` en `reduceLines.ts` tout en conservant `reduceAptLines` comme export. L'auto-évaluation dit « additif, sans casser `reduceAptLines` ». Correct sur le fond, mais le renommage de fichier implique de mettre à jour les imports existants — à vérifier que `server/templates/render.ts` ou tout autre consommateur est mis à jour dans SJ-0. Non bloquant pour le design, à tracer dans le plan SJ-0.
+
+**Réserve 3 — Contexte partagé : `shared/types.ts` modifié par jalon 2 concurrent.**
+La modification est imputable au jalon 2, pas à la tâche 2. Elle n'introduit aucune rupture pour les extensions prévues par la tâche 2. À surveiller : s'assurer que SJ-0 s'appuie sur l'état committé de `shared/types.ts` et non sur le diff non committé.
+
+---
+
+### Coquilles corrigées dans les livrables
+
+Aucune coquille textuelle ou factuelle identifiée dans les 11 fichiers de design. L'absence de `===SU:EXIT=N===` dans `reboot.sh.tpl` était un faux positif : comportement intentionnel cohérent avec le jalon 1 (voir réserve 1 levée ci-dessus). L'auto-évaluation `99-couverture-gate.md` est globalement correcte — les trois réserves résiduelles qu'elle liste (`§4 non-régression`, `UI JSX → tâche 3`, `délimiteurs Mustache/Docker`) sont légitimes et non bloquantes.
+
+---
+
+### Écarts avec l'auto-évaluation du producteur
+
+L'auto-évaluation coche tout en ✅ sauf les trois réserves résiduelles. La présente revue indépendante confirme intégralement ce tableau. Aucun manque réel n'a été identifié que l'auto-évaluation aurait raté. Un faux positif initial (marqueur EXIT de reboot) a été levé après vérification du template existant en prod.
+
+---
+
+### Actions de suite
+
+1. **Lancer `rtk pnpm check && rtk pnpm test && rtk pnpm build`** pour confirmer la non-régression formelle (§4 ; aucun code de prod touché par la tâche 2, résultat attendu vert).
+2. **Procéder à `writing-plans` pour SJ-0** (socle types/réduction/résolution) dès que la non-régression est confirmée.
+3. Tracer dans le plan SJ-0 la mise à jour des imports consommateurs si `aptReduce.ts` est renommé en `reduceLines.ts`.
+4. S'assurer que SJ-0 s'appuie sur l'état **committé** de `shared/types.ts` (le diff non committé provient du jalon 2 concurrent — à intégrer ou isoler avant de démarrer SJ-0).
diff --git a/validation_tache3.md b/validation_tache3.md
new file mode 100644
index 0000000..46b16c8
--- /dev/null
+++ b/validation_tache3.md
@@ -0,0 +1,223 @@
+# Protocole de validation — Tâche 3 (frontend web, tuiles, layout, paramètres)
+
+> **Type** : grille de validation. Utilisée après la mission décrite dans `tache3.md`.
+> **But** : vérifier que la spec frontend est complète, respecte le design system, et s'intègre au backend/API sans inventer de logique métier côté client.
+> **Gate obligatoire** : cette validation doit être passée avant implémentation frontend majeure.
+> **Rappel** : la tâche 3 est une mission design UX/UI, pas d'implémentation.
+
+---
+
+## 0. Quand lancer cette validation
+
+- La mission `tache3.md` est annoncée terminée.
+- Les maquettes ASCII et specs frontend sont présentes.
+- `consigne_icon.md` existe si des icônes spécifiques sont demandées.
+
+---
+
+## 1. Discipline & périmètre
+
+- [ ] Aucun code de production frontend modifié dans cette mission de design.
+- [ ] Le design respecte `design_system/consigne_design_system.md`.
+- [ ] Les icônes passent d'abord par le `ui-kit`/Font Awesome.
+- [ ] Les SVG spécifiques sont listés dans `consigne_icon.md`, pas improvisés dans le JSX.
+- [ ] Aucun secret n'est affiché ou stocké côté navigateur.
+
+---
+
+## 2. Layout global webapp
+
+- [ ] Vue globale header/volet Hermes/centre/terminal/footer définie.
+- [ ] Le volet Hermes ne masque pas la zone centrale.
+- [ ] Le terminal droit ne masque pas les tuiles.
+- [ ] Les dimensions sont bornées, redimensionnables et responsive.
+- [ ] Le footer contient les métriques compactes attendues.
+- [ ] Le mode smartphone est traité comme une UX dédiée, pas comme trois colonnes compressées.
+
+---
+
+## 3. Tuiles machine
+
+- [ ] État compact défini.
+- [ ] État Docker ouvert défini.
+- [ ] État Post-install ouvert défini.
+- [ ] État machine physique/Proxmox/hardware défini.
+- [ ] OS et type machine sont visibles.
+- [ ] APT, Docker, Post-install, Hardware, Health et Alerts sont représentés.
+- [ ] Les actions dangereuses sont clairement distinguées et validables.
+- [ ] La tuile s'agrandit sans casser la grille.
+
+---
+
+## 4. Paramètres frontend
+
+- [ ] Vue onglet Paramètres définie.
+- [ ] Paramètres d'apparence, tuiles, volets, Docker, scripts, Hermes, terminal SSH et nettoyage présents.
+- [ ] Les paramètres persistants sont destinés au backend/BDD, pas uniquement `localStorage`.
+- [ ] Les paramètres app icons/favicon/PWA sont listés.
+
+---
+
+## 5. Hermes et terminal
+
+- [ ] Discussion Hermes lisible : messages utilisateur/Hermes/système distingués.
+- [ ] Copier-coller natif et boutons copier prévus.
+- [ ] Blocs de commandes copiables mais non exécutés automatiquement.
+- [ ] Cartes de validation séparées du texte Hermes.
+- [ ] Terminal logs d'exécution distinct du vrai terminal SSH interactif.
+- [ ] Terminal SSH interactif désactivable et clairement risqué.
+
+---
+
+## 6. Icônes et assets
+
+- [ ] `favicon.svg`, `favicon.ico`, `apple-touch-icon.png`, icônes PWA sont spécifiés.
+- [ ] Liste d'alias `ICON_MAP` à ajouter/vérifier.
+- [ ] Liste d'icônes SVG custom candidates priorisée.
+- [ ] Le style respecte Gruvbox seventies.
+- [ ] Pas de logo propriétaire copié.
+
+---
+
+## 7. Intégration JSON/API
+
+- [ ] Le frontend consomme uniquement JSON canonique/API.
+- [ ] Le frontend ne parse jamais les logs bruts pour calculer un état.
+- [ ] Les références rapports/logs sont affichées par liens/actions.
+- [ ] Les erreurs structurées sont affichables sans ouvrir le terminal.
+
+---
+
+## 8. Verdict
+
+- **✅ Accepté** : spec UI complète et implémentable.
+- **🟡 Accepté avec réserves** : points UX mineurs à clarifier.
+- **❌ Rejeté** : design incomplet, non conforme au DS, ou logique métier côté client.
+
+---
+
+## 9. Notes de validation
+
+> Date : 2026-06-05. Relecteur : orchestrateur, validation déléguée.
+
+---
+
+### Verdict global
+
+**Accepté avec réserves (🟡)**
+
+La spec `tache3.md` couvre l'essentiel du périmètre attendu : layout global, tuiles machine à plusieurs états, paramètres frontend, volet Hermes, terminal, icônes/favicon/PWA, intégration JSON/API. Les règles du design system sont explicitement rappelées et la distinction terminal de logs vs SSH interactif est traitée. Quelques points nécessitent une clarification ou un complément avant implémentation.
+
+---
+
+### Section 1 — Discipline et périmètre
+
+- [x] Aucun code de production modifié : la tâche est déclarée design-only et aucune modification de code n'est demandée.
+- [x] Respect du DS formellement exigé : section 2 liste toutes les règles (variables CSS, ui-kit, ``/``, pas de hover sauf jauges, tooltips, polices, Popup, labels uppercase).
+- [x] Icônes par `ui-kit`/Font Awesome en premier ; SVG custom uniquement pour les concepts manquants listés.
+- [x] SVG spécifiques délégués à `consigne_icon.md` (lien présent en §3).
+- [x] Aucun secret affiché côté navigateur : règle explicite en §9 (terminal) et §10 (JSON). Masquage backend exigé avant diffusion UI.
+
+**Note :** le ui-kit actuel (`ui-kit.tsx`) n'expose ni composant `Checkbox` ni composant `Select`/`Dropdown` ni `Input`. La spec §8 demande des « vraies cases à cocher du design system » (`Toggle` ou checkbox stylée) et les maquettes Post-install montrent des `