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 `` / ``** non présents dans le ui-kit actuel, alors que les maquettes montrent des menus déroulants (OS, type machine, interface réseau). À créer ou à utiliser un `