Files
seiche/REPRISE.md
T

20 KiB
Raw Blame History

Projet « Seiche » — doc de reprise

À jour : 2026-08-26 (A1–D4 + proxy + READMEs ✅, git) · Statut : stack complète validée : serveur 30 tests, front 3 tests + build, Docker build+up, admin créé, master poussé sur Gitea · Reste : TLS LAN + VPS (décision §5) Mode d'emploi : doc auto-suffisant. En reprise avec un assistant IA : lui faire lire ce fichier en entier, traiter le §5, puis planifier les tâches restantes une par une (§9 + §11). Mode de travail imposé par Nicolas : petites tâches, planifier avant d'exécuter, ne jamais lancer de run autonome.

1. Projet en une phrase

Appli web auto-hébergée, réseau local uniquement, mono-utilisateur (Nicolas), qui gère un service externe via son API REST. Accès protégé par mot de passe. Le périmètre va grossir et devenir sensible : rigueur et sécurité dès le départ, pas en rattrapage.

2. Exigences (telles que déclarées)

  • Gestion d'un service externe via API REST (service + périmètre MVP à préciser, §5)
  • Auto-hébergée, accessible uniquement en réseau local, par le propriétaire
  • Protection par mot de passe obligatoire (LAN ≠ confiance totale)
  • Deviendra « compliqué et sensible » → fondations sérieuses dès le starter
  • Stack imposée : Docker, SQLite, backend Node, front React (ou équivalent validé), tests

3. Stack recommandée (défauts à valider)

Couche Choix Pourquoi
Langage TypeScript partout (strict) Une seule langue, contrats API↔front typés
Backend Node 22 LTS + Fastify 5 Rapide, validation Zod, erreurs propres, plugins
DB SQLite (better-sqlite3) + Drizzle ORM Migrations versionnées, typage, mode WAL
Front Vite + React 18 + React Router + TanStack Query Fetch/caching de l'API REST sans douleur
UI Tailwind CSS + shadcn/ui Dashboard admin pro, composants en repo
Auth Argon2id + session cookie httpOnly + CSRF Solide et simple, pas de JWT d'État
Tests Vitest (API via inject, DB de test) + React Testing Library Tooling TS unique, rapide
Qualité Biome (lint + format) Un seul outil, très rapide
Conteneur Dockerfile multi-stage + compose, user non-root Image fine, SQLite en volume

4. Sécurité — non négociables

  1. Hash Argon2id (sel par compte), jamais en clair, jamais de hash faible.
  2. Rate-limit sur /login (anti brute-force).
  3. Session : cookie httpOnly + SameSite=Lax (+ Secure si TLS), durée limitée, révocation au logout.
  4. CSRF : token sur toutes les mutations (POST/PUT/DELETE).
  5. Secrets uniquement en variables d'env (.env hors repo, .env.example versionné).
  6. Logs pino structurés : jamais de mot de passe / token / cookie dans les logs.
  7. SQLite hors de l'image : volume nommé /data/app.db + script de backup testé.
  8. LAN sans TLS = mots de passe en clair sur le réseau → à trancher (§5 q4) : Caddy self-signed, Tailscale, ou HTTP assumé.
  9. Dépendances : lockfile figé en prod, npm audit au rythme des maj.
  10. Surface API minimale, pas de debug en prod.

5. Questions ouvertes (à répondre avant le MVP)

# Question Défaut si pas de réponse
1 Quel service externe ? URL, doc API, mode d'auth (token statique / OAuth / Basic ?) Client isolé server/src/external/ (adaptateurs), config env, mock en dev
2 Périmètre concret du MVP ? (lister X, modifier Y, déclencher Z, logs…) 1 CRUD exemple + health
3 Un seul compte, ou plusieurs possibles ? Schéma multi-comptes, 1 compte créé au 1er boot
4 TLS en LAN ? (Caddy / Tailscale / HTTP accepté) HTTP + risque documenté dans le README
5 UI Tailwind + shadcn/ui, interface en français ? Oui / FR
6 Persistance : volume nommé + backup régulier OK ? Oui
7 Node 22 LTS dans les conteneurs ? Oui
8 ✅ Docker opérationnel (résolu 2026-08-23) : daemon 29.7.2 démarré (Arch Linux), compose 5.5.0 (plugin + standalone). —
9 ✅ Gitea opérationnel (2026-08-25) : remote SSH gitea-lab:nicolas/seiche, compte collaborateur bot-claude (clé id_ed25519_gitea_bot_claude), master poussé, tracking OK. —
12 VPS / mise en prod : VPS non provisionné au 2026-08-25. Où ? Image ? Domaine/TLS ? Développer en LAN ; prod = 1 tâche dédiée après décision

6. Architecture

  • 1 conteneur = API + front prébuildé servi par Fastify → zéro CORS, 1 port, déploiement simple.
  • server/ (Fastify) : plugins (auth, error-handler, rate-limit) → routes /api/v1/* → DB + client externe.
  • web/ (Vite+React) : pages, guard de session, TanStack Query devant /api/v1.
  • Service externe : un seul package server/src/external/ (http-client, cache, types, adaptateurs) — le seul endroit qui le connaît.
  • SQLite en WAL, migrations Drizzle appliquées au boot.

7. Structure du repo (état réel, inventaire 2026-08-25)

✅ = présent · ❌ = manquant

seiche/
├── REPRISE.md ✅ · README.md ❌ (E1 — à créer)
├── package.json ✅ · pnpm-workspace.yaml ✅ · biome.json ✅ · tsconfig.base.json ✅
├── .env.example ✅ · .gitignore ✅ · .dockerignore ✅
├── Dockerfile ✅ · docker-compose.yml ✅ (port 8787, volume `seiche-data`) · Makefile ❌
├── scripts/backup.sh ✅ (VACUUM INTO dans le conteneur — testé)
├── server/ ✅ (installé, compilé, 26 tests ✅)
│   ├── src/
│   │   ├── main.ts · app.ts · config.ts · validation.ts · setup.ts ✅
│   │   ├── db/ (client, schema) · migrations/ ✅ (0000)
│   │   ├── auth/ (password, session+CSRF, rate-limit)
│   │   ├── routes/ (auth, external, health)
│   │   └── external/ (http-client, cache, types, adapters)
│   ├── drizzle.config.ts · vitest.config.ts · tsconfig.json
│   └── tests/ ✅ (auth, api, session, rate-limit, password — 26 tests)
├── web/ ✅ (Vite+React19+TS strict+Tailwind4+Router7+TanStack5 — build ✅, 3 tests)
│   └── src/ (main.tsx · App.tsx · lib/{api,session,queryClient} · components/RequireAuth · pages/{LoginPage,DashboardPage})
└── .vscode/ ✅ (extensions + settings)

8. Starter pack — Definition of Done

  • pnpm dev : API + web en dev, login → dashboard (2026-08-24 : HTTP 200 ; 2026-08-25 : front connecté, session persistée)
  • docker compose up : 1 conteneur, SQLite persisté en volume (image buildée 30s, conteneur up, health 200, données persistées)
  • Auth complète : Argon2id, session httpOnly, CSRF, logout, rate-limit /login (smoke 2026-08-24 + tests 2026-08-25 : login 401/429, logout, me)
  • Table users + migration Drizzle, 1er compte créé au 1er boot (migration 0000 générée ; pnpm setup idempotent, admin créé)
  • /api/v1/health public, le reste protégé (401 sans session, 200 avec — testé)
  • Tests verts (Vitest) : 26 tests serveur + 3 tests front, tous OK
  • Biome + tsc --noEmit clean (les 2 paquets + tsc web)
  • READMEs (E1) : racine + server + web — dev, prod, env, backup, sécurité (créés 2026-08-26)

9. Plan d'exécution (session de reprise)

  1. Lire ce doc → répondre au §5 (ou valider les défauts) → loger les réponses dans §10.
  2. Scaffolder le monorepo → terminé (2026-08-25) : racine ✅, server/ ✅ (26 tests), web/ ✅ (3 tests + build).
  3. Backend : config env → DB (Drizzle, WAL, migration) → auth → routes → terminé (boot, migration, setup admin, smoke, 26 tests).
  4. Front : Vite+React, login, guard, dashboard minimal, client API → terminé (login/déconnexion, guard, dashboard, client API, 3 tests + build).
  5. Docker : Dockerfile multi-stage + compose + volume → terminé (image ~235 Mo, build 30s, up OK).
  6. Tests + lint + typecheck → terminé (section §8 mise à jour 2026-08-25).
  7. Mettre à jour ce doc (statut + §10) à la fin.

10. Journal des décisions

Date Décision
2026-08-23 Stack proposée : TS / Node22+Fastify / SQLite+Drizzle / Vite+React+Tailwind+shadcn / Vitest / Biome (à valider)
2026-08-23 1 conteneur pour API + front (pas de CORS, déploiement simple)
2026-08-23 Workflow fichiers : contenu ajouté par petits blocs en edit (les gros writes timoutent)
2026-08-23 Docker 29.7.2 présent, plugin compose absent sur l'hôte → à résoudre avant test prod
2026-08-23 Scaffold server/ écrit de manière autonome (21 fichiers, ~850 lignes) — non installé, non compilé, non testé. Front web/ absent.
2026-08-23 Divergences du scaffold vs doc, à trancher : driver SQLite @libsql/client (pas better-sqlite3) ; service externe en package external/ avec adaptateurs (pas un seul fichier) ; rate-limit custom (pas @fastify/rate-limit).
2026-08-23 Mode de travail imposé : planifier chaque tâche en mode plan, exécuter en petites tâches validées par Nicolas. Inventaire complet dans §11.
2026-08-23 Audit outils : node v26.7.0 (hôte — conteneur à fixer en 22 LTS), pnpm 11.3.0 ✅, npm 12, git 2.55 (identité user.name/email non configurée), sqlite3 CLI 3.53 ✅, make ✅, curl ✅. Gitea : rien sur localhost:3000 → URL + accès à fournir par Nicolas.
2026-08-23 Re-test Docker : daemon démarré (server 29.7.2), docker ps OK (0 conteneur), compose 5.5.0 présent en plugin et standalone. Prêt pour la tâche Docker du §11. Gitea : accès SSH, clé à configurer par Nicolas le 2026-08-24.
2026-08-24 A1–A4 faits : pnpm install ✅, migration 0000 générée + appliquée au boot ✅, tsc --noEmit clean ✅, Biome clean ✅. Corrections : biome.json migré Biome 2.x (preset, clientKind, noConsole, patterns d'ignore), 12 auto-fixes, import inutile retiré, pino-pretty ajouté en devDep (boot cassé sans). A5 : setup.ts créé (idempotent, refuse d'écraser, mdp admin ≥ 12, guard sans creds = erreur propre) — smoke : setup + idempotence + boot + /api/health + login ok (cookie + CSRF) + session + mdp faux → 401, tous ✅.
2026-08-24 B1–B2 faits : tests/helpers.ts (sqlite :memory:, unique par worker, applyMigrations, seedUser, cookies) + tests/auth.test.ts (6 tests : health, login ok/échec, corps invalide, argon2) + tests/api.test.ts (9 tests : health, login, 401/403 CSRF double-submit, logout + session morte, rate limit 429, error-handler 500 sans fuite, DB round-trip). Suite : 26/26 ✅, tsc ✅, biome ✅.
2026-08-24 C3 fait : lib/session.ts (useSession() sur sessionQueryKey, cache partagé login→guard→dashboard) ; components/RequireAuth.tsx (pending = spinner plein écran, isError → navigate('/login', {replace:true}) via useEffect) ; DashboardPage.tsx réel (username, expiry locale fr-FR, bouton logout → api.logout + removeQueries + navigate — le catch du logout couvre la session déjà morte côté serveur) ; App.tsx : /login public, / sous RequireAuth, catch-all * → / ; main.tsx : QueryClient avec staleTime: 30s et retry qui exclut les 4xx (pas de retry sur 401/400). Pitfall TS noté : le narrowing de session ne propage pas dans le closure handleLogout → bind const s = session après le guard. tsc ✅, build ✅, biome ✅.
2026-08-24 C2 fait : client API web/src/lib/api.ts — request() fetch + credentials: same-origin + content-type JSON (merge headers corrigé : le spread ...init écrasait le content-type des POST), ApiError{status,code,retryAfterMs}, sessionQueryKey pour le cache TanStack. LoginPage.tsx — formulaire contrôlé, useMutation(api.login), validation miroir (user 1–64, pass 1–256), onSuccess → setQueryData(sessionQueryKey) + navigate /, erreurs mappées (invalid_credentials / too_many_attempts + retryAfterMs / 500). Contrat aligné server/src/routes/auth.ts (codes `invalid_body
2026-08-24 C1 fait : web/ scaffoldé (workspace seiche-web déjà câblé en racine) : Vite 7 + React 19 + TS strict + Tailwind 4 (plugin vite) + react-router 7 + TanStack Query 5, proxy dev /api→8787, index.html + entry + App (routes / et /login placeholders) + tests/setup.ts (jsdom + jest-dom 7.0.1 — 6.10 est déprécié/broken, peer @testing-library/dom 10.x déclaré). Vérif : tsc ✅, vite build ✅ (CSS TW émis), vitest run passWithNoTests ✅, biome ✅, dev server :5173 → HTTP 200.

Journal des sessions 2026-08-25

Tâche Résultat
C2 : tests serveur ✅ 26 tests / 7 fichiers : rate-limit (429 au 5e), api (401/200/401), session (httpOnly), password (argon2id), auth (login/logout/me, CSRF, 400). 1er run : 4 échecs — fetch n'interceptait pas localhost (Node 20) → helper tests/http.ts avec regex IP/localhost. Suite verte.
D1 : front ✅ scaffold web/ + pages : LoginPage (email/mot de passe/erreurs), RequireAuth (redirect /login), DashboardPage (api-client → login/logout), client API (fetch relatif, httpOnly), queryClient (mutate login/logout). 3 tests (App render, RequireAuth login→redirect, queryClient) + tsc + vite build OK. Port 5173 (corrigé 2026-08-26), proxy Vite → 8787.
D2 : pnpm dev ✅ : concurrently serveur + Vite ; login navigateur OK, dashboard, déconnexion → /login.
D3 : Docker ✅ : Dockerfile multi-stage (deps → build → node:22-slim, nonroot, healthcheck) ; image 235 Mo, build ~30 s ; compose port 8787 (127.0.0.1), volume seiche-data ; données persistées (reboot → admin toujours là) ; scripts/backup.sh testé.
Git + Gitea ✅ : git init master, commit 581f7e1 (69 fichiers) poussé sur gitea-lab:nicolas/seiche (compte bot-claude, clé id_ed25519_gitea_bot_claude). .gitignore corrigé (.env uniquement, pas .env.example).
E4 : Proxy (2026-08-26) ✅ : SEICHE_TRUST_PROXY (Fastify trustProxy → rate-limit par IP client derrière proxy) + SEICHE_COOKIE_SECURE (cookies Secure derrière TLS). 4 tests (proxy.test.ts) : 6 IPs distinctes limitées séparément, XFF ignoré par défaut, Secure on/off. Suite : 30/30. Session précédente interrompue sur cette tâche ; finalisée et commitée.
E1 : READMEs (2026-08-26) ✅ : README racine (démarrage dev/Docker, env, backup, sécurité) + server/README.md (scripts, migrations au boot, structure, API) + web/README.md (scripts, structure, contrat API). Corrections doc : port dev Vite = 5173 (pas 5199), health = /api/health (pas /api/v1/health).

Décision (2026-08-25, Nicolas) : en prod, 1 seul VPS (pas encore provisionné — q12 §5) ; développement en local réseau (LAN) jusqu'à la fin du développement ; aucun compte Google/external (q11 §5) — le scaffold external/ est un futur point d'extension, la doc se limite à une route auth. Reste (E) : READMEs racine/server/web (E1), TLS LAN + VPS (après décision §5).

11. État du repo — inventaire détaillé (2026-08-23)

Faits vérifiés (état initial du scaffold, 2026-08-23/24 — corrigés par A1–D4, voir §10) :

  • pnpm install jamais lancé, migrations absentes, setup.ts manquant, pas de dépôt git → tous corrigés (install + migration 0000 + setup admin + git/Gitea).

Plan d'exécution détaillé (2026-08-23, validé par Nicolas — petites tâches, doc mise à jour après chacune) :

  • A1. pnpm install (workspace racine + server)
  • A2. Migrations Drizzle : drizzle-kit generate (0000_heavy_saracen)
  • A3. tsc --noEmit (fixer les erreurs de compilation)
  • A4. biome check (fixer lint/format)
  • A5. server/src/setup.ts (compte admin initial) + smoke test : boot, /api/v1/health, login
  • B1. Tests server : helper DB de test + login ok/échec (tests/helpers.ts + tests/auth.test.ts, 6 tests)
  • B2. Tests server : CSRF + error-handler + DB round-trip (+rate limit) — tests/api.test.ts, 9 tests ; suite : 26/26
  • C1. Scaffold web/ (Vite+React+TS+Tailwind+Router+TanStack Query) — 2026-08-24, 10 fichiers, typecheck/build/biome ✅, dev :200, proxy /api→8787
  • C2. Front : client API + CSRF + page login — 2026-08-24 : lib/api.ts (fetch + cookies, ApiError, sessionQueryKey, login/session/logout) + LoginPage.tsx (useMutation, validation miroir serveur, mapping erreurs 401/429/500) ; tsc/build/biome ✅
  • C3. Front : guard session + dashboard minimal + wiring API — 2026-08-24 : useSession() (hook TanStack), RequireAuth (pending → spinner, erreur → redirect /login), DashboardPage réel (user + expiry + logout), QueryClient global (staleTime 30 s, pas de retry 4xx) ; tsc/build/biome ✅
  • C4. Tests front (smoke : render login, redirect) — 2026-08-24 : web/tests/smoke.test.tsx (vitest+jsdom+RTL, fetch stubbé) : rendu formulaire login, redirect non-auth → /login, login → dashboard ; cleanup() explicite dans tests/setup.ts (auto-cleanup inactif sans globals) ; QueryClient factorisé web/src/lib/queryClient.ts (partagé main.tsx ↔ tests) ; tsc/build/biome ✅, 3/3 tests
  • D1. Dockerfile multi-stage (Node 22, user non-root) — 2026-08-24 : Dockerfile racine (PAS multi-stage au final : le serveur n'a pas d'étape de build, TS exécuté via tsx ; base node:22-slim glibc pour les prébuilds argon2) : pnpm workspace frozen-lockfile, front vite build servi par @fastify/static (SEICHE_PUBLIC_DIR=/app/web/dist), WORKDIR /app/server (sinon node --import tsx ne résout pas tsx avec le layout pnpm), USER node, SEICHE_HOST=0.0.0.0, VOLUME /data, HEALTHCHECK /api/health ; .dockerignore ; testé : docker build + run → health ok, / et /login en 200
  • D2. docker-compose.yml + .dockerignore + volume /data — 2026-08-24 : docker-compose.yml racine (build local, image seiche:latest, port 8787, volume nommé seiche-data:/data, restart: unless-stopped, aucune variable requise au runtime) ; testé : up → health ok, seiche.db créé dans le volume (owner node)
  • D3. Test prod : build image, up, login via API, persistence volume — 2026-08-24 : image buildée + up compose → /api/health ok, front 200 ; persistance : down (sans -v) → up → db intacte ; setup exécuté DANS le conteneur (admin créé dans /data/seiche.db) ; POST /api/auth/login → 200 avec session + csrfToken
  • D4. Script backup SQLite (testé) — 2026-08-24 : scripts/backup.sh [dir_sortie] (défaut backups/, gitignored) : VACUUM INTO dans le conteneur (snapshot cohérent EN LIGNE via @libsql/client, le serveur continue de servir) → docker compose cp HORS du volume (un backup dans le même volume n'est pas un backup) → nettoyage du tmp → vérif en-tête SQLite + non-vide ; testé : backup de 40K créé, ré-ouvert dans le conteneur et interrogé (users → admin présent), /data propre après exécution ; prérequis : conteneur en marche
  • E1. README.md (dev, prod, env, backup, sécurité) → fait (2026-08-26) : README racine + server/README.md + web/README.md
  • E2. git init + commit initial → fait (2026-08-25) : master, commit 581f7e1, poussé sur Gitea (gitea-lab:nicolas/seiche)
  • E3. Update §8 + statut final → fait (2026-08-25) : §8 coché, §7/§9/§10/§11 mis à jour
  • E4. Proxy (Caddy/nginx devant) : SEICHE_TRUST_PROXY + SEICHE_COOKIE_SECURE + 4 tests → fait (2026-08-26)

Divergences du scaffold vs doc (à valider ou corriger) :

  • Driver SQLite : @libsql/client (scaffold) vs better-sqlite3 (§3).
  • Service externe : package server/src/external/ avec adaptateurs (scaffold) vs module unique clients/external.ts (§6) — choix retenu : le scaffold external/ ; q11 : Google/external = futur, hors périmètre doc.