From 1310bc16376ef4e7758db77e6855ed8b98736170 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Fri, 5 Jun 2026 05:09:14 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20plan=20d'impl=C3=A9mentation=20jalon=20?= =?UTF-8?q?2=20(polish=20design=20system)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../2026-06-05-jalon2-polish-design-system.md | 894 ++++++++++++++++++ 1 file changed, 894 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-05-jalon2-polish-design-system.md diff --git a/docs/superpowers/plans/2026-06-05-jalon2-polish-design-system.md b/docs/superpowers/plans/2026-06-05-jalon2-polish-design-system.md new file mode 100644 index 0000000..c5f99a3 --- /dev/null +++ b/docs/superpowers/plans/2026-06-05-jalon2-polish-design-system.md @@ -0,0 +1,894 @@ +# Jalon 2 — Polish design system — Implementation Plan + +> **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

+
+ + +
+ ); +} +``` + +- [ ] **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 ( +
+
SYSTEM UPDATE
+
{machineCount} machine{machineCount > 1 ? "s" : ""}
+
{sumUpdates(counts)} update{sumUpdates(counts) > 1 ? "s" : ""}
+
{clock}
+
+ ); +} +``` + +- [ ] **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 ( + + + + + } + > +
+ 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.