docs: roadmap tâches 1.9-8 (briefs, gates de validation, designs tâche 2) + plans d'implémentation
Cartographie complète (liste_taches/coherence_taches), briefs tacheN + gates validation_tacheN, design tâche 2 (docs/design/tache2/), specs/plans jalon 1-2 et tâche 1.9/2 (Phase 1, Phase 2, SJ-0→3). Validations consignées (1.9 ✅, 2-8 🟡). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||||
@@ -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
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="..." stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -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/<famille>/<commande>.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.
|
||||||
@@ -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 <pkg> [<cur>] (<target> <origin> [<arch>])`, `Conf <pkg>`, `Remv <pkg>`. 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/<profil>/<commande>.sh.tpl` est choisi, sinon fallback `templates/apt/<commande>.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).
|
||||||
@@ -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`).
|
||||||
@@ -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`).
|
||||||
@@ -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<string, string | number | boolean>; // 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`).
|
||||||
@@ -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`).
|
||||||
@@ -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/<osFamily>/<action>.sh.tpl
|
||||||
|
if exists(candidate): return candidate
|
||||||
|
return templates/apt/<action>.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 "<url>";` 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.
|
||||||
@@ -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.
|
||||||
@@ -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`.
|
||||||
@@ -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/<osFamily>/<action>.sh.tpl` → sinon `templates/apt/<action>.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`.
|
||||||
@@ -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é**.
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# Jalon 2 — Polish design system — Implementation Plan
|
# 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.
|
> **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.
|
**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.
|
||||||
|
|||||||
@@ -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<Omit<MachineStateInsert, "machineId" | "updatedAt" | "status">> & { 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.
|
||||||
@@ -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.
|
||||||
@@ -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<string, string | number | boolean>;
|
||||||
|
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) :
|
||||||
|
* `<osFamily>/<action>.sh.tpl` s'il existe, sinon fallback base `apt/<action>.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.
|
||||||
@@ -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.
|
||||||
@@ -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<string, { version: string; arch: string }> {
|
||||||
|
const out: Record<string, { version: string; arch: string }> = {};
|
||||||
|
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<RunResult> {
|
||||||
|
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<Record<ActionType, string>> = {
|
||||||
|
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.
|
||||||
@@ -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<string | null> {
|
||||||
|
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<RebootResult> {
|
||||||
|
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).
|
||||||
+148
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
+1335
File diff suppressed because it is too large
Load Diff
@@ -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`.
|
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.
|
- 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 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/`.
|
- 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)
|
### 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.
|
- **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).
|
- **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.
|
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/`.
|
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 ?).
|
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. **Contrats JSON** : quelles extensions exactes à `UpdateSnapshot` et `ExecutionResult` (`shared/types.ts`) pour Docker, erreurs, scripts custom ? Proposer les types.
|
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. **Idempotence & opérations longues** : généraliser l'exécution détachée (`nohup` + exit-code) ? Comment suivre la progression et reprendre ?
|
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. **Sécurité Docker `prune` / scripts custom** : où placer la barrière de validation, comment éviter toute fuite de secret vers Hermes/MCP.
|
6. **Idempotence & opérations longues** : généraliser l'exécution détachée (`nohup` + exit-code) ? Comment suivre la progression et reprendre ?
|
||||||
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.
|
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.
|
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.
|
4. **Taxonomie des erreurs** + stratégie de gestion et de remédiation.
|
||||||
5. **Modèle des profils OS et des overrides par machine.**
|
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.
|
6. **Modèle des profils machine** : physique, VM, hôte Proxmox, LXC, Raspberry Pi, serveur GPU, workstation.
|
||||||
7. **Note de sécurité** : ce qui ne doit jamais atteindre Hermes/MCP, validations requises pour les actions destructives.
|
7. **Modèle des scripts personnalisés** (post-install, install paquets, hardware, drivers, métriques) avec garde-fous.
|
||||||
8. **Découpage en sous-jalons** implémentables indépendamment (chacun = un cycle spec → plan → implémentation), avec ordre recommandé.
|
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
|
## 6. Définition de « terminé » pour cette mission
|
||||||
|
|
||||||
- Tous les axes A→E couverts par un design argumenté.
|
- 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.
|
- 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`.
|
- 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é).
|
- 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).
|
- 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).
|
> **É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/<osFamily>/`, **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`.
|
||||||
|
|||||||
@@ -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 `<Icon>` / `<IconButton>`, 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 `<Popup>`.
|
||||||
|
|
||||||
|
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`.
|
||||||
@@ -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 <user>`
|
||||||
|
- 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`.
|
||||||
@@ -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/<machineId>/...log` ou dans un stockage d'artefacts ;
|
||||||
|
- le rapport Markdown reste dans `reports/<machineId>/...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`.
|
||||||
@@ -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`.
|
||||||
@@ -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`.
|
||||||
@@ -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`.
|
||||||
@@ -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.
|
||||||
+537
-3
@@ -48,7 +48,7 @@ Confronter au § « Livrables attendus » et aux 5 axes de `tache2.md` :
|
|||||||
- [ ] Découpage en sous-jalons priorisé.
|
- [ ] Découpage en sous-jalons priorisé.
|
||||||
|
|
||||||
**Questions d'investigation (§3) :**
|
**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**.
|
> 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)
|
## 6. Focus de validation — scripts Docker Compose et intégration webapp
|
||||||
> _(Section laissée vide ; à compléter lors de la validation effective : date, verdict, points relevés, décisions.)_
|
|
||||||
|
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).
|
||||||
|
|||||||
@@ -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, `<Icon>`/`<IconButton>`, 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 `<select>` (ex. Interface `ens18 ▼`). `Toggle` est disponible dans le ui-kit mais ne couvre pas exactement la sémantique d'une case à cocher de profil. Ce point est une réserve mineure : l'agent d'implémentation devra soit utiliser `Toggle`, soit créer un composant `Checkbox` dans le ui-kit (avec accord préalable).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Section 2 — Layout global webapp
|
||||||
|
|
||||||
|
- [x] Vue globale ASCII définie en §4 (header / volet Hermes / centre / terminal / footer).
|
||||||
|
- [x] Volet Hermes : ne masque pas la zone centrale — explicitée en §4 (contrainte).
|
||||||
|
- [x] Terminal droit : ne recouvre pas les tuiles — explicitée en §4 (contrainte).
|
||||||
|
- [x] Largeurs redimensionnables et bornées : mentionné en §4 ; valeurs cibles (px min/max) renvoyées aux paramètres §9 (Hermes 280px, Terminal 420px — visibles dans la maquette paramètres). Les valeurs précises min/max ne sont pas formalisées dans un tableau, mais les tokens DS (`consigne_design_system.md` §tailles) couvrent Sidebar 200-260px et Volet logs 320-360px — légère divergence avec les 280/420px de la maquette. Réserve mineure.
|
||||||
|
- [x] Footer avec métriques compactes : présent dans la maquette ASCII §4 (`mode · machines · apt · docker · jobs · cpu · ram · db · hermes · ts`). La spec détaillée du footer est renvoyée à `tache7.md` — acceptable car explicitement référencé.
|
||||||
|
- [x] Mode smartphone traité comme UX dédiée (§9, mode smartphone) : onglets / bottom bar explicites, liste de questions à trancher, proposition MVP mobile. La spec mobile complète (breakpoints, composants, interactions) est demandée comme livrable dédié — c'est une réserve attendue et déclarée.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Section 3 — Tuiles machine
|
||||||
|
|
||||||
|
- [x] État compact défini (§5, maquette ASCII détaillée).
|
||||||
|
- [x] État Docker ouvert défini (§5, état Docker déplié).
|
||||||
|
- [x] État Post-install ouvert défini (§5, état Post-install déplié).
|
||||||
|
- [x] État machine physique/Proxmox/hardware défini (§5, état complet hôte physique / Proxmox).
|
||||||
|
- [x] OS et type machine visibles (§5, badge `debian · vm · amd64 · qemu` et champs ajout machine).
|
||||||
|
- [x] APT, Docker, Post-install, Hardware et Alerts représentés. Health (CPU/RAM/disk) présent dans les maquettes compactes et Proxmox.
|
||||||
|
- [x] Actions dangereuses identifiées (upgrade, full-upgrade, reboot, prune mode agressif demandent confirmation — §6, §7). `<Popup>` exigé à la place de `window.alert`.
|
||||||
|
- [x] Agrandissement de tuile sans casser la grille : `grid-column: 1 / -1` pour la variante expanded mentionné en §1.
|
||||||
|
|
||||||
|
**Réserve mineure :** l'état « erreur machine / machine hors ligne / machine déconnectée » (statut `error` ou `unknown`) est listé dans §6 mais aucune maquette ASCII ne le montre de façon dédiée. Le livrable §11 point 2 demande explicitement un état `erreur` — ce sera à produire dans le doc de design final.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Section 4 — Paramètres frontend
|
||||||
|
|
||||||
|
- [x] Vue onglet Paramètres définie (maquette ASCII §9 : nav gauche + panneau droit avec catégories).
|
||||||
|
- [x] Catégories minimales présentes : Général/Apparence, Machines, Docker, Scripts, Hermes/MCP, Terminal SSH, Nettoyage, Sécurité, Découverte, Icônes/application.
|
||||||
|
- [x] Persistance backend/BDD explicitée : §9 « les paramètres frontend persistants doivent être sauvegardés côté backend/BDD » + règle complémentaire localStorage uniquement comme cache. Cohérent avec `tache1.9.md` tables `app_settings` / `user_preferences` / `machine_ui_state`.
|
||||||
|
- [x] Paramètres app icons/favicon/PWA listés (§9, maquette, catégorie Icônes/application).
|
||||||
|
|
||||||
|
**Note :** la maquette paramètres affiche `[save] [close]` dans le header, ce qui implique un mécanisme de sauvegarde groupée. La spec ne détaille pas si les paramètres sont persistés à chaque changement (auto-save) ou à la validation. Ce point devra être tranché à l'implémentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Section 5 — Hermes et terminal
|
||||||
|
|
||||||
|
- [x] Discussion Hermes : messages utilisateur/Hermes/système distingués (§9, volet Hermes). Horodatage, état streaming, saisie multi-ligne définis.
|
||||||
|
- [x] Copier-coller natif + boutons copier prévus (§9, volet Hermes).
|
||||||
|
- [x] Blocs de commandes copiables mais non exécutés automatiquement : règle explicite (§9, volet Hermes — « une commande copiée ne doit pas être exécutée automatiquement »).
|
||||||
|
- [x] Cartes de validation séparées du texte Hermes : « actions proposées affichées comme cartes séparées, jamais comme texte ambigu » (§9).
|
||||||
|
- [x] Terminal logs d'exécution distinct du terminal SSH interactif : deux modes définis (§9, terminal volet droit), avec règle de masquage des secrets.
|
||||||
|
- [x] Terminal SSH interactif désactivable : bouton explicite, paramètre global d'activation/désactivation (§9).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Section 6 — Icônes et assets
|
||||||
|
|
||||||
|
- [x] `favicon.svg`, `favicon.ico`, `apple-touch-icon.png`, icônes PWA (`192x192`, `512x512`, masquable) spécifiés (§3).
|
||||||
|
- [x] Liste d'alias `ICON_MAP` à ajouter : §3 fournit la liste complète en catégories (navigation, actions système, machine/profil, APT/Docker/post-install, sécurité/états).
|
||||||
|
- [x] Icônes SVG custom candidates priorisées : `consigne_icon.md` §6 liste priorité haute/moyenne/basse.
|
||||||
|
- [x] Style Gruvbox seventies : défini comme contrainte principale.
|
||||||
|
- [x] Pas de logo propriétaire copié (§3 direction visuelle).
|
||||||
|
|
||||||
|
**Réserve — incohérence inter-docs :** `tache3.md` §3 dit « éviter les logos de distributions ou marques propriétaires » tandis que `consigne_icon.md` §2 dit « on peut utiliser des logos officiels de distributions, Docker, Proxmox, Raspberry Pi, NVIDIA, etc. ». Ces deux directives sont contradictoires. La `consigne_icon.md` est le brief de référence pour l'agent SVG : c'est elle qui fait foi pour la création des icônes. Mais la phrase de `tache3.md` sur la direction visuelle du **favicon/app icon** peut rester cohérente (le favicon principal est une icône maison, pas un logo redistribué). Il faudra clarifier dans la prochaine révision de `tache3.md` : les logos officiels sont admis pour les **icônes de type de machine** (proxmox-host, raspberry-pi, docker), mais pas pour le **favicon ou l'app icon** qui doit être une création originale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Section 7 — Intégration JSON/API
|
||||||
|
|
||||||
|
- [x] Frontend consomme uniquement JSON canonique : règle explicite en §10 et en §12 (Définition de terminé).
|
||||||
|
- [x] Frontend ne parse jamais les logs bruts : règle explicite en §10 (« le frontend ne doit jamais parser du log brut pour décider l'état d'une tuile »).
|
||||||
|
- [x] Références rapports/logs affichées par liens/actions : mentionné en §9 (terminal — « lien rapport/log brut après fin d'exécution »).
|
||||||
|
- [x] Erreurs structurées lisibles sans ouvrir le terminal : règle explicite en §6 (« les erreurs doivent être lisibles directement dans la tuile sans ouvrir le terminal »).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Cohérence avec le jalon 2 (polish DS absorbé par tâche 3)
|
||||||
|
|
||||||
|
Le wiring DS (exports ESM ui-kit + Font Awesome + polices) est déjà commité selon `2026-06-05-jalon2-polish-design-system.md`. La tâche 3 ne le contredit pas et le prend comme acquis (§13 checklist et §2 contraintes DS). La spec tâche 3 est cohérente avec ce qui a été livré : elle exige les mêmes composants (`<Icon>`, `<IconButton>`, `<StatusLed>`, `<Popup>`, `<Button>`, `<Toggle>`) et les mêmes règles (pas de hover, tooltips, deux thèmes). Pas de contradiction.
|
||||||
|
|
||||||
|
**Note :** l'état actuel de `MachineTile.tsx` utilise encore des `<button className="interactive">` bruts et une pastille inline (pas `<StatusLed>`). La tâche 3 est une spec design, pas d'implémentation — ce delta est attendu et sera traité lors de l'implémentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Réserves majeures
|
||||||
|
|
||||||
|
Aucune réserve bloquante ne justifie un rejet. Les points ci-dessous sont des réserves à traiter avant ou pendant l'implémentation :
|
||||||
|
|
||||||
|
1. **Incohérence logos officiels** (`tache3.md` §3 vs `consigne_icon.md` §2) : à clarifier formellement. Suggestion : autoriser les logos officiels uniquement pour les icônes de type de machine ; le favicon/app icon reste une création originale.
|
||||||
|
2. **Composant Checkbox absent du ui-kit** : `Toggle` couvre on/off, mais les cases à cocher des profils Post-install ont une sémantique différente (sélection d'un profil dans une liste, pas un toggle binaire global). L'agent d'implémentation devra arbitrer ou créer un composant `Checkbox` ds-compliant.
|
||||||
|
3. **Spec mobile incomplète** : la spec déclare explicitement qu'elle doit être produite séparément (breakpoints, composants, interactions). Acceptable comme livrable différé — mais doit être produite avant implémentation du mode smartphone.
|
||||||
|
4. **Largeurs min/max des volets** : les valeurs cibles (280px Hermes, 420px Terminal) dans la maquette paramètres divergent légèrement des tokens DS (Sidebar 200-260px, Volet logs 320-360px). À aligner ou à justifier.
|
||||||
|
|
||||||
|
### Réserves mineures
|
||||||
|
|
||||||
|
5. **État « machine hors ligne / erreur »** : pas de maquette ASCII dédiée, bien que le statut soit listé (voir livrable §11 point 2).
|
||||||
|
6. **Mécanisme save paramètres** (auto-save vs validation groupée) : à trancher à l'implémentation.
|
||||||
|
7. **`<Select>` / `<Dropdown>`** 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 `<select>` HTML stylé en CSS DS.
|
||||||
|
|
||||||
|
### Coquilles corrigées dans `tache3.md`
|
||||||
|
|
||||||
|
- **Lignes 351 et 501** : `10.0.4.25/22` corrigé en `10.0.4.25/24`. Le CIDR `/22` (plage 10.0.0.0–10.0.3.255) était incorrect pour une adresse en `10.0.4.x` ; `/24` est le masque attendu pour l'exemple.
|
||||||
|
|
||||||
|
### Coquille signalée (non corrigée — dans bloc ASCII illustratif)
|
||||||
|
|
||||||
|
- **Ligne 330** : emoji `🧹` dans le bloc ASCII maquette « État Docker déplié ». Le document lui-même précise que « le rendu final doit utiliser des icônes Font Awesome via le design system, pas les symboles ASCII ci-dessus ». Cet emoji est donc toléré comme raccourci illustratif dans une maquette ASCII. L'alias `prune` (icône DS correspondante) est bien listé en §3. Aucune correction appliquée.
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
# Protocole de validation — Tâche 4 (scripts post-install et installateurs)
|
||||||
|
|
||||||
|
> **Type** : grille de validation. Utilisée après la mission décrite dans `tache4.md`.
|
||||||
|
> **But** : vérifier que les profils/scripts post-install sont complets, non interactifs, sûrs, versionnables et compatibles JSON.
|
||||||
|
> **Gate obligatoire** : cette validation doit être passée avant implémentation de nouveaux scripts.
|
||||||
|
> **Rappel** : la tâche 4 est une mission design scripts/contrats, pas d'implémentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Quand lancer cette validation
|
||||||
|
|
||||||
|
- La mission `tache4.md` est annoncée terminée.
|
||||||
|
- Le catalogue de profils/scripts est produit.
|
||||||
|
- Les JSON d'entrée/sortie sont spécifiés.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Discipline & périmètre
|
||||||
|
|
||||||
|
- [ ] Aucun script de production ajouté/modifié.
|
||||||
|
- [ ] Aucun `curl | sh` opaque validé sans justification.
|
||||||
|
- [ ] Aucun secret en clair dans variables, logs, rapports ou MCP.
|
||||||
|
- [ ] Les scripts sont prévus en templates versionnés sur disque.
|
||||||
|
- [ ] Tout script est non interactif.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Profils minimum
|
||||||
|
|
||||||
|
- [ ] `bootstrap_root`.
|
||||||
|
- [ ] `identity_network`.
|
||||||
|
- [ ] `base_tools` sans `vim`.
|
||||||
|
- [ ] `network_tools` avec `nmap` classé admin local contrôlé.
|
||||||
|
- [ ] `dev_git`.
|
||||||
|
- [ ] `sharing` avec Samba/NFS/mDNS/wsdd2.
|
||||||
|
- [ ] `docker_official`.
|
||||||
|
- [ ] `home_automation`.
|
||||||
|
- [ ] `dev_tools`.
|
||||||
|
- [ ] `embedded_esp_platformio`.
|
||||||
|
- [ ] `terminal_customization`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Hardware, OS et métriques
|
||||||
|
|
||||||
|
- [ ] `machine_probe` spécifié.
|
||||||
|
- [ ] `machine_metrics_simple` spécifié.
|
||||||
|
- [ ] `apt_repositories` spécifié.
|
||||||
|
- [ ] `firmware_tools` spécifié.
|
||||||
|
- [ ] `gpu_drivers` spécifié.
|
||||||
|
- [ ] `benchmark_tools` spécifié.
|
||||||
|
- [ ] Les scripts dépendent du couple OS/type machine.
|
||||||
|
- [ ] Les drivers/firmware/benchmark ne sont pas installés par défaut.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Manifestes et champs UI
|
||||||
|
|
||||||
|
- [ ] Chaque profil possède id, label, description, risque, fields, defaults, validations.
|
||||||
|
- [ ] Cocher un profil déplie les champs obligatoires.
|
||||||
|
- [ ] Le bouton run reste désactivé si les champs requis sont invalides.
|
||||||
|
- [ ] Preview template prévue avec masquage secrets.
|
||||||
|
- [ ] Les décisions manquantes produisent `human_interaction_required`, pas un blocage SSH.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Cycle d'exécution
|
||||||
|
|
||||||
|
- [ ] `precheck`.
|
||||||
|
- [ ] `install`.
|
||||||
|
- [ ] `configure`.
|
||||||
|
- [ ] `initialize`.
|
||||||
|
- [ ] `verify`.
|
||||||
|
- [ ] `report JSON`.
|
||||||
|
- [ ] Les sauvegardes de fichiers système sont prévues avant modification.
|
||||||
|
- [ ] Reboot/reconnexion sont signalés et intégrés à `reboot_verified`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. JSON et erreurs
|
||||||
|
|
||||||
|
- [ ] JSON d'entrée défini.
|
||||||
|
- [ ] JSON résultat défini.
|
||||||
|
- [ ] Liste paquets/services/fichiers modifiés incluse.
|
||||||
|
- [ ] Taxonomie erreurs présente.
|
||||||
|
- [ ] Les logs bruts sont archivés, les lignes importantes réduites.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Verdict
|
||||||
|
|
||||||
|
- **✅ Accepté** : scripts/profils prêts à planifier.
|
||||||
|
- **🟡 Accepté avec réserves** : profils à compléter.
|
||||||
|
- **❌ Rejeté** : interactivité SSH, secrets, risques non validés ou JSON absent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Notes de validation
|
||||||
|
|
||||||
|
**Date** : 2026-06-05 — Gilles Soulier (orchestrateur, validation déléguée)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Verdict global : 🟡 Accepté avec réserves
|
||||||
|
|
||||||
|
Le design est solide dans ses fondements : non-interactivité, JSON canonique, profils minimum, cycle precheck/install/configure/initialize/verify/report, taxonomie d'erreurs, secrets exclus. Les réserves ci-dessous sont mineures et n'invalident pas la mise en planification, sous réserve qu'elles soient traitées avant ou pendant l'implémentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Résultat case par case
|
||||||
|
|
||||||
|
#### Section 1 — Discipline & périmètre
|
||||||
|
|
||||||
|
- [x] Aucun script de production ajouté/modifié. Confirmé : la tâche est design seul.
|
||||||
|
- [x] Aucun `curl | sh` opaque validé sans justification. §6 exige URL officielle, checksum si disponible, confirmation. **Note** : `rustup`, `nvm`, `uv` sont pré-autorisés par leur liste en §14 — ce statut devrait être rendu explicite dans la spec (note « installateur pré-autorisé justifié »).
|
||||||
|
- [x] Aucun secret en clair. §6 + §12 clairs.
|
||||||
|
- [x] Templates versionnés sur disque. §6 + §13 checklist.
|
||||||
|
- [x] Tout script non interactif. §2 + §13.
|
||||||
|
|
||||||
|
#### Section 2 — Profils minimum
|
||||||
|
|
||||||
|
- [x] `bootstrap_root` — §3.
|
||||||
|
- [x] `identity_network` — §3 avec variables JSON.
|
||||||
|
- [x] `base_tools` sans `vim` — §3, explicitement listé.
|
||||||
|
- [x] `network_tools` avec `nmap` classé admin local contrôlé — §3, classé "outil réseau d'administration pour découverte locale", distinction security_audit/security_lab présente.
|
||||||
|
- [x] `dev_git` — §3.
|
||||||
|
- [x] `sharing` avec Samba/NFS/mDNS/wsdd2 — §3 + §5 exemples JSON complets.
|
||||||
|
- [x] `docker_official` — §3, modélisé comme installateur externe officiel.
|
||||||
|
- [x] `home_automation` — §4.
|
||||||
|
- [x] `dev_tools` — §4.
|
||||||
|
- [x] `embedded_esp_platformio` — §4, groupes dialout/plugdev, règles udev.
|
||||||
|
- [x] `terminal_customization` — §8, variables UI + JSON retour.
|
||||||
|
|
||||||
|
#### Section 3 — Hardware, OS et métriques
|
||||||
|
|
||||||
|
- [x] `machine_probe` spécifié — §3, JSON de sortie complet avec recommendations.
|
||||||
|
- [x] `machine_metrics_simple` spécifié — §3, JSON de sortie complet, non destructif.
|
||||||
|
- [x] `apt_repositories` spécifié — §3, analyse puis action séparée validée.
|
||||||
|
- [x] `firmware_tools` spécifié — §3, règles VM/bare-metal/Proxmox.
|
||||||
|
- [x] `gpu_drivers` spécifié — §3, sous-profils nvidia/amd/intel/intel_arc, séquence en trois étapes.
|
||||||
|
- [x] `benchmark_tools` spécifié — §3, confirmation explicite, rapport JSON, optionnel.
|
||||||
|
- [x] Scripts dépendent du couple OS/type machine — §3 machine_probe détecte virtualization/raspberryPi/proxmoxHost, §13 checklist.
|
||||||
|
- [x] Drivers/firmware/benchmark non installés par défaut — §3 ("optionnels et jamais installés par défaut"), §12 définition de terminé.
|
||||||
|
|
||||||
|
#### Section 4 — Manifestes et champs UI
|
||||||
|
|
||||||
|
- [x] Chaque profil possède id, label, description, risque, fields, defaults, validations. **Coquille corrigée** : le manifeste JSON exemple §2 ne comportait pas `description`, `defaults`, `validations` — ajoutés lors de la présente revue.
|
||||||
|
- [~] Cocher un profil déplie les champs obligatoires. Comportement UI non décrit dans tache4.md. Relève principalement de la tâche 3 (formulaires) ; la tâche 4 ne fixe que les champs déclarés dans le manifeste. Acceptable dans le périmètre tâche 4.
|
||||||
|
- [~] Bouton run désactivé si champs requis invalides. Non mentionné dans tache4.md. Relève de la tâche 3. Acceptable.
|
||||||
|
- [x] Preview template avec masquage secrets. §6 "prévisualisable" + principe "sans secret en clair". La formulation explicite "masquage secrets lors de la preview" est absente mais découlée du principe général. **Réserve légère** : à formuler explicitement lors de l'implémentation.
|
||||||
|
- [x] `human_interaction_required` plutôt que blocage SSH. §10 taxonomie d'erreurs, présent.
|
||||||
|
|
||||||
|
#### Section 5 — Cycle d'exécution
|
||||||
|
|
||||||
|
- [x] `precheck` — §2 + §7.
|
||||||
|
- [x] `install` — §2 + §7.
|
||||||
|
- [x] `configure` — §2 + §7.
|
||||||
|
- [x] `initialize` — §2 + §7.
|
||||||
|
- [x] `verify` — §2 + §7.
|
||||||
|
- [x] `report JSON` — §2 + §9.
|
||||||
|
- [x] Sauvegardes de fichiers système avant modification. §6 "rollback ou sauvegarde quand un fichier système est modifié" + §8 terminal_customization `backupFiles` dans JSON retour.
|
||||||
|
- [~] Reboot/reconnexion signalés et intégrés à `reboot_verified`. `requiresReboot`/`requiresRelogin` présents dans le JSON de retour. **Réserve** : la confirmation post-reboot (`reboot_verified`) — c'est-à-dire le booléen attestant que la machine a redémarré et est de nouveau joignable — n'est pas explicitement nommée. §3 identity_network mentionne "reboot vérifié si nécessaire" mais le champ JSON correspondant est absent du modèle canonique §9. À ajouter.
|
||||||
|
|
||||||
|
#### Section 6 — JSON et erreurs
|
||||||
|
|
||||||
|
- [x] JSON d'entrée défini — §3 variables UI identity_network, §5 Samba/NFS, §8 terminal.
|
||||||
|
- [x] JSON résultat défini — §2, §3, §5, §8, §9 champs communs.
|
||||||
|
- [x] Liste paquets/services/fichiers modifiés incluse — §5 packagesInstalled/filesChanged/services, §9.
|
||||||
|
- [x] Taxonomie erreurs présente — §10, 16 codes distincts.
|
||||||
|
- [x] Logs bruts archivés, lignes importantes réduites — §9 "Le log brut reste archivé. Hermes/MCP ne reçoivent que le JSON réduit et les lignes importantes."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Réserves majeures (à traiter avant ou en début d'implémentation)
|
||||||
|
|
||||||
|
**R1 — Destination BDD des résultats machine_probe et machine_metrics_simple non spécifiée**
|
||||||
|
La tâche 4 ne mentionne pas explicitement que le JSON de sortie de `machine_probe` alimente `machine_hardware` et que `machine_metrics_simple` alimente `machine_metrics_latest` (tables définies dans tache1.9.md §9). Cette liaison doit être documentée dans les livrables sous `docs/` avant que la tâche 5 (exécution backend) ne commence à router ces résultats.
|
||||||
|
|
||||||
|
**R2 — Champ `reboot_verified` absent du JSON canonique §9**
|
||||||
|
Le JSON commun de §9 contient `requiresReboot` (intention) mais pas de champ attestant que le reboot a effectivement été vérifié. Le cycle identity_network exige "reboot vérifié" — le contrat JSON doit porter ce champ (ex. `rebootVerified: boolean | null`).
|
||||||
|
|
||||||
|
**R3 — Installateurs `curl | sh` (rustup, nvm, uv) non explicitement justifiés dans la spec**
|
||||||
|
Ces installateurs sont listés dans §4 et §14 mais la spec ne les désigne pas formellement comme "pré-autorisés avec justification documentée" conformément à la règle §6. La liste des installateurs autorisés avec leurs URLs officielles et la stratégie de vérification (checksum interne rustup-init, hash nvm/install.sh via GitHub release) devrait apparaître dans un tableau dans les livrables `docs/`.
|
||||||
|
|
||||||
|
**R4 — Masquage des secrets lors de la preview template non formulé**
|
||||||
|
Le principe "sans secret en clair" est global mais la preview de template (§6 "prévisualisable") ne précise pas comment les variables sensibles (password, token) sont masquées dans l'affichage UI. À formaliser explicitement dans la spec des installateurs externes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Réserves mineures (à noter, non bloquantes)
|
||||||
|
|
||||||
|
- Le manifeste JSON exemple §2 ne contenait pas `description`, `defaults`, `validations` — **corrigé lors de la présente revue**.
|
||||||
|
- Le backup path §8 terminal_customization comportait une date figée en dur — **corrigé lors de la présente revue** (remplacé par `{{backupDate}}`).
|
||||||
|
- `iotop` dans `base_tools` : sur certaines distributions récentes ce paquet requiert le kernel avec CONFIG_TASK_IO_ACCOUNTING ou peut s'appeler différemment. Non bloquant au niveau design mais à anticiper lors de l'implémentation avec un fallback.
|
||||||
|
- La distinction des trois niveaux nmap (network_tools / security_audit / security_lab) est bien documentée dans §3 et §4. Elle devrait être reprise dans le manifeste de chaque profil concerné (champ `risk` + note UI d'avertissement pour security_lab).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Coquilles corrigées dans tache4.md
|
||||||
|
|
||||||
|
1. **§2 manifeste JSON** : ajout de `"description"`, `"defaults": {}`, `"validations": []` pour correspondre au gate §4 qui exige ces trois champs dans chaque manifeste de profil.
|
||||||
|
2. **§8 terminal_customization, JSON retour `backupFiles`** : date figée `20260605` remplacée par le placeholder Mustache `{{backupDate}}` pour indiquer que la valeur est dynamique à l'exécution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Cohérence inter-tâches
|
||||||
|
|
||||||
|
- **Tâche 2 (moteur templates)** : tache4.md s'appuie correctement sur Mustache + templates versionnés sur disque, conforme à tache2.md. Aucun moteur parallèle créé. OK.
|
||||||
|
- **Tâche 1.9 (BDD)** : tables `install_profiles`, `install_recipes`, `install_recipe_versions`, `machine_profile_state`, `script_variables_presets`, `machine_hardware`, `machine_metrics_latest` sont bien définies dans tache1.9.md §9. La liaison explicite depuis tache4.md vers ces tables est manquante (réserve R1 ci-dessus).
|
||||||
|
- **Tâche 3 (formulaires UI)** : les comportements "déplier champs" et "désactiver bouton run" relèvent de tache3.md et ne sont pas à spécifier dans tache4.md. Frontière correcte.
|
||||||
|
- **Tâche 5 (exécution)** : les JSON d'entrée/sortie définis dans tache4.md constituent les contrats que tache5 devra respecter pour le routage et le stockage. Dépendance correctement identifiée en §15.
|
||||||
|
- **client.ts / templates APT existants** : le modèle d'exécution (sudo -S, base64, marqueurs ===SU:XXX===, non interactif) est cohérent avec les conventions. Les scripts post-install de tache4 doivent émettre les marqueurs de section appropriés ou un JSON terminal en remplacement — la spec tache4 choisit le JSON terminal, ce qui est cohérent avec l'intention d'évolution décrite dans tache2 (JSON-in-shell).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Conclusion
|
||||||
|
|
||||||
|
Le design tache4.md est **prêt à entrer en planification d'implémentation** sous réserve du traitement des quatre réserves majeures ci-dessus, en priorité R1 (liaison BDD) et R2 (champ reboot_verified), qui sont nécessaires pour que la tâche 5 puisse router correctement les résultats.
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# Protocole de validation — Tâche 5 (backend, historique JSON et automatisations)
|
||||||
|
|
||||||
|
> **Type** : grille de validation. Utilisée après la mission décrite dans `tache5.md`.
|
||||||
|
> **But** : vérifier que le backend cible stocke, orchestre, expose et nettoie les données correctement.
|
||||||
|
> **Gate obligatoire** : cette validation doit être passée avant implémentation backend majeure.
|
||||||
|
> **Rappel** : la tâche 5 est une mission design backend, pas d'implémentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Quand lancer cette validation
|
||||||
|
|
||||||
|
- La mission `tache5.md` est annoncée terminée.
|
||||||
|
- API, jobs, stockage, rétention et intégration Hermes/app locale sont spécifiés.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Discipline & périmètre
|
||||||
|
|
||||||
|
- [ ] Aucun code backend modifié.
|
||||||
|
- [ ] Aucune migration BDD appliquée.
|
||||||
|
- [ ] Le design réutilise la couche SSH existante.
|
||||||
|
- [ ] Les jobs/actions passent par un modèle verrouillé/idempotent.
|
||||||
|
- [ ] Aucun secret ne sort du backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Stockage et historique
|
||||||
|
|
||||||
|
- [ ] Tous les échanges JSON machine ↔ serveur sont historisables.
|
||||||
|
- [ ] Snapshots et exécutions sont séparés.
|
||||||
|
- [ ] Rapports Markdown et logs bruts sont référencés.
|
||||||
|
- [ ] Messages importants extraits des logs sont stockés et consultables.
|
||||||
|
- [ ] Hardware/profil/métriques simples sont historisables.
|
||||||
|
- [ ] L'état courant machine est dérivé sans parser les logs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Automatisations
|
||||||
|
|
||||||
|
- [ ] Schedules persistants définis.
|
||||||
|
- [ ] `apt_update_analyze` planifiable.
|
||||||
|
- [ ] `machine_metrics_simple` planifiable.
|
||||||
|
- [ ] Docker scan planifiable selon config.
|
||||||
|
- [ ] Verrous machine et idempotency keys spécifiés.
|
||||||
|
- [ ] Timeouts, retries et reprise serveur spécifiés.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. API backend
|
||||||
|
|
||||||
|
- [ ] Endpoints machines/state/hardware/metrics/snapshots/executions/events/messages.
|
||||||
|
- [ ] Endpoints reports/artifacts/search.
|
||||||
|
- [ ] Endpoints schedules.
|
||||||
|
- [ ] Endpoints settings.
|
||||||
|
- [ ] Endpoints events/live output.
|
||||||
|
- [ ] `/api/capabilities` défini.
|
||||||
|
- [ ] Erreurs structurées, pagination, filtres, version API.
|
||||||
|
- [ ] Clients visés : web, Hermes/MCP, app Rust/GNOME.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Sécurité et clients API
|
||||||
|
|
||||||
|
- [ ] Auth API clients séparée des credentials machines.
|
||||||
|
- [ ] Scopes `read/operate/admin/debug_logs` ou équivalent.
|
||||||
|
- [ ] Tokens hashés/révocables/audités.
|
||||||
|
- [ ] Secrets SSH/sudo jamais exposés.
|
||||||
|
- [ ] Logs filtrés avant UI/Hermes/app locale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Objectif final Docker Compose webserver
|
||||||
|
|
||||||
|
- [ ] Le backend est conçu pour tourner en container.
|
||||||
|
- [ ] DB SQLite persistée en volume.
|
||||||
|
- [ ] Reports/logs/artifacts persistés en volume.
|
||||||
|
- [ ] Configuration par variables d'environnement.
|
||||||
|
- [ ] `SU_MASTER_KEY` et tokens hors image.
|
||||||
|
- [ ] Le container backend peut joindre les machines en SSH.
|
||||||
|
- [ ] Un packaging Docker Compose final est explicitement compatible avec l'architecture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Verdict
|
||||||
|
|
||||||
|
- **✅ Accepté** : backend cible cohérent et prêt à planifier.
|
||||||
|
- **🟡 Accepté avec réserves** : endpoints ou rétention à préciser.
|
||||||
|
- **❌ Rejeté** : secrets exposés, jobs non verrouillés, API instable ou incompatible Docker Compose.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Notes de validation
|
||||||
|
|
||||||
|
> Revue du 2026-06-05 — Gilles Soulier (orchestrateur, validation déléguée).
|
||||||
|
|
||||||
|
### Verdict global : 🟡 Accepté avec réserves
|
||||||
|
|
||||||
|
Le design `tache5.md` est cohérent dans ses grandes lignes et couvre la majorité des cases du gate. Les points fondamentaux (séparation snapshots/exécutions, état courant dérivé, machine_locks + idempotencyKey, scopes API clients, secrets hors backend, croner MVP) sont correctement traités. Deux réserves majeures et plusieurs points d'attention sont relevés ci-dessous.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Réserves majeures
|
||||||
|
|
||||||
|
**R1 — Contrainte réseau Docker Compose non spécifiée**
|
||||||
|
|
||||||
|
La case « Le container backend peut joindre les machines en SSH » (section 6 du gate) n'est pas couverte dans `tache5.md`. Il n'est nulle part précisé que le container Docker doit avoir accès réseau aux machines cibles (réseau `host`, bridge avec routage LAN, ou VPN/overlay). Sans cette précision, un déploiement en container isolé sera en échec silencieux au premier refresh SSH. La spec doit ajouter au moins une note dans la checklist section 11 ou dans un sous-jalons Docker Compose.
|
||||||
|
|
||||||
|
**R2 — Exemple `capabilities` appauvri, incohérent avec le WIP**
|
||||||
|
|
||||||
|
La section 6 de `tache5.md` présente un exemple JSON de capabilities minimal (`machines`, `actions`, `terminalOutput`, `interactiveSsh`, `hermes`, `settings`) qui ne correspond pas au type `ServerCapabilities` déjà défini dans `shared/types.ts` et implémenté dans `server/services/capabilities.ts` (WIP non commité). Le WIP expose des features plus granulaires : `machineSnapshots`, `aptFullUpgrade`, `reboot`, `reports`, `docker`, `postInstall`, `scheduledJobs`, `authTokens`, et un bloc `endpoints` structuré. La spec doit référencer le type existant ou s'y aligner explicitement pour éviter une divergence à l'implémentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Points d'attention (non bloquants)
|
||||||
|
|
||||||
|
**P1 — Réutilisation couche SSH non citée explicitement**
|
||||||
|
|
||||||
|
La section 0 « À lire avant de travailler » omettait `tache1.9.md` (corrigé en coquille) et ne cite pas `server/ssh/client.ts`. La spec backend qui planifie des jobs SSH devrait référencer la couche `runScriptSudo`/`runPlain` existante pour signaler explicitement qu'elle est réutilisée telle quelle et ne doit pas être réécrite.
|
||||||
|
|
||||||
|
**P2 — Verrou machine : granularité non tranchée**
|
||||||
|
|
||||||
|
Section 5 liste les règles de verrouillage (apt bloque pendant upgrade, Docker scan autorisé sauf action destructive, reboot bloque tout) mais ne tranche pas sur la représentation en BDD : utilise-t-on `machine_locks` (table tache1.9) avec un `lock_kind` (apt | docker | post_install | reboot | exclusive) ou un simple champ `running_job_id` dans `machine_state` ? Les deux approches sont citées (`table machine_locks ou statut job courant`) sans choix définitif. Acceptable en design préliminaire mais à trancher avant implémentation.
|
||||||
|
|
||||||
|
**P3 — Reprise après redémarrage serveur non détaillée pour croner**
|
||||||
|
|
||||||
|
Section 5 mentionne « reprise après redémarrage serveur selon le moteur choisi » mais le choix est `croner` in-process. Croner ne persiste rien : un job `running` au moment du crash restera bloqué en BDD. La spec doit préciser la procédure de récupération au démarrage (ex. : au boot, marquer `cancelled` les jobs `running` orphelins et libérer les `machine_locks` expirés).
|
||||||
|
|
||||||
|
**P4 — Pagination et filtres : non détaillés**
|
||||||
|
|
||||||
|
Section 6 mentionne pagination et filtres dans la liste à puces mais aucun endpoint n'est détaillé (pas de paramètres `?page`, `?limit`, `?status`, `?from`, `?to`). Acceptable pour un design préliminaire, mais à spécifier avant d'écrire les routes.
|
||||||
|
|
||||||
|
**P5 — `POST /api/machines/:id/actions` vs actions déjà existantes**
|
||||||
|
|
||||||
|
Le WIP `server/routes/actions.ts` expose `POST /:id/actions` qui n'accepte que `apt_full_upgrade` et `reboot`. La spec tache5.md liste des actions bien plus larges (apt_upgrade, apt_autoremove, apt_clean, docker_apply, docker_prune, post_install_profile, reboot_verified). La spec devrait explicitement noter que cet endpoint doit être élargi et protégé par les verrous machine, pas seulement validé par whitelist statique.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Coquilles corrigées dans `tache5.md`
|
||||||
|
|
||||||
|
1. **Section 6, liste des endpoints** : `GET /api/ws/machines/:id/output` remplacé par `WS /api/ws/machines/:id/output` — alignement avec `server/services/capabilities.ts` WIP qui l'annote `"WS /api/ws/machines/:id/output"`.
|
||||||
|
|
||||||
|
2. **Section 0, liste « À lire avant de travailler »** : `tache1.9.md` ajouté — était absent alors que c'est le parent direct du schéma BDD consommé par la tâche 5.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Cohérence avec l'existant (WIP capabilities)
|
||||||
|
|
||||||
|
| Point | tache5.md | WIP shared/types.ts + capabilities.ts | Verdict |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Endpoint `/api/capabilities` | Présent | Implémenté (`api.get("/capabilities", ...)`) | Cohérent |
|
||||||
|
| `apiVersion: "1"` | Présent | Présent (`apiVersion: "1"` literal) | Cohérent |
|
||||||
|
| Features capabilities | Exemple minimal 6 features | 14 features granulaires + bloc `endpoints` | **Incohérent — R2** |
|
||||||
|
| Endpoint WS output | Noté `GET` (avant correction) | Noté `WS` | Corrigé |
|
||||||
|
| Auth tokens | Mentionnés section 6 | `authTokens: false` (WIP, à activer) | Cohérent en intention |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Conclusion
|
||||||
|
|
||||||
|
Le design peut passer en plan d'implémentation **à condition de** :
|
||||||
|
1. Préciser la contrainte réseau SSH dans le packaging Docker Compose (R1).
|
||||||
|
2. Aligner l'exemple capabilities avec `ServerCapabilities` de `shared/types.ts` ou le mettre à jour dans le type partagé (R2).
|
||||||
|
3. Documenter la procédure de récupération des jobs orphelins au démarrage croner (P3).
|
||||||
|
|
||||||
|
Les autres points (P1, P2, P4, P5) peuvent être affinés en cours d'implémentation sans bloquer le plan.
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# Protocole de validation — Tâche 6 (Hermes, MCP, skills et messagerie)
|
||||||
|
|
||||||
|
> **Type** : grille de validation. Utilisée après la mission décrite dans `tache6.md`.
|
||||||
|
> **But** : vérifier que Hermes analyse et propose, sans accéder aux secrets ni exécuter SSH directement.
|
||||||
|
> **Gate obligatoire** : cette validation doit être passée avant intégration Hermes/MCP.
|
||||||
|
> **Rappel** : la tâche 6 est une mission design intégration agent, pas d'implémentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Quand lancer cette validation
|
||||||
|
|
||||||
|
- La mission `tache6.md` est annoncée terminée.
|
||||||
|
- Architecture API Hermes/MCP/skill est décrite.
|
||||||
|
- Les tools MCP sont listés.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Discipline & périmètre
|
||||||
|
|
||||||
|
- [ ] Aucun code Hermes/MCP/backend modifié.
|
||||||
|
- [ ] Hermes ne fait jamais de SSH direct.
|
||||||
|
- [ ] Hermes ne reçoit jamais de secret.
|
||||||
|
- [ ] Les actions dangereuses passent par `request_action` + validation UI.
|
||||||
|
- [ ] Le volet Hermes reste distinct du terminal SSH.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Architecture Hermes
|
||||||
|
|
||||||
|
- [ ] API Server Hermes configuré côté backend, clé jamais navigateur.
|
||||||
|
- [ ] MCP HTTP `system_update` défini.
|
||||||
|
- [ ] Skill `system-update-ops` défini.
|
||||||
|
- [ ] TUI/messagerie et volet web couverts.
|
||||||
|
- [ ] Webhooks/notifications couverts.
|
||||||
|
- [ ] Cron Hermes limité aux rapports/analyses, pas aux opérations SSH.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Tools MCP
|
||||||
|
|
||||||
|
- [ ] `list_machines`.
|
||||||
|
- [ ] `get_machine_state`.
|
||||||
|
- [ ] `get_machine_hardware`.
|
||||||
|
- [ ] `get_machine_metrics`.
|
||||||
|
- [ ] `get_machine_snapshot`.
|
||||||
|
- [ ] `get_machine_execution`.
|
||||||
|
- [ ] `get_machine_messages`.
|
||||||
|
- [ ] `search_reports`.
|
||||||
|
- [ ] `get_report`.
|
||||||
|
- [ ] `search_log_artifacts`.
|
||||||
|
- [ ] `run_refresh`.
|
||||||
|
- [ ] `request_action`.
|
||||||
|
- [ ] `get_pending_actions`.
|
||||||
|
- [ ] `preview_template`.
|
||||||
|
- [ ] `list_templates`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Données envoyées à Hermes
|
||||||
|
|
||||||
|
- [ ] Priorité : état courant, JSON réduit, messages importants, rapports, extraits ciblés.
|
||||||
|
- [ ] Logs bruts complets jamais envoyés par défaut.
|
||||||
|
- [ ] Masquage secrets prévu.
|
||||||
|
- [ ] Usage tokens mesurable.
|
||||||
|
- [ ] Références rehydratables prévues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UX volet Hermes
|
||||||
|
|
||||||
|
- [ ] Messages utilisateur/Hermes/système distingués.
|
||||||
|
- [ ] Blocs de commandes copiables.
|
||||||
|
- [ ] Actions proposées sous cartes de validation.
|
||||||
|
- [ ] Rapports/logs/snapshots cités par liens.
|
||||||
|
- [ ] Stop/streaming/health/capabilities prévus.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Verdict
|
||||||
|
|
||||||
|
- **✅ Accepté** : Hermes utile et sûr.
|
||||||
|
- **🟡 Accepté avec réserves** : tools ou skill à préciser.
|
||||||
|
- **❌ Rejeté** : SSH direct, secrets exposés ou actions non validées.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Notes de validation
|
||||||
|
|
||||||
|
> Revue du 2026-06-05 — orchestrateur, validation déléguée.
|
||||||
|
|
||||||
|
### Verdict global
|
||||||
|
|
||||||
|
**Accepté avec réserves mineures.**
|
||||||
|
|
||||||
|
La tâche 6 couvre l'ensemble des points du gate. Les règles de sécurité fondamentales (pas de SSH direct, pas de secret transmis à Hermes, actions dangereuses via `request_action` + validation UI, clé API côté backend uniquement) sont présentes et cohérentes dans tous les paragraphes du document. La séparation volet Hermes / terminal SSH est explicitement formulée. La liste des tools MCP, le skill `system-update-ops`, les notifications/webhooks, la messagerie et les deux cas d'usage (volet web et TUI/messagerie) sont couverts. La cohérence avec `tache1.9.md` (tables `hermes_sessions`, `hermes_runs`, `hermes_usage`, `mcp_audit_log`, `action_requests`), `tache5.md` (API consommée par MCP) et `tache7.md` (réduction tokens, masquage secrets) est correcte.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Cases du gate passées
|
||||||
|
|
||||||
|
**Section 1 — Discipline & périmètre**
|
||||||
|
- Aucun code modifié : conforme (mission design uniquement, §0 et §11).
|
||||||
|
- Pas de SSH direct : conforme (§2 règle forte + §9 règle non négociable + §12 checklist).
|
||||||
|
- Pas de secret transmis : conforme (§3, §9, §12).
|
||||||
|
- Actions dangereuses via `request_action` + validation UI : conforme (§4, §5, §12).
|
||||||
|
- Volet Hermes distinct du terminal SSH : conforme (§5, terminal SSH décrit comme outil séparé dans le volet droit).
|
||||||
|
|
||||||
|
**Section 2 — Architecture Hermes**
|
||||||
|
- API Server côté backend, clé jamais navigateur : conforme (§1, §3 avec variable `HERMES_API_KEY`, §12).
|
||||||
|
- MCP HTTP `system_update` défini : conforme (§1, §4, §5).
|
||||||
|
- Skill `system-update-ops` défini : conforme (§6).
|
||||||
|
- TUI/messagerie et volet web couverts : conforme (§3 et §4).
|
||||||
|
- Webhooks/notifications couverts : conforme (§8).
|
||||||
|
- Cron Hermes limité rapports/analyses, pas SSH : conforme (§1 section Cron Hermes).
|
||||||
|
|
||||||
|
**Section 3 — Tools MCP**
|
||||||
|
Tous les 15 tools du gate sont couverts dans la surface minimale (§5).
|
||||||
|
Après correction des coquilles (voir ci-dessous), la liste `include` du yaml config Hermes (§4) est alignée avec la liste §5 et le gate.
|
||||||
|
|
||||||
|
**Section 4 — Données envoyées à Hermes**
|
||||||
|
- Priorité état courant / JSON réduit / messages importants / rapports / extraits ciblés : conforme (§5, §6 procédure).
|
||||||
|
- Logs bruts complets jamais par défaut : conforme (§5 "à éviter au MVP", §9).
|
||||||
|
- Masquage secrets prévu : conforme (§9 + renvoi explicite à tache7.md pour la spec de masquage).
|
||||||
|
- Usage tokens mesurable : conforme par référence à tache7.md §5 et tables tache1.9.md.
|
||||||
|
- Références rehydratables : conforme (tools MCP `get_machine_snapshot`, `get_machine_execution`, `get_report`).
|
||||||
|
|
||||||
|
**Section 5 — UX volet Hermes**
|
||||||
|
- Messages utilisateur/Hermes/système distingués : conforme (§3).
|
||||||
|
- Blocs de commandes copiables : conforme (§3 "blocs de commande monospace avec bouton copier").
|
||||||
|
- Actions proposées sous cartes de validation : conforme (§3 "carte de validation, pas comme exécution directe").
|
||||||
|
- Rapports/logs/snapshots cités par liens : conforme (§3 "références cliquables").
|
||||||
|
- Stop/streaming/health/capabilities prévus : conforme (§3 endpoints dédiés).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Réserves
|
||||||
|
|
||||||
|
**R1 — Interdiction d'auto-exécution des commandes copiées non formulée explicitement**
|
||||||
|
Le gate vérifie "blocs de commandes copiables non auto-exécutés". tache6.md précise "bouton copier" mais ne formule pas explicitement l'interdiction pour la webapp d'injecter automatiquement une commande dans le terminal interactif. L'esprit du document l'implique (toute action passe par validation UI), mais la formulation directe manque. À clarifier lors du plan d'implémentation de tache3.md (volet UI).
|
||||||
|
|
||||||
|
**R2 — Liste include yaml (§4) initialement incohérente avec surface minimale (§5) et gate (§3)**
|
||||||
|
La liste `include` du yaml config Hermes (§4) omettait `get_all_snapshots`, `preview_template` et `list_templates`, présents dans la surface minimale §5 et dans le gate. Corrigé dans la présente revue (coquille).
|
||||||
|
|
||||||
|
**R3 — Usage tokens : pas de contrat propre dans tache6.md**
|
||||||
|
tache6.md délègue entièrement la spec de mesure tokens à tache7.md. Acceptable pour une tâche design, mais le plan d'implémentation devra s'assurer que les deux specs sont lues ensemble. Aucun blocage.
|
||||||
|
|
||||||
|
**R4 — `get_all_snapshots` absent du gate**
|
||||||
|
`get_all_snapshots` figure dans la surface minimale §5 et a été ajouté dans la liste yaml (correction R2), mais il est absent de la liste du gate (section 3). Cela signifie que ce tool ne sera pas vérifié par le gate lors de la future revue d'implémentation. À porter en note lors de la mise à jour éventuelle du gate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Coquilles corrigées dans tache6.md
|
||||||
|
|
||||||
|
1. **§4 yaml config include** : ajout de `get_all_snapshots`, `preview_template`, `list_templates` absents de la liste `include` alors qu'ils figurent dans §5 et le gate.
|
||||||
|
2. **§5 mention `get_log_excerpt`** : reformulation "futur, hors liste MVP" pour lever l'ambiguïté sur son statut (il était décrit dans §5 mais absent de la liste surface minimale).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Incohérences détectées (sans blocage)
|
||||||
|
|
||||||
|
- La liste MCP dans `ajout.md` (document d'origine) utilise `request_action_plan` et `run_approved_action` ; tache6.md consolide en `request_action` + `get_pending_actions`. Évolution volontaire et cohérente. Aucun conflit.
|
||||||
|
- `get_all_snapshots` est présent dans §5 de tache6.md mais absent du gate. Non bloquant : la surface est couverte côté design, la validation gate reste partielle sur ce point.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Conclusion
|
||||||
|
|
||||||
|
La tâche 6 satisfait les critères de sécurité fondamentaux et couvre les deux cas d'usage. Les réserves sont mineures et ne nécessitent pas de rééditer la spec avant le plan d'implémentation. Les coquilles d'alignement de liste ont été corrigées directement. Le passage en plan d'implémentation est autorisé, avec la charge pour l'implémenteur de lire tache7.md conjointement pour la politique de masquage et de mesure tokens.
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# Protocole de validation — Tâche 7 (optimisation, métriques, nettoyage, sécurité)
|
||||||
|
|
||||||
|
> **Type** : grille de validation. Utilisée après la mission décrite dans `tache7.md`.
|
||||||
|
> **But** : vérifier que l'observabilité, la réduction tokens, le nettoyage et la sécurité sont conçus sans perdre de données importantes.
|
||||||
|
> **Gate obligatoire** : cette validation doit être passée avant implémentation des optimisations transverses.
|
||||||
|
> **Rappel** : la tâche 7 est une mission design optimisation, pas d'implémentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Quand lancer cette validation
|
||||||
|
|
||||||
|
- La mission `tache7.md` est annoncée terminée.
|
||||||
|
- Les politiques métriques, tokens, nettoyage, discovery et secrets sont spécifiées.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Discipline & périmètre
|
||||||
|
|
||||||
|
- [ ] Aucun code de production modifié.
|
||||||
|
- [ ] Aucun nettoyage destructif appliqué.
|
||||||
|
- [ ] Aucun scan réseau lancé.
|
||||||
|
- [ ] Aucun secret déplacé ou exposé.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Observabilité
|
||||||
|
|
||||||
|
- [ ] Footer métriques backend spécifié.
|
||||||
|
- [ ] `/api/system/status`, `/metrics`, `/storage` ou équivalent définis.
|
||||||
|
- [ ] Métriques simples par machine distinguées des métriques backend.
|
||||||
|
- [ ] Seuils warning et tooltips prévus.
|
||||||
|
- [ ] Pas de dépendance obligatoire à un agent installé sur les machines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Optimisation Hermes/tokens
|
||||||
|
|
||||||
|
- [ ] Réduction déterministe définie.
|
||||||
|
- [ ] Données brutes conservées côté webapp.
|
||||||
|
- [ ] Références rehydratables prévues.
|
||||||
|
- [ ] Usage tokens mesuré.
|
||||||
|
- [ ] Diffs/dédup/pagination prévus.
|
||||||
|
- [ ] Aucune perte de donnée critique.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Nettoyage DB/logs
|
||||||
|
|
||||||
|
- [ ] Politique de rétention paramétrable.
|
||||||
|
- [ ] Dry-run obligatoire avant purge manuelle.
|
||||||
|
- [ ] Rapports épinglés conservés.
|
||||||
|
- [ ] Erreurs conservées plus longtemps.
|
||||||
|
- [ ] Messages importants non acquittés conservés.
|
||||||
|
- [ ] Maintenance SQLite : optimize, checkpoint, vacuum contrôlé.
|
||||||
|
- [ ] Export/sauvegarde avant purge majeure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Sécurité secrets
|
||||||
|
|
||||||
|
- [ ] Mots de passe/keys chiffrés au repos.
|
||||||
|
- [ ] Host key policy prévue.
|
||||||
|
- [ ] Pas de secret en localStorage/sessionStorage.
|
||||||
|
- [ ] Redaction logs/UI/Hermes/MCP.
|
||||||
|
- [ ] Rotation/révocation prévues.
|
||||||
|
- [ ] Terminal SSH interactif encadré.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Découverte réseau
|
||||||
|
|
||||||
|
- [ ] Bouton scan SSH à l'ajout machine spécifié.
|
||||||
|
- [ ] CIDR allowlist.
|
||||||
|
- [ ] Scan port 22 contrôlé.
|
||||||
|
- [ ] Comparatif nmap vs scanner TCP.
|
||||||
|
- [ ] Sortie structurée.
|
||||||
|
- [ ] Journalisation des scans.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Objectif final Docker Compose webserver
|
||||||
|
|
||||||
|
- [ ] Nettoyage compatible volumes Docker.
|
||||||
|
- [ ] Métriques DB/WAL compatibles container.
|
||||||
|
- [ ] Variables d'environnement pour rétention/secrets.
|
||||||
|
- [ ] Pas de dépendance à des chemins hôte non montés.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Verdict
|
||||||
|
|
||||||
|
- **✅ Accepté** : optimisation sûre et exploitable.
|
||||||
|
- **🟡 Accepté avec réserves** : seuils/rétention à préciser.
|
||||||
|
- **❌ Rejeté** : perte de données importantes, secrets exposés ou scan non encadré.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Notes de validation
|
||||||
|
|
||||||
|
> Revue effectuée le 2026-06-05 (orchestrateur, validation déléguée).
|
||||||
|
|
||||||
|
### Verdict global
|
||||||
|
|
||||||
|
**🟡 Accepté avec réserves**
|
||||||
|
|
||||||
|
La spec couvre l'essentiel du périmètre. Les principes structurants sont solides et cohérents avec `tache1.9.md`, `tache5.md`, `tache6.md`, `server/crypto/secrets.ts` et `server/templates/aptReduce.ts`. Aucun risque de perte de donnée critique, aucun secret exposé, aucun scan non encadré. Les réserves ci-dessous ne bloquent pas l'implémentation mais devront être levées au plus tard lors du plan d'implémentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Cases du gate
|
||||||
|
|
||||||
|
#### Section 1 — Discipline & périmètre
|
||||||
|
- **Aucun code de production modifié** : conforme — tâche design explicitement délimitée. ✅
|
||||||
|
- **Aucun nettoyage destructif appliqué** : conforme — purge uniquement spécifiée. ✅
|
||||||
|
- **Aucun scan réseau lancé** : conforme — design seul. ✅
|
||||||
|
- **Aucun secret déplacé ou exposé** : conforme. ✅
|
||||||
|
|
||||||
|
#### Section 2 — Observabilité
|
||||||
|
- **Footer métriques backend spécifié** : OUI — exemple texte complet + liste métriques (section 2). ✅
|
||||||
|
- **`/api/system/status`, `/metrics`, `/storage` définis** : OUI — les trois endpoints sont listés avec exemple JSON (section 2). ✅
|
||||||
|
- **Métriques simples par machine distinguées des métriques backend** : OUI — section "Métriques simples par machine" explicitement séparée, avec note "Ces métriques concernent le backend `system_update`, pas le monitoring détaillé des hôtes distants." ✅
|
||||||
|
- **Seuils warning et tooltips prévus** : PARTIELLEMENT — les tooltips sont mentionnés dans les contraintes design ("tooltips sur métriques peu évidentes"), et la section paramètres (section 8) liste "seuils warning". Mais aucune valeur de seuil (ex. RAM > X %, disk > Y %) n'est proposée dans la spec. 🟡 **Réserve mineure** : à préciser au plan d'implémentation.
|
||||||
|
- **Pas de dépendance obligatoire à un agent installé sur les machines** : OUI — "agentless, via SSH" (section 2). ✅
|
||||||
|
|
||||||
|
#### Section 3 — Optimisation Hermes/tokens
|
||||||
|
- **Réduction déterministe définie** : OUI — reducers par domaine listés (APT, Docker, post-install, reboot, erreurs). Cohérent avec `aptReduce.ts` existant. ✅
|
||||||
|
- **Données brutes conservées côté webapp** : OUI — principe "Ne jamais perdre la donnée" + liste explicite (log brut, JSON complet, rapport Markdown, historique, diff, erreurs structurées). ✅
|
||||||
|
- **Références rehydratables prévues** : OUI — `snapshotId`, `executionId`, `reportRef` dans le payload réduit, avec MCP tools `get_machine_snapshot`, `get_machine_execution`, `get_report`. ✅
|
||||||
|
- **Usage tokens mesuré** : OUI — section 5 avec table `hermes_usage` et champs `rawBytes`, `reducedBytes`, `reductionRatio`, `cachedTokens`. ✅
|
||||||
|
- **Diffs/dédup/pagination prévus** : OUI — section 4. ✅
|
||||||
|
- **Aucune perte de donnée critique** : OUI — architecture clairement séparée webapp / payload Hermes. ✅
|
||||||
|
|
||||||
|
#### Section 4 — Nettoyage DB/logs
|
||||||
|
- **Politique de rétention paramétrable** : OUI — JSON de rétention avec jours configurables par catégorie (section 6). ✅
|
||||||
|
- **Dry-run obligatoire avant purge manuelle** : OUI — "mode dry-run obligatoire avant suppression manuelle" et dans l'exemple de tâche (`"mode": "dry_run"`). ✅
|
||||||
|
- **Rapports épinglés conservés** : OUI — `keepPinnedReportsForever: true`, règle "ne jamais supprimer un rapport épinglé". ✅
|
||||||
|
- **Erreurs conservées plus longtemps** : OUI — `keepFailedExecutionsDays: 365` vs `executionsDays: 180`. ✅
|
||||||
|
- **Messages importants non acquittés conservés** : OUI — `keepUnacknowledgedWarningsForever: true`, règle explicite sur les warnings de sécurité/dépôt/dépréciation. ✅
|
||||||
|
- **Maintenance SQLite : optimize, checkpoint, vacuum contrôlé** : OUI — section 6 "SQLite maintenance" couvre les trois avec nuances correctes (VACUUM contrôlé car coûteux). ✅
|
||||||
|
- **Export/sauvegarde avant purge majeure** : OUI — mentionné ("politique de sauvegarde avant purge majeure"). 🟡 **Réserve mineure** : la procédure (format export, destination, déclencheur) n'est pas détaillée dans la spec — à préciser au plan.
|
||||||
|
|
||||||
|
#### Section 5 — Sécurité secrets
|
||||||
|
- **Mots de passe/keys chiffrés au repos** : OUI — référence explicite au chiffrement existant AES-256-GCM (`server/crypto/secrets.ts`), à "conserver et renforcer". ✅
|
||||||
|
- **Host key policy prévue** : OUI — code d'erreur `host_key_changed`, `validation host key`, `fingerprint`, paramètre "host key policy" (section 8). ✅
|
||||||
|
- **Pas de secret en localStorage/sessionStorage** : OUI — interdit explicitement. ✅
|
||||||
|
- **Redaction logs/UI/Hermes/MCP** : OUI — section 3 liste tous les vecteurs (stdout/stderr, exceptions backend, terminal live, rapports, notifications, payloads Hermes). ✅
|
||||||
|
- **Rotation/révocation prévues** : OUI — "rotation des credentials par machine". ✅
|
||||||
|
- **Terminal SSH interactif encadré** : PARTIELLEMENT — tache7 mentionne le masquage des secrets dans le terminal (section 3 : "jamais de secret dans [...] terminal live") et dans les paramètres ("terminal SSH interactif encadré" en section 8). Les règles détaillées sont dans `tache6.md` et `tache1.9.md` (`ssh_terminal_sessions`). 🟡 **Réserve mineure** : tache7 en propre ne répète pas les règles complètes (désactivation globale, auditabilité, enregistrement) — acceptable dans un design multi-tâches mais à référencer explicitement.
|
||||||
|
|
||||||
|
#### Section 6 — Découverte réseau
|
||||||
|
- **Bouton scan SSH à l'ajout machine spécifié** : OUI — bouton "[scanner le réseau]" dans l'UI "Ajouter une machine" (section 7). ✅
|
||||||
|
- **CIDR allowlist** : OUI — "scan uniquement sur plages autorisées en paramètres", paramètre "CIDR autorisés" (section 8). ✅
|
||||||
|
- **Scan port 22 contrôlé** : OUI — port 22 seul, CIDR configuré, pas de scan agressif, pas de détection intrusive. ✅
|
||||||
|
- **Comparatif nmap vs TCP scanner** : OUI — "Le design doit comparer MVP nmap vs TCP scanner maison" (section 7). ✅
|
||||||
|
- **Sortie structurée** : OUI — JSON candidats avec champs structurés (`host`, `port`, `service`, `hostKeyFingerprint`, `reverseDns`, `alreadyKnown`). ✅
|
||||||
|
- **Journalisation des scans** : OUI — "journaliser scans" + table `discovery_scans` dans `tache1.9.md`. ✅
|
||||||
|
|
||||||
|
#### Section 7 — Docker Compose webserver
|
||||||
|
- **Nettoyage compatible volumes Docker** : OUI — checklist section 11 "Docker volumes compatibles purge/logs". ✅
|
||||||
|
- **Métriques DB/WAL compatibles container** : PARTIELLEMENT — les métriques lisent `sizeBytes` sur le chemin DB, qui doit être monté. Ceci est implicitement couvert par `tache5.md` et `tache1.9.md` (volumes Docker Compose pour DB/WAL) mais pas rappelé dans tache7. 🟡 **Réserve mineure** : tache7 ne mentionne pas explicitement que le chemin DB doit correspondre au volume monté dans le container.
|
||||||
|
- **Variables d'environnement pour rétention/secrets** : OUI — paramètres via API/settings et env vars mentionnés. ✅
|
||||||
|
- **Pas de dépendance à des chemins hôte non montés** : NON TRAITÉ explicitement dans tache7. La contrainte est portée par `tache5.md` et `tache1.9.md` mais tache7 n'y fait pas référence pour les chemins logs/rapports/DB de son périmètre. 🟡 **Réserve mineure** : à vérifier au plan d'implémentation que les chemins `rawLogDays`, `reports/`, `system-update.db` sont bien sous le préfixe `DATA_DIR` monté.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Réserves à lever au plan d'implémentation
|
||||||
|
|
||||||
|
1. **Seuils warning observabilité** : valeurs concrètes manquantes (ex. CPU > 80 %, RAM RSS > 500 Mo, DB > 500 Mo). À définir dans le plan ou une config par défaut.
|
||||||
|
2. **Procédure d'export avant purge majeure** : format (JSON, tar, CSV ?), destination (même répertoire `data/` ? volume séparé ?), déclencheur (automatique avant tout cleanup `apply` > N lignes ?). Non précisé.
|
||||||
|
3. **Encadrement terminal SSH interactif dans tache7** : ajouter une référence explicite vers `tache6.md` section 5 et `tache1.9.md` table `ssh_terminal_sessions` pour que la spec tache7 soit auto-suffisante sur ce point.
|
||||||
|
4. **Compatibilité Docker Compose pour métriques et nettoyage** : préciser que `database.path`, `rawLogsDirs` et `reportsDir` doivent être sous un `DATA_DIR` monté en volume dans le Compose — ne pas laisser cela à l'implémentation sans contrainte documentée.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Coquilles corrigées dans `tache7.md`
|
||||||
|
|
||||||
|
- Ligne 16 (section 0) : `.` remplacé par `;` en fin d'item de liste (cohérence de ponctuation).
|
||||||
|
- Ligne 52 (section 1) : `.` remplacé par `;` en fin d'item de liste (cohérence de ponctuation).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Incohérences avec les tâches parentes
|
||||||
|
|
||||||
|
Aucune incohérence structurelle majeure détectée :
|
||||||
|
|
||||||
|
- Les reducers déterministes de tache7 s'appuient et étendent `aptReduce.ts` — cohérent.
|
||||||
|
- La table `hermes_usage` est identique entre tache7 (section 5) et `tache1.9.md` (section 12) — cohérent.
|
||||||
|
- Les tables `discovery_scans` / `discovery_candidates` / `system_metrics` / `cleanup_runs` de tache7 correspondent exactement aux tables "Optimisation / maintenance" de `tache1.9.md` — cohérent.
|
||||||
|
- Le chiffrement AES-256-GCM de `server/crypto/secrets.ts` est explicitement référencé comme existant — cohérent.
|
||||||
|
- L'endpoint `/api/system/status` de tache7 est présent dans la liste API de `tache5.md` — cohérent.
|
||||||
|
- La table `machine_metrics_latest` de `tache1.9.md` correspond au `machine_metrics_simple` de tache7 — cohérent dans l'intention, mais le nom `machine_metrics_latest` (BDD) vs `machine_metrics_simple` (snapshot kind) doit être aligné au plan.
|
||||||
|
|
||||||
|
**Point d'attention** : tache7 section 5 liste le code d'erreur `secret_redaction_failed` — ceci implique un mécanisme de redaction actif côté backend. La spec ne précise pas comment ce cas est détecté ni quelle est l'action de fallback (fail-safe : bloquer l'envoi ? logguer séparément ? alerter ?). À préciser au plan.
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# Protocole de validation — Tâche 8 (app locale Rust/GNOME)
|
||||||
|
|
||||||
|
> **Type** : grille de validation. Utilisée après la mission décrite dans `tache8.md`.
|
||||||
|
> **But** : vérifier que l'app locale future est conçue comme client API sûr, sans dupliquer la logique SSH/backend.
|
||||||
|
> **Gate validé par l'utilisateur le 2026-06-05** : le développement Rust peut démarrer.
|
||||||
|
> **Rappel** : l'app locale reste cliente du backend et ne remplace pas la webapp serveur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Quand lancer cette validation
|
||||||
|
|
||||||
|
- La mission `tache8.md` avance par incréments.
|
||||||
|
- Les choix techniques Rust/GNOME restent argumentés au fur et à mesure.
|
||||||
|
- Le contrat API attendu est défini avant chaque intégration UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Discipline & périmètre
|
||||||
|
|
||||||
|
- [x] Le code Rust créé reste dans le périmètre `app_rust/system-update-gnome`.
|
||||||
|
- [x] L'app locale reste future, pas bloquante pour la webapp MVP.
|
||||||
|
- [x] L'app ne fait pas de SSH direct au MVP.
|
||||||
|
- [x] Les credentials machines restent côté backend.
|
||||||
|
- [x] Le design réutilise les contrats JSON existants.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Architecture
|
||||||
|
|
||||||
|
- [x] Rust + GTK4/libadwaita étudié.
|
||||||
|
- [x] Alternatives comme `relm4`/`iced` comparées si utile.
|
||||||
|
- [x] Client HTTP/WebSocket défini.
|
||||||
|
- [x] Cache local lecture seule défini.
|
||||||
|
- [x] Notifications desktop prévues.
|
||||||
|
- [x] Configuration serveur/token prévue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. API commune
|
||||||
|
|
||||||
|
- [x] `/api/capabilities`.
|
||||||
|
- [x] Machines/state/hardware/metrics.
|
||||||
|
- [x] Snapshots/executions/reports/messages.
|
||||||
|
- [x] Actions via backend.
|
||||||
|
- [x] Live output via WS/SSE.
|
||||||
|
- [x] Settings si scope admin.
|
||||||
|
- [~] Erreurs structurées et version API. *(jalons 8.1/8.2 non encore couverts)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Sécurité
|
||||||
|
|
||||||
|
- [~] Token stocké via trousseau local. *(keyring prévu dans tache8.md §6 et plan_8.md jalon 8.3 ; pas encore implémenté)*
|
||||||
|
- [x] Token jamais stocké en clair. *(README et règles explicites ; token lu en env var dans le scaffold — acceptable à ce stade)*
|
||||||
|
- [x] Scopes de token définis. *(tache1.9.md api_clients + tache8.md §6)*
|
||||||
|
- [x] Révocation/rotation prévues. *(tache1.9.md api_clients : champ revoked_at, status)*
|
||||||
|
- [x] Actions dangereuses confirmées dans l'app. *(tache8.md §6)*
|
||||||
|
- [x] Logs locaux sans secret. *(tache8.md §6 ; pas de log de token dans le scaffold)*
|
||||||
|
- [~] HTTPS/certificats traités. *(api.rs rejette explicitement https:// avec un test nommé "until_tls_client_is_added" — accepté comme dette connue et documentée)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UX native
|
||||||
|
|
||||||
|
- [x] Layout GNOME HeaderBar/Sidebar spécifié.
|
||||||
|
- [x] Tuiles machine adaptées.
|
||||||
|
- [x] Détail machine avec onglets.
|
||||||
|
- [x] Paramètres natifs spécifiés.
|
||||||
|
- [x] Thème Gruvbox via tokens GNOME.
|
||||||
|
- [x] Différences webapp/app locale assumées.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Cohérence avec objectif final webserver Docker Compose
|
||||||
|
|
||||||
|
- [x] L'app locale consomme le serveur Docker Compose, elle ne le remplace pas.
|
||||||
|
- [x] Le backend reste la source de vérité.
|
||||||
|
- [x] La webapp reste utilisable sans app locale.
|
||||||
|
- [x] Les API nécessaires sont compatibles avec un backend containerisé.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Verdict
|
||||||
|
|
||||||
|
**🟡 Accepté avec réserves**
|
||||||
|
|
||||||
|
Le premier incrément (jalon 8.0 et scaffold 8.3 partiel) est cohérent, bien périmétré et non bloquant pour le MVP web. Les réserves ci-dessous sont attendues et planifiées pour les jalons 8.1–8.3 ; elles ne remettent pas en cause la direction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Notes de validation
|
||||||
|
|
||||||
|
> 2026-06-05 — revue effectuée par l'orchestrateur (validation déléguée).
|
||||||
|
|
||||||
|
### Résumé de la revue
|
||||||
|
|
||||||
|
**Périmètre scaffold**
|
||||||
|
|
||||||
|
Le dossier `app_rust/system-update-gnome/` contient uniquement : `Cargo.toml`, `Cargo.lock`, `.gitignore`, `README.md`, `src/main.rs`, `src/api.rs`, `src/config.rs`. Le dossier `target/` est ignoré par `.gitignore`. Aucun fichier Rust n'a été créé en dehors de ce périmètre. Conforme.
|
||||||
|
|
||||||
|
Le `Cargo.toml` déclare `edition = "2024"` et n'a aucune dépendance externe (`[dependencies]` vide), ce qui est cohérent avec la priorité « client API avant UI GTK » et permet un premier build vérifiable sans accès réseau.
|
||||||
|
|
||||||
|
**Contrat API / capabilities**
|
||||||
|
|
||||||
|
`GET /api/capabilities` est exposé par le backend (`server/routes/index.ts`), généré par `server/services/capabilities.ts`, typé dans `shared/types.ts` (`ServerCapabilities`). Le client Rust (`api.rs`) consomme cet endpoint. La chaîne est complète pour le jalon 8.0.
|
||||||
|
|
||||||
|
**SSH direct**
|
||||||
|
|
||||||
|
Aucune référence SSH dans le code Rust. Le README rappelle explicitement la règle. Conforme.
|
||||||
|
|
||||||
|
**Credentials machines**
|
||||||
|
|
||||||
|
Le scaffold ne manipule que l'URL serveur et un token API optionnel. Pas de credential machine. Conforme.
|
||||||
|
|
||||||
|
**Sécurité token**
|
||||||
|
|
||||||
|
Le token est lu depuis la variable d'environnement `SYSTEM_UPDATE_TOKEN` ou la CLI `--token`, jamais écrit dans un fichier. La crate `keyring` est bien listée dans `tache8.md §4` et dans la checklist §11 ; son implémentation est planifiée au jalon 8.3. Acceptable à ce stade ; à réaliser avant tout déploiement.
|
||||||
|
|
||||||
|
**HTTPS**
|
||||||
|
|
||||||
|
Le client HTTP artisanal (`std::net::TcpStream`) supporte uniquement `http://` et rejette `https://` avec un test unitaire explicitement nommé `rejects_https_until_tls_client_is_added`. C'est une dette assumée et documentée. À traiter (crate `rustls` ou `reqwest` avec TLS) avant toute utilisation hors localhost.
|
||||||
|
|
||||||
|
**Erreurs structurées / version API**
|
||||||
|
|
||||||
|
Non encore couverts dans le code Rust (jalons 8.1/8.2). La structure de base existe côté backend (`apiVersion: "1"` dans capabilities). La tache8.md §5 liste les besoins. Réserve mineure.
|
||||||
|
|
||||||
|
**Cohérence inter-tâches**
|
||||||
|
|
||||||
|
- `tache1.9.md` : table `api_clients` avec `token_hash`, `scopes_json`, `status`, `revoked_at` — cohérente avec le modèle de sécurité de tache8.md.
|
||||||
|
- `tache5.md` : `GET /api/capabilities` listé parmi les endpoints API ; cohérent.
|
||||||
|
- `design_system/tokens/tokens.gnome.css` : fichier présent, référencé dans tache8.md §4 et §11 ; le scaffold ne l'utilise pas encore (normal : UI GTK pas commencée).
|
||||||
|
- `tache7.md` : référencé pour sécurité token/cache/metrics — cohérent.
|
||||||
|
- L'app ne remplace pas la webapp, elle en est cliente. Conforme à l'objectif Docker Compose.
|
||||||
|
|
||||||
|
### Réserves (non bloquantes pour ce jalon)
|
||||||
|
|
||||||
|
1. **HTTPS/TLS** : à implémenter avant tout usage hors localhost (jalon 8.3). La dette est documentée dans le code.
|
||||||
|
2. **Keyring** : le token n'est pas encore stocké via le trousseau système. À réaliser au jalon 8.3 avant toute distribution.
|
||||||
|
3. **Erreurs structurées et pagination** : jalons 8.1/8.2 non démarrés ; nécessaires avant l'UI GTK.
|
||||||
|
4. **Choix GTK4 direct vs relm4** : case du gate cochée (comparaison mentionnée dans tache8.md) mais la décision formelle est reportée au démarrage du jalon 8.4 (normal à ce stade).
|
||||||
|
|
||||||
|
### Coquilles corrigées
|
||||||
|
|
||||||
|
Dans `plan_8.md` :
|
||||||
|
- Sections 5 et 6 étaient inversées dans l'ordre de numérotation (section 6 apparaissait avant la section 5) : corrigé.
|
||||||
|
- Faute de conjugaison ligne 120 : « pour que tu commence le dev » → « pour que tu commences le dev » : corrigé.
|
||||||
Reference in New Issue
Block a user