16 KiB
Projet « Seiche » — doc de reprise
À jour : 2026-08-24 (A1–A5 ✅) · Statut : scaffold backend installé, compilé, linté, boot + setup admin + smoke login OK — front absent 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
- Hash Argon2id (sel par compte), jamais en clair, jamais de hash faible.
- Rate-limit sur /login (anti brute-force).
- Session : cookie httpOnly + SameSite=Lax (+ Secure si TLS), durée limitée, révocation au logout.
- CSRF : token sur toutes les mutations (POST/PUT/DELETE).
- Secrets uniquement en variables d'env (
.envhors repo,.env.exampleversionné). - Logs pino structurés : jamais de mot de passe / token / cookie dans les logs.
- SQLite hors de l'image : volume nommé
/data/app.db+ script de backup testé. - LAN sans TLS = mots de passe en clair sur le réseau → à trancher (§5 q4) : Caddy self-signed, Tailscale, ou HTTP assumé.
- Dépendances : lockfile figé en prod,
npm auditau rythme des maj. - 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 auto-hébergé : URL + nom du repo à confirmer. Accès SSH choisi ; Nicolas configure la clé le 2026-08-24. (rien sur localhost:3000 au 2026-08-23) |
git init local d'abord, remote Gitea dès que la clé sera opérationnelle |
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-23)
✅ = présent · ❌ = manquant
seiche/
├── REPRISE.md ✅ · README.md ❌
├── package.json ✅ · pnpm-workspace.yaml ✅ · biome.json ✅ · tsconfig.base.json ✅
├── .env.example ✅ · .gitignore ✅ · .dockerignore ❌
├── Dockerfile ❌ · docker-compose.yml ❌ · Makefile ❌
├── server/ ✅ (scaffold ~850 lignes — non installé, non compilé, non testé)
│ ├── src/
│ │ ├── main.ts · app.ts · config.ts · validation.ts
│ │ ├── db/ (client, schema) · migrations ❌ (à générer)
│ │ ├── auth/ (password, session+CSRF, rate-limit)
│ │ ├── routes/ (auth, external, health)
│ │ ├── external/ (http-client, cache, types, adapters/{http,example,index})
│ │ └── setup.ts ❌ (referencé par `pnpm setup`, absent)
│ ├── drizzle.config.ts · vitest.config.ts · tsconfig.json
│ └── test/ ❌ (aucun test)
└── web/ ❌ (front entier absent)
8. Starter pack — Definition of Done
pnpm dev: API + web en dev, login → dashboarddocker compose up: 1 conteneur, SQLite persisté en volume- Auth complète : Argon2id, session httpOnly, CSRF, logout, rate-limit /login
- Table
users+ migration Drizzle, 1er compte créé au 1er boot /api/v1/healthpublic, le reste protégé- Tests verts (Vitest) : login ok/échec, CSRF, error-handler, DB round-trip
- Biome +
tsc --noEmitclean - README : dev, prod, env, backup, sécurité
9. Plan d'exécution (session de reprise)
- Lire ce doc → répondre au §5 (ou valider les défauts) → loger les réponses dans §10.
Scaffolder le monorepo→ partiel (2026-08-23) : racine ✅,server/✅ (non vérifié),web/❌.- Backend : config env → DB (Drizzle, WAL, migration) → auth (Argon2id, session, CSRF, rate-limit) → routes. → code écrit ; reste : installer, migrer, compiler, tester.
- Front : Vite+React, login, guard, dashboard minimal, client API. → à faire.
- Docker : Dockerfile multi-stage + compose + volume.
- Tests + lint + typecheck → cocher le §8.
- 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 :5199 → HTTP 200. |
11. État du repo — inventaire détaillé (2026-08-23)
Faits vérifiés :
pnpm installjamais lancé : pas denode_modules, pas depnpm-lock.yaml→ aucun fichier du scaffold n'a été compilé ni exécuté.- Migrations Drizzle absentes (
drizzle-kit generatenon lancé) alors quedb/client.tsles applique au boot. server/src/setup.tsmanquant alors queserver/package.jsonle référence (pnpm setup).- Pas de dépôt git.
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),DashboardPageré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 danstests/setup.ts(auto-cleanup inactif sansglobals) ; 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 :
Dockerfileracine (PAS multi-stage au final : le serveur n'a pas d'étape de build, TS exécuté via tsx ; basenode:22-slimglibc pour les prébuildsargon2) : pnpm workspace frozen-lockfile, frontvite buildservi par @fastify/static (SEICHE_PUBLIC_DIR=/app/web/dist),WORKDIR /app/server(sinonnode --import tsxne 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/loginen 200 - D2. docker-compose.yml + .dockerignore + volume /data — 2026-08-24 :
docker-compose.ymlracine (build local, imageseiche:latest, port 8787, volume nomméseiche-data:/data,restart: unless-stopped, aucune variable requise au runtime) ; testé : up → health ok,seiche.dbcréé 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/healthok, front 200 ; persistance :down(sans -v) →up→ db intacte ;setupexé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éfautbackups/, gitignored) :VACUUM INTOdans le conteneur (snapshot cohérent EN LIGNE via@libsql/client, le serveur continue de servir) →docker compose cpHORS 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é)
- E2.
git init+ commit initial (identité git à fournir par Nicolas) - E3. Update §8 (Definition of Done) + statut final de ce doc
Divergences du scaffold vs doc (à valider ou corriger) :
- Driver SQLite :
@libsql/client(scaffold) vsbetter-sqlite3(§3). - Service externe : package
server/src/external/avec adaptateurs (scaffold) vs module uniqueclients/external.ts(§6) — fonctionnellement équivalent, à confirmer.