# 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 ``. ## 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).