docs: fondation projet (CLAUDE.md, design system, spec + plan jalon 1)

Ignore les dépôts de référence imbriqués (linux-update-dashboard, nas-ops).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 04:41:30 +02:00
parent 032287e2ab
commit 1e1be7f627
15 changed files with 6279 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,179 @@
# Jalon 1 — Tranche verticale APT — Design
> Spec du premier jalon de la webapp de mise à jour distante Linux.
> Statut : **validé** (2026-06-04). Langue de travail : français.
> Voir aussi : `CLAUDE.md`, `deep-research-report(7).md` (étude d'architecture), `ajout.md` (volet Hermes / MCP).
## Objectif
Construire la première tranche verticale qui valide toute l'ossature de la plateforme
(UI ↔ API ↔ worker ↔ SSH ↔ JSON canonique ↔ rapport) sur le cas le plus simple :
> ajouter une machine Debian/Ubuntu → tester la connexion → rafraîchir les mises à jour APT
> en tâche de fond → afficher les paquets dans une tuile → déclencher `full-upgrade` (ou `reboot`)
> manuellement avec terminal live → archiver un rapport Markdown + log brut.
## Décisions verrouillées
| Sujet | Décision |
|-------|----------|
| Périmètre | Tranche verticale APT, Debian/Ubuntu, 1 machine à la fois |
| Stockage | SQLite via Drizzle ORM |
| File de jobs | In-process (croner / worker interne), pas de pg-boss/BullMQ |
| Auth SSH | Mot de passe (login + password + sudo password), chiffré au repos |
| Déploiement | Docker, clé maître de chiffrement via variable d'environnement |
| Auth webapp | Aucune au MVP (réseau de confiance derrière reverse proxy / VPN) |
| Exécution | Templates shell versionnés `.sh.tpl` (Mustache), esprit `nas-ops` |
| Architecture | API headless agnostique du frontend (réutilisable par TUI / MCP / Hermes) |
| Frontend | React 19 + Vite + design system Gruvbox seventies (`design_system/`) |
| Backend | Hono + TypeScript |
| Terminal live | WebSocket + xterm.js |
| Packaging code | Mono-package pnpm (client + server, un seul `package.json`) |
| Volet Hermes | Stub visuel uniquement (« à venir ») |
| Actions | `apt_full_upgrade` **et** `reboot` |
### Hors périmètre de ce jalon (jalons suivants)
Docker Compose ; profils Proxmox / Raspberry Pi OS ; serveur MCP ; intelligence Hermes ;
authentification de la webapp ; clés SSH ; déduplication multi-machines ; apt-cacher-ng persistant.
## Contrainte d'architecture transverse
Le **cœur métier (API) est headless et agnostique du frontend**. Le client web n'est qu'un
consommateur parmi d'autres ; un futur TUI, un bot de messagerie, le serveur MCP et Hermes
taperont sur la **même API interne**. Conséquence : **aucune logique métier dans le client**.
Les secrets et l'exécution restent côté backend ; les clients (web, TUI, bot) proposent et
déclenchent uniquement des actions autorisées.
## Structure du projet (mono-package pnpm, racine du dépôt)
```
system_update/
├─ shared/ # types JSON canoniques (snapshot, execution result), partagés client+server
├─ server/ # API Hono headless — cœur métier
│ ├─ index.ts
│ ├─ routes/ # machines, refresh, actions, ws
│ ├─ services/ # orchestration update, génération snapshot, rapports
│ ├─ ssh/ # wrapper ssh2 (password, sudo -S, exécution détachée)
│ ├─ templates/ # rendu Mustache + parsing JSON
│ ├─ crypto/ # chiffrement AES-256-GCM des credentials
│ ├─ jobs/ # worker in-process (croner)
│ └─ db/ # schéma + migrations Drizzle (SQLite)
├─ client/ # React 19 + Vite + design system
│ └─ src/{components,panels,features,lib}
├─ templates/apt/ # check.sh.tpl, full-upgrade.sh.tpl, reboot-check.sh.tpl
├─ reports/ # rapports .md + logs bruts (volume Docker)
├─ docker/ # Dockerfile + docker-compose.yml
└─ docs/superpowers/specs/
```
Le design system existant (`design_system/tokens/*`, `design_system/components/ui-kit.jsx`)
est consommé/porté dans `client/src/components` : `tokens.css` chargé globalement,
`data-theme` posé sur `<html>`.
## Modèle de données (SQLite / Drizzle)
- **machines** : `id, name, hostname, port, os_family, username, enc_password, enc_sudo_password,
apt_proxy_mode, apt_proxy_url, status, last_checked_at, created_at`
- **snapshots** : `id, machine_id, checked_at, status, payload_json`
(le *update availability snapshot* canonique du rapport)
- **executions** : `id, machine_id, action, mode, started_at, finished_at, status, result_json,
report_path, raw_log_path`
Les jobs de fond sont suivis en mémoire + reflétés via `machine.status` ; pas de table jobs
dédiée au MVP.
### Secrets
Chiffrement **AES-256-GCM**. Clé maître lue depuis `SU_MASTER_KEY` (env). Les credentials sont
déchiffrés uniquement en mémoire au moment de l'exécution SSH. **Jamais loggués, jamais
sérialisés vers l'API, l'UI ou (futur) le MCP.**
## Couche SSH + templates (le moteur)
- `ssh2` en authentification mot de passe.
- Commandes enveloppées `sh -c`, avec `LC_ALL=C` et un `PATH` minimal pour stabiliser les sorties
(pattern repris de `linux-update-dashboard`).
- sudo via `sudo -S` (mot de passe sudo poussé sur stdin).
- **Opérations longues détachées** : `nohup` + fichier de log + fichier d'exit-code, pour survivre
à une coupure SSH.
- Les templates `.sh.tpl` (Mustache) **produisent eux-mêmes le JSON** (esprit `nas-ops`) :
- `check.sh.tpl` → `apt-get update -qq` puis simulation `full-upgrade`, émet
`{ count, packages[], reboot_required }`.
- `full-upgrade.sh.tpl` → applique `apt-get full-upgrade` (options dpkg défensives), émet le
*execution result*.
- `reboot-check.sh.tpl` → état `reboot_required` / reboot.
- Le backend rend le template avec les variables par machine (proxy APT, etc.), pousse en SSH,
parse la **dernière ligne JSON**, et **streame** la sortie lisible vers le terminal via WebSocket.
## API headless (contrat réutilisable par TUI / MCP / Hermes)
```
GET /api/machines
POST /api/machines # crée + test-connection
POST /api/machines/:id/test-connection
GET /api/machines/:id/snapshot
POST /api/machines/:id/refresh # déclenche un job de fond
POST /api/machines/:id/actions # { action: "apt_full_upgrade" | "reboot" }
GET /api/machines/:id/executions
GET /api/machines/:id/executions/:execId
GET /api/machines/:id/executions/:execId/report
WS /api/ws/machines/:id/output # flux live + buffer rejouable
```
## Flux fonctionnels
1. **Ajout machine** — `POST /api/machines` : connexion SSH de test + détection OS
(`lsb_release` / `/etc/os-release`), chiffrement et stockage des credentials, création de la
tuile avec état initial.
2. **Refresh (tâche de fond)** — le worker rend `check.sh.tpl`, exécute en SSH, parse le snapshot
JSON, le stocke ; la tuile UI se met à jour (polling ou WS).
3. **Upgrade (manuel)** — `POST /api/machines/:id/actions { action: "apt_full_upgrade" }` : job
détaché, sortie streamée sur `WS /api/ws/machines/:id/output` ; à la fin, parsing du résultat,
écriture du rapport `.md` + log brut, persistance de l'exécution.
4. **Reboot (manuel)** — même mécanique, action `reboot`.
5. **Rapport** — `GET …/report` renvoie le Markdown archivé.
## Frontend (design system Gruvbox, layout 3 volets)
Layout `Resizable` :
- **Gauche** : panneau Hermes (stub « à venir »).
- **Centre** : dashboard de tuiles machines + bouton `+` (Popup d'ajout).
- **Droite** : terminal `xterm.js` branché sur le WebSocket.
Composants issus du design system (Button, IconButton, StatusLed, Popup, tuiles `glass`,
BatteryGauge pour les compteurs d'updates). Respect strict des règles : pas de hover (pression
3D), tooltips sur icônes seules, polices Inter / JetBrains Mono / Share Tech Mono, vérification
des deux thèmes (`data-theme`).
## Gestion d'erreurs & réduction
- Échec de connexion → `machine.status = error` + message **sans secret**.
- Template avec exit ≠ 0 → `execution.status = error`, capture des lignes stderr importantes.
- Reconnexion WS → rejoue le buffer circulaire côté serveur.
- **Réducteur déterministe APT** construit dès ce jalon : filtre les lignes utiles
(`Inst`, `Conf`, `Remv`, `Err`, `E:`, `W:`, `dpkg:`, `reboot-required`) ; le log brut complet
reste sur disque. Le JSON canonique reste propre — prépare l'intégration Hermes sans la câbler.
## Tests (vitest)
Unitaires :
- rendu de template Mustache,
- **parser de sortie APT → snapshot JSON** (sur fixtures de sortie capturées),
- round-trip chiffrement / déchiffrement des credentials,
- réducteur de lignes APT.
La couche SSH réelle est validée manuellement contre une vraie machine pour ce jalon (pas de
mock SSH lourd).
## Critères d'acceptation
- [ ] Ajout d'une machine Debian/Ubuntu via le bouton `+`, avec test de connexion réussi.
- [ ] Credentials stockés chiffrés ; aucun secret visible en base, API, logs ou UI.
- [ ] Refresh de fond produit un snapshot JSON canonique avec compteur + liste de paquets.
- [ ] La tuile affiche nom, IP, OS, compteur d'updates, paquets concernés.
- [ ] `full-upgrade` déclenché manuellement, sortie visible en direct dans le terminal xterm.js.
- [ ] `reboot` déclenchable manuellement.
- [ ] Un rapport `.md` + un log brut sont archivés après exécution.
- [ ] L'app tourne en Docker avec `SU_MASTER_KEY` en env et volumes pour SQLite + rapports.
- [ ] Tests vitest verts (parser, crypto, réducteur, rendu template).