1e1be7f627
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>
180 lines
9.1 KiB
Markdown
180 lines
9.1 KiB
Markdown
# 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).
|