# 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. **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. **Architecture:** Frontend React/Vite. Le `ui-kit.tsx` (design system) passe en exports ESM. L'état (machines, compteurs, machine sélectionnée, thème) remonte dans `App`, qui distribue en props au Header, au Dashboard (présentationnel), à la StatusBar et au TerminalPanel. Helpers purs (`theme`, `stats`) testés ; le reste est vérifié visuellement. **Tech Stack:** React 19, Vite 6, @xterm/xterm, @fortawesome/fontawesome-free, @fontsource/{inter,jetbrains-mono,share-tech-mono}, vitest. --- ## File Structure ``` client/src/ ├─ main.tsx # MODIF: imports CSS FA + polices ├─ App.tsx # MODIF: remontée d'état, header+statusbar+thème ├─ components/ui-kit.tsx # MODIF: ajout exports ESM (1 ligne) ├─ styles/app.css # MODIF: classes header/statusbar/input/term-header ├─ lib/ │ ├─ theme.ts # NOUVEAU: thème (getInitial/apply/next) │ ├─ theme.test.ts # NOUVEAU │ ├─ stats.ts # NOUVEAU: sumUpdates │ └─ stats.test.ts # NOUVEAU ├─ panels/ │ ├─ Header.tsx # NOUVEAU │ ├─ StatusBar.tsx # NOUVEAU │ ├─ HermesPanel.tsx # MODIF: label + Icon │ ├─ Dashboard.tsx # MODIF: présentationnel (props) │ └─ TerminalPanel.tsx # MODIF: machine + en-tête + bannière └─ features/machines/ ├─ MachineTile.tsx # MODIF: StatusLed + IconButton └─ AddMachineModal.tsx # MODIF: Popup + Button vitest.config.ts # MODIF: inclure client/**/*.test.ts package.json # MODIF: deps FA + fontsource ``` Le composant `ui-kit.tsx` ne doit JAMAIS être importé dans un test (il touche `window`/`document` au chargement → KO en environnement node). Les tests ne portent que sur `lib/theme.ts` et `lib/stats.ts` (purs, node-safe). --- ## Task 1: Brancher le design system (deps + exports + CSS) **Files:** - Modify: `package.json` - Modify: `client/src/components/ui-kit.tsx` (fin de fichier) - Modify: `client/src/main.tsx` - [ ] **Step 1: Installer les dépendances** Run: ```bash rtk pnpm add @fortawesome/fontawesome-free @fontsource/inter @fontsource/jetbrains-mono @fontsource/share-tech-mono ``` Expected: 4 paquets ajoutés dans `dependencies`, `pnpm-lock.yaml` mis à jour, install OK. - [ ] **Step 2: Exporter les composants du design system** Ajouter à la toute fin de `client/src/components/ui-kit.tsx` (après le dernier `})();`) : ```ts export { Icon, Tooltip, IconButton, Toggle, StatusLed, BatteryGauge, RadialGauge, BigRadialGauge, Popup, Button, TreeNav, Sparkline, LineChart, }; ``` Ne rien supprimer (garder `// @ts-nocheck`, l'import React, le `Object.assign(window, …)`). - [ ] **Step 3: Importer FA + polices dans main.tsx** Remplacer le contenu de `client/src/main.tsx` par : ```tsx import React from "react"; import { createRoot } from "react-dom/client"; import "@fortawesome/fontawesome-free/css/all.min.css"; import "@fontsource/inter"; import "@fontsource/jetbrains-mono"; import "@fontsource/share-tech-mono"; import "./styles/app.css"; import { App } from "./App.js"; createRoot(document.getElementById("root")!).render( , ); ``` - [ ] **Step 4: Vérifier le build** Run: `rtk pnpm check && rtk pnpm vite build` Expected: `pnpm check` 0 erreur, build OK (`dist/client` produit, le CSS FA/polices intégré). - [ ] **Step 5: Commit** ```bash rtk git add package.json pnpm-lock.yaml client/src/components/ui-kit.tsx client/src/main.tsx rtk git commit -m "feat(ui): brancher le design system (exports ESM, Font Awesome, polices offline) Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 2: Helper thème (TDD) **Files:** - Modify: `vitest.config.ts` - Create: `client/src/lib/theme.ts`, `client/src/lib/theme.test.ts` - [ ] **Step 1: Inclure les tests client dans vitest** Dans `vitest.config.ts`, remplacer la ligne `include` par : ```ts include: ["server/**/*.test.ts", "shared/**/*.test.ts", "client/**/*.test.ts"], ``` - [ ] **Step 2: Écrire le test (échec attendu)** `client/src/lib/theme.test.ts` : ```ts import { describe, it, expect, beforeEach } from "vitest"; import { nextTheme, getInitialTheme } from "./theme.js"; describe("nextTheme", () => { it("bascule dark <-> light", () => { expect(nextTheme("dark")).toBe("light"); expect(nextTheme("light")).toBe("dark"); }); }); describe("getInitialTheme", () => { beforeEach(() => { // @ts-expect-error - environnement node sans localStorage delete globalThis.localStorage; }); it("retombe sur dark sans localStorage", () => { expect(getInitialTheme()).toBe("dark"); }); it("lit la valeur persistée si présente", () => { const store: Record = { "su-theme": "light" }; // @ts-expect-error - stub minimal globalThis.localStorage = { getItem: (k: string) => store[k] ?? null }; expect(getInitialTheme()).toBe("light"); }); }); ``` - [ ] **Step 3: Lancer le test (échec)** Run: `rtk pnpm vitest run client/src/lib/theme.test.ts` Expected: FAIL — module `./theme.js` introuvable. - [ ] **Step 4: Implémenter `client/src/lib/theme.ts`** ```ts // client/src/lib/theme.ts export type Theme = "dark" | "light"; const KEY = "su-theme"; export function nextTheme(t: Theme): Theme { return t === "dark" ? "light" : "dark"; } export function getInitialTheme(): Theme { try { const v = globalThis.localStorage?.getItem(KEY); return v === "light" ? "light" : "dark"; } catch { return "dark"; } } export function applyTheme(t: Theme): void { try { document.documentElement.dataset.theme = t; globalThis.localStorage?.setItem(KEY, t); } catch { /* localStorage indisponible (mode privé) : on ignore la persistance */ } } ``` - [ ] **Step 5: Lancer le test (succès)** Run: `rtk pnpm vitest run client/src/lib/theme.test.ts` Expected: PASS (3 tests). - [ ] **Step 6: Commit** ```bash rtk git add vitest.config.ts client/src/lib/theme.ts client/src/lib/theme.test.ts rtk git commit -m "feat(ui): helper de thème dark/light persisté (TDD) Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 3: Helper stats (TDD) **Files:** - Create: `client/src/lib/stats.ts`, `client/src/lib/stats.test.ts` - [ ] **Step 1: Écrire le test (échec attendu)** `client/src/lib/stats.test.ts` : ```ts import { describe, it, expect } from "vitest"; import { sumUpdates } from "./stats.js"; describe("sumUpdates", () => { it("somme les compteurs", () => { expect(sumUpdates({ a: 2, b: 3, c: 0 })).toBe(5); }); it("retourne 0 pour un objet vide", () => { expect(sumUpdates({})).toBe(0); }); }); ``` - [ ] **Step 2: Lancer le test (échec)** Run: `rtk pnpm vitest run client/src/lib/stats.test.ts` Expected: FAIL — module introuvable. - [ ] **Step 3: Implémenter `client/src/lib/stats.ts`** ```ts // client/src/lib/stats.ts export function sumUpdates(counts: Record): number { return Object.values(counts).reduce((acc, n) => acc + n, 0); } ``` - [ ] **Step 4: Lancer le test (succès)** Run: `rtk pnpm vitest run client/src/lib/stats.test.ts` Expected: PASS (2 tests). - [ ] **Step 5: Commit** ```bash rtk git add client/src/lib/stats.ts client/src/lib/stats.test.ts rtk git commit -m "feat(ui): helper sumUpdates (TDD) Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 4: Classes CSS (header, status bar, inputs, en-tête terminal) **Files:** - Modify: `client/src/styles/app.css` - [ ] **Step 1: Mettre à jour `client/src/styles/app.css`** Remplacer tout le contenu par : ```css @import "./tokens.css"; * { box-sizing: border-box; } html, body, #root { height: 100%; margin: 0; } body { font-family: var(--font-ui); background: var(--bg-1); color: var(--ink-1); } /* Ossature : header / rangée 3 volets / status bar */ .su-app { display: flex; flex-direction: column; height: 100vh; } .su-row { flex: 1; display: flex; min-height: 0; } .su-header { height: 52px; flex: 0 0 52px; display: flex; align-items: center; gap: 12px; padding: 0 16px; background: var(--bg-2); border-bottom: 1px solid var(--border-1); } .su-header h1 { font-size: 15px; margin: 0; font-weight: 600; } .su-spacer { flex: 1; } .su-hermes { width: 280px; min-width: 200px; background: var(--bg-2); border-right: 1px solid var(--border-1); padding: 14px; overflow: auto; } .su-center { flex: 1; overflow: auto; padding: 18px; } .su-terminal-wrap { width: 360px; min-width: 320px; display: flex; flex-direction: column; background: var(--bg-0); border-left: 1px solid var(--border-1); } .su-terminal-head { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--border-1); } .su-terminal { flex: 1; min-height: 0; padding: 6px; } .su-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; } /* Status bar style tmux */ .su-statusbar { height: 26px; flex: 0 0 26px; display: flex; align-items: stretch; background: var(--bg-2); border-top: 1px solid var(--border-1); font-family: var(--font-terminal); font-size: 11px; } .su-statusbar .cell { display: flex; align-items: center; padding: 0 12px; border-right: 1px solid var(--border-1); color: var(--ink-2); } .su-statusbar .cell.mode { background: var(--accent); color: var(--bg-1); font-weight: 700; letter-spacing: 0.08em; } .su-statusbar .clock { margin-left: auto; border-right: none; border-left: 1px solid var(--border-1); } /* Champs de formulaire tokenisés */ .su-field { padding: 9px 12px; font-size: 13px; font-family: var(--font-ui); background: var(--bg-1); color: var(--ink-1); border: 1px solid var(--border-2); border-radius: 8px; outline: none; } .su-field:focus { border-color: var(--accent-soft); } ``` - [ ] **Step 2: Vérifier le build** Run: `rtk pnpm vite build` Expected: build OK. - [ ] **Step 3: Commit** ```bash rtk git add client/src/styles/app.css rtk git commit -m "feat(ui): classes layout header/statusbar/inputs/terminal Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 5: Composant Header **Files:** - Create: `client/src/panels/Header.tsx` - [ ] **Step 1: Créer `client/src/panels/Header.tsx`** ```tsx // client/src/panels/Header.tsx import { Button, IconButton } from "../components/ui-kit.js"; import type { Theme } from "../lib/theme.js"; interface Props { theme: Theme; onToggleTheme: () => void; onAdd: () => void; } export function Header({ theme, onToggleTheme, onAdd }: Props) { return ( System Update Ajouter ); } ``` - [ ] **Step 2: Vérifier la compilation** Run: `rtk pnpm check` Expected: 0 erreur. - [ ] **Step 3: Commit** ```bash rtk git add client/src/panels/Header.tsx rtk git commit -m "feat(ui): header (titre, ajout, bascule thème) Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 6: Composant StatusBar **Files:** - Create: `client/src/panels/StatusBar.tsx` - [ ] **Step 1: Créer `client/src/panels/StatusBar.tsx`** ```tsx // client/src/panels/StatusBar.tsx import { useEffect, useState } from "react"; import { sumUpdates } from "../lib/stats.js"; interface Props { machineCount: number; counts: Record; } export function StatusBar({ machineCount, counts }: Props) { const [clock, setClock] = useState(() => new Date().toLocaleTimeString("fr-FR")); useEffect(() => { const id = setInterval(() => setClock(new Date().toLocaleTimeString("fr-FR")), 1000); return () => clearInterval(id); }, []); return ( ); } ``` - [ ] **Step 2: Vérifier la compilation** Run: `rtk pnpm check` Expected: 0 erreur. - [ ] **Step 3: Commit** ```bash rtk git add client/src/panels/StatusBar.tsx rtk git commit -m "feat(ui): status bar tmux (mode, compteurs, horloge live) Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 7: Refonte MachineTile (StatusLed + IconButton) **Files:** - Modify: `client/src/features/machines/MachineTile.tsx` - [ ] **Step 1: Remplacer le contenu de `client/src/features/machines/MachineTile.tsx`** ```tsx // client/src/features/machines/MachineTile.tsx import type { MachineView } from "@shared/types.js"; import { StatusLed, IconButton } from "../../components/ui-kit.js"; interface Props { machine: MachineView; packageCount: number; selected: boolean; onSelect: (id: string) => void; onRefresh: (id: string) => void; onUpgrade: (id: string) => void; onReboot: (id: string) => void; } // Map statut machine -> statut StatusLed du design system const LED: Record = { ok: "ok", updates_available: "warn", error: "err", running: "info", unknown: "off", }; export function MachineTile({ machine, packageCount, selected, onSelect, onRefresh, onUpgrade, onReboot }: Props) { return ( onSelect(machine.id)} > {machine.name} {machine.hostname}:{machine.port} · {machine.osFamily} UPDATES{" "} {packageCount} e.stopPropagation()}> onRefresh(machine.id)} size={30} /> onUpgrade(machine.id)} size={30} /> onReboot(machine.id)} size={30} /> ); } ``` - [ ] **Step 2: Vérifier la compilation** Run: `rtk pnpm check` Expected: 0 erreur. (Le prop `selected` sera fourni par le Dashboard en Task 10.) - [ ] **Step 3: Commit** ```bash rtk git add client/src/features/machines/MachineTile.tsx rtk git commit -m "feat(ui): tuile machine avec StatusLed + IconButton (tooltips) Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 8: Refonte AddMachineModal (Popup + Button) **Files:** - Modify: `client/src/features/machines/AddMachineModal.tsx` - [ ] **Step 1: Remplacer le contenu de `client/src/features/machines/AddMachineModal.tsx`** ```tsx // client/src/features/machines/AddMachineModal.tsx import { useState } from "react"; import { Popup, Button, StatusLed } from "../../components/ui-kit.js"; interface Props { onClose: () => void; onCreated: () => void; } export function AddMachineModal({ onClose, onCreated }: Props) { const [form, setForm] = useState({ name: "", hostname: "", port: 22, username: "", password: "", sudoPassword: "" }); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const set = (k: string, v: string | number) => setForm({ ...form, [k]: v }); async function submit() { setBusy(true); setError(null); try { const res = await fetch("/api/machines", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...form, port: Number(form.port), sudoPassword: form.sudoPassword || null }), }); if (!res.ok) throw new Error((await res.json()).error ?? "Échec"); onCreated(); onClose(); } catch (e) { setError((e as Error).message); } finally { setBusy(false); } } return ( Annuler {busy ? "Test…" : "Ajouter"} > } > set("name", e.target.value)} /> set("hostname", e.target.value)} /> set("username", e.target.value)} /> set("port", e.target.value)} /> set("password", e.target.value)} /> set("sudoPassword", e.target.value)} /> {error && ( {error} )} ); } ``` - [ ] **Step 2: Vérifier la compilation** Run: `rtk pnpm check` Expected: 0 erreur. - [ ] **Step 3: Commit** ```bash rtk git add client/src/features/machines/AddMachineModal.tsx rtk git commit -m "feat(ui): modale d'ajout avec Popup + Button du design system Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 9: Refonte TerminalPanel (machine + en-tête + bannière de séparation) **Files:** - Modify: `client/src/panels/TerminalPanel.tsx` - [ ] **Step 1: Remplacer le contenu de `client/src/panels/TerminalPanel.tsx`** ```tsx // client/src/panels/TerminalPanel.tsx import { useEffect, useRef } from "react"; import { Terminal } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"; import "@xterm/xterm/css/xterm.css"; import type { MachineView } from "@shared/types.js"; import { connectOutput } from "../lib/ws.js"; import { StatusLed } from "../components/ui-kit.js"; const LED: Record = { ok: "ok", updates_available: "warn", error: "err", running: "info", unknown: "off", }; export function TerminalPanel({ machine }: { machine: MachineView | null }) { const ref = useRef(null); useEffect(() => { if (!ref.current) return; const term = new Terminal({ fontFamily: "'Share Tech Mono', monospace", fontSize: 12, theme: { background: "#1d2021", foreground: "#ebdbb2" }, convertEol: true, }); const fit = new FitAddon(); term.loadAddon(fit); term.open(ref.current); fit.fit(); if (machine) { // Bannière de séparation franche entre machines (couleur accent ANSI 33). const bar = "─".repeat(20); term.writeln(`\x1b[33m${bar} ${machine.name} (${machine.hostname}) ${bar}\x1b[0m`); } else { term.writeln("# sélectionne une machine"); } const disconnect = machine ? connectOutput(machine.id, (c) => term.write(c)) : () => {}; return () => { disconnect(); term.dispose(); }; }, [machine?.id]); return ( TERMINAL {machine && {machine.name}} {machine && {machine.hostname}} ); } ``` - [ ] **Step 2: Vérifier la compilation** Run: `rtk pnpm check` Expected: 0 erreur. (Le prop `machine` sera fourni par `App` en Task 11.) - [ ] **Step 3: Commit** ```bash rtk git add client/src/panels/TerminalPanel.tsx rtk git commit -m "feat(ui): terminal identifie la machine + bannière de séparation Répond au retour d'usage (amelioration.md): séparation franche entre machines. Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 10: Dashboard présentationnel + HermesPanel **Files:** - Modify: `client/src/panels/Dashboard.tsx` - Modify: `client/src/panels/HermesPanel.tsx` - [ ] **Step 1: Remplacer le contenu de `client/src/panels/Dashboard.tsx`** ```tsx // client/src/panels/Dashboard.tsx import type { MachineView } from "@shared/types.js"; import { MachineTile } from "../features/machines/MachineTile.js"; interface Props { machines: MachineView[]; counts: Record; selectedId: string | null; onSelect: (id: string) => void; onRefresh: (id: string) => void; onUpgrade: (id: string) => void; onReboot: (id: string) => void; } export function Dashboard({ machines, counts, selectedId, onSelect, onRefresh, onUpgrade, onReboot }: Props) { return ( Machines {machines.length === 0 && ( Aucune machine. Clique sur « Ajouter » dans l'en-tête. )} {machines.map((m) => ( ))} ); } ``` - [ ] **Step 2: Remplacer le contenu de `client/src/panels/HermesPanel.tsx`** ```tsx // client/src/panels/HermesPanel.tsx import { Icon } from "../components/ui-kit.js"; export function HermesPanel() { return ( ); } ``` - [ ] **Step 3: Vérifier la compilation** Run: `rtk pnpm check` Expected: 0 erreur. - [ ] **Step 4: Commit** ```bash rtk git add client/src/panels/Dashboard.tsx client/src/panels/HermesPanel.tsx rtk git commit -m "feat(ui): Dashboard présentationnel (props) + HermesPanel iconé Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 11: App — remontée d'état, thème, header + status bar **Files:** - Modify: `client/src/App.tsx` - [ ] **Step 1: Remplacer le contenu de `client/src/App.tsx`** ```tsx // client/src/App.tsx import { useCallback, useEffect, useState } from "react"; import type { MachineView } from "@shared/types.js"; import { api } from "./lib/api.js"; import { getInitialTheme, applyTheme, nextTheme, type Theme } from "./lib/theme.js"; import { Header } from "./panels/Header.js"; import { StatusBar } from "./panels/StatusBar.js"; import { HermesPanel } from "./panels/HermesPanel.js"; import { Dashboard } from "./panels/Dashboard.js"; import { TerminalPanel } from "./panels/TerminalPanel.js"; import { AddMachineModal } from "./features/machines/AddMachineModal.js"; export function App() { const [machines, setMachines] = useState([]); const [counts, setCounts] = useState>({}); const [selectedId, setSelectedId] = useState(null); const [adding, setAdding] = useState(false); const [theme, setTheme] = useState("dark"); const load = useCallback(async () => { const ms = await api.listMachines(); setMachines(ms); const entries = await Promise.all( ms.map(async (m) => { try { return [m.id, (await api.snapshot(m.id)).apt.count] as const; } catch { return [m.id, 0] as const; } }), ); setCounts(Object.fromEntries(entries)); }, []); useEffect(() => { const initial = getInitialTheme(); setTheme(initial); applyTheme(initial); void load(); }, [load]); const toggleTheme = () => { const t = nextTheme(theme); setTheme(t); applyTheme(t); }; const onRefresh = (id: string) => { setSelectedId(id); void api.refresh(id).then(load); }; const onUpgrade = (id: string) => { setSelectedId(id); void api.runAction(id, "apt_full_upgrade"); }; const onReboot = (id: string) => { setSelectedId(id); void api.runAction(id, "reboot"); }; const selected = machines.find((m) => m.id === selectedId) ?? null; return ( setAdding(true)} /> {adding && setAdding(false)} onCreated={load} />} ); } ``` - [ ] **Step 2: Vérifier compilation + build complet** Run: `rtk pnpm check && rtk pnpm vite build` Expected: 0 erreur TS, build OK. - [ ] **Step 3: Commit** ```bash rtk git add client/src/App.tsx rtk git commit -m "feat(ui): App orchestre état+thème, header et status bar Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 12: Vérification finale (build + tests + deux thèmes) **Files:** aucun (vérification). - [ ] **Step 1: Suite complète** Run: `rtk pnpm check && rtk pnpm test && rtk pnpm build` Expected: `check` 0 erreur ; tests verts (serveur 19 + theme 3 + stats 2 = 24) ; build OK (`dist/index.js` + `dist/client`). - [ ] **Step 2: Vérification visuelle manuelle (utilisateur, navigateur)** Lancer `pnpm dev`, ouvrir `http://localhost:5173`, vérifier : - Header avec titre, bouton « Ajouter » (icône +), bascule thème (soleil/lune) qui change l'apparence et persiste après rechargement (F5). - Les **deux thèmes** (dark ET light) restent lisibles et cohérents. - Icônes Font Awesome affichées (pas de carré vide), polices Inter/JetBrains Mono/Share Tech Mono appliquées. - Tuiles : StatusLed colorée selon l'état, 3 IconButton avec **tooltips** au survol, **pas de hover** (pression 3D au clic seulement), sélection visible (bordure accent). - Modale d'ajout = Popup (titre, bouton fermer, footer Annuler/Ajouter). - Status bar en bas : « SYSTEM UPDATE » + nb machines + nb updates + **horloge qui avance**. - Terminal : en-tête avec nom + hostname de la machine ; en sélectionnant une autre machine, **bannière de séparation** claire (ligne accent avec nom/hostname). Plus d'UUID affiché. - [ ] **Step 3: Commit éventuel de finition** S'il a fallu un ajustement après vérif visuelle, le committer : ```bash rtk git add -A rtk git commit -m "fix(ui): ajustements après vérification visuelle des deux thèmes Co-Authored-By: Claude Opus 4.8 " ``` --- ## Self-Review (couverture du spec) - **Wiring DS (exports ESM + FA + polices)** → Task 1. ✓ - **ui-kit jamais importé en test** → tests uniquement sur lib/theme + lib/stats (Tasks 2, 3) ; vitest include ajoute `client/**` mais les seuls tests client sont ces helpers purs. ✓ - **Header (titre, ajout, bascule thème)** → Task 5, câblé en Task 11. ✓ - **Status bar tmux (mode, compteurs, horloge)** → Task 6, câblée Task 11. ✓ - **Thème dark/light persisté** → Task 2 (`lib/theme`), appliqué Task 11. ✓ - **MachineTile : StatusLed + IconButton (tooltips), danger reboot** → Task 7. ✓ - **AddMachineModal : Popup + Button** → Task 8. ✓ - **Dashboard présentationnel** → Task 10. ✓ - **TerminalPanel : machine nommée + bannière de séparation (retour amelioration.md)** → Task 9. ✓ - **Remontée d'état dans App** → Task 11. ✓ - **Deux thèmes vérifiés** → Task 12 step 2. ✓ - **Tests helpers + build** → Tasks 2, 3, 12. ✓ Pas de placeholder. Noms cohérents entre tâches : `MachineTile` reçoit `selected` (Task 7) fourni par Dashboard `selectedId===m.id` (Task 10) depuis App `selectedId` (Task 11) ; `TerminalPanel` reçoit `machine` (Task 9) fourni par App `selected` (Task 11) ; helpers `getInitialTheme`/`applyTheme`/`nextTheme`/`sumUpdates` utilisés tels que définis.
Aucune machine. Clique sur « Ajouter » dans l'en-tête.