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>
9.1 KiB
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(oureboot) 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)
ssh2en authentification mot de passe.- Commandes enveloppées
sh -c, avecLC_ALL=Cet unPATHminimal pour stabiliser les sorties (pattern repris delinux-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 (espritnas-ops) :check.sh.tpl→apt-get update -qqpuis simulationfull-upgrade, émet{ count, packages[], reboot_required }.full-upgrade.sh.tpl→ appliqueapt-get full-upgrade(options dpkg défensives), émet le execution result.reboot-check.sh.tpl→ étatreboot_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
- 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. - 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). - Upgrade (manuel) —
POST /api/machines/:id/actions { action: "apt_full_upgrade" }: job détaché, sortie streamée surWS /api/ws/machines/:id/output; à la fin, parsing du résultat, écriture du rapport.md+ log brut, persistance de l'exécution. - Reboot (manuel) — même mécanique, action
reboot. - Rapport —
GET …/reportrenvoie 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.jsbranché 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-upgradedéclenché manuellement, sortie visible en direct dans le terminal xterm.js.rebootdéclenchable manuellement.- Un rapport
.md+ un log brut sont archivés après exécution. - L'app tourne en Docker avec
SU_MASTER_KEYen env et volumes pour SQLite + rapports. - Tests vitest verts (parser, crypto, réducteur, rendu template).