Ajout de BLUEPRINT.md : socle réutilisable pour une nouvelle appli

Philosophie, stack, architecture, style et conventions de l'appli,
débarrassés du domaine métier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-27 18:05:16 +01:00
co-authored by Claude Opus 5.5
parent 85e841ae4a
commit 5b493c9737
+362
View File
@@ -0,0 +1,362 @@
# BLUEPRINT — Socle réutilisable
Ce fichier capture la **philosophie, la stack, l'architecture, le style et
les conventions** de l'application « Gestion des bénévoles », débarrassés de
son domaine métier (festival, créneaux, cotisations…). Il sert de point de
départ pour une nouvelle application construite sur les mêmes bases.
**Mode d'emploi pour une nouvelle appli :**
1. Copier ce fichier dans le nouveau dépôt.
2. Le donner à Claude avec le brief métier : « Crée l'appli X sur les bases
de `BLUEPRINT.md` ».
3. La première étape produit un `CLAUDE.md` propre au nouveau projet (même
plan que celui de `benevoles` : glossaire, modèle, règles, périmètre,
arborescence, commandes, conventions, déploiement, journal des étapes),
qui devient ensuite le contrat de travail.
Les passages marqués **⟨à adapter⟩** dépendent du métier ; tout le reste est
le socle à reprendre tel quel.
---
## 1. Philosophie
- **Un outil pour une petite organisation réelle** (association, 50–200
personnes), pas une plateforme générique. On résout le besoin concret,
sans sur-ingénierie, mais avec un vrai niveau de qualité (tests, sécurité,
RGPD, déploiement automatisé).
- **Le métier parle français, le code parle anglais.** Un glossaire
FR ↔ EN fige les noms une fois pour toutes ; aucun nom métier français
dans le code, aucun texte d'interface en dur nulle part.
- **`CLAUDE.md` est le contrat de travail.** Il décrit le modèle, les règles,
les choix et leurs *raisons*. Il est mis à jour **à chaque étape**, en même
temps que le code. Quand un bug est corrigé, la cause et la règle qui
l'empêche de revenir y sont notées (« bug corrigé une fois — faire Y
recréerait la même classe de bug »).
- **Étapes courtes et validées.** Une étape = une fonctionnalité cohérente,
montrée à l'utilisateur, puis validée avant de continuer. Un commit atomique
par étape, message en français. Le journal des étapes (§ 10 du `CLAUDE.md`)
garde l'historique des décisions.
- **Le cœur métier est pur et testé d'abord.** Les règles critiques vivent
dans un module isolé, sans base de données ni HTTP (fonction pure :
entrée typée → résultat typé), écrit en TDD : un cas passant + un cas
déclenchant par règle. Un `loader.py` est le seul point de contact avec la
base.
- **L'outil assiste, l'humain décide.** Les calculs « intelligents »
(solveur, suggestions) proposent ; ils ne persistent jamais rien
d'eux-mêmes. La validation reste seule juge au moment de l'écriture.
- **Erreurs = codes, pas phrases.** Le backend renvoie
`{"detail": {"code": "domaine.raison", ...params}}` ; la traduction se
fait côté locales. Les violations de règles sont typées (`BLOCKING` /
`WARNING`) avec des paramètres structurés.
- **Simplicité assumée et documentée.** Quand on simplifie (ex. « vérifié
seulement contre l'existant, pas entre deux suggestions »), on l'écrit
explicitement comme simplification assumée.
- **Retirer est une fonctionnalité.** On n'hésite pas à supprimer un concept
devenu inutile (table, règle, écran) — migration, API, front et doc
nettoyés ensemble, plutôt que de laisser du code mort.
- **Aucune dépendance locale hors Docker.** Tout se lance, se teste et se
migre via `docker compose`.
---
## 2. Stack technique
### Backend
| Rôle | Choix |
|------|-------|
| Langage | Python 3.12 |
| Gestion des dépendances | `uv` (`pyproject.toml`, groupe `dev`) |
| API | FastAPI + Uvicorn |
| ORM / schémas | SQLModel (SQLAlchemy + Pydantic) |
| Base | PostgreSQL 16 (driver `psycopg[binary]` v3) |
| Migrations | Alembic (autogenerate + `alembic check` pour vérifier modèle ↔ migrations) |
| Configuration | `pydantic-settings` (`app/config.py`, valeurs réelles dans `.env` non versionné) |
| Auth | `pyjwt` (HS256) + `bcrypt` |
| Tests | `pytest` + `pytest-cov`, **vraie base PostgreSQL de test** |
| Lint / format | `ruff` (line-length 100, règles `E F I UP B`) |
| Outils ponctuels | `openpyxl` (import/export Excel), `reportlab` + `pypdf` (PDF), `pillow` (validation d'images), `httpx` (API tierces), `ortools` (optimisation) — **⟨à adapter⟩** selon le besoin |
### Frontend
| Rôle | Choix |
|------|-------|
| Framework | React 18 + TypeScript (strict) |
| Build | Vite 5 |
| Style | Tailwind CSS 3 (thème étendu, aucune lib de composants) |
| Données serveur | TanStack React Query 5 (hooks centralisés dans `src/api/hooks.ts`) |
| Routing | React Router 6 |
| Graphiques | SVG maison (`components/charts.tsx`), aucune bibliothèque |
| Qualité | ESLint 9 + `tsc --noEmit` + script `smoke` (rendu headless via `tsx`) |
Dépendances runtime volontairement minimales : `react`, `react-dom`,
`react-router-dom`, `@tanstack/react-query`. Rien d'autre sans bonne raison.
### Infra
- Docker Compose en dev (db + backend en hot reload + frontend Vite).
- Production : images `backend` (uvicorn) et `frontend` (build statique
servi par nginx), ports exposés uniquement sur `127.0.0.1`, Caddy du VPS en
reverse proxy HTTPS (`/api/*` → backend avec préfixe retiré, le reste →
frontend).
- Dépôt Gitea auto-hébergé + Gitea Actions (`act_runner`) : tests → build des
images (registre Gitea) → SSH sur le VPS → `pull` → `alembic upgrade head`
→ `up -d`.
- Fuseau horaire : `TZ=Europe/Paris` + `tzdata` dans le conteneur.
---
## 3. Architecture
### Arborescence type
```
.
├── CLAUDE.md # contrat de travail (tenu à jour à chaque étape)
├── README.md
├── Makefile # make demo / make blank / make down / make logs
├── docker-compose.yml # dev
├── .env.example # toutes les variables, sans vraies valeurs
├── .gitea/workflows/ci-cd.yml
├── deploy/ # docker-compose.prod.yml, bloc Caddyfile, README de mise en service
├── docker/postgres-init/ # crée la base de test au premier démarrage
├── backend/
│ ├── pyproject.toml · Dockerfile · alembic.ini
│ ├── app/
│ │ ├── main.py # instance FastAPI, montage des routers
│ │ ├── config.py # Settings (pydantic-settings), get_settings() en cache
│ │ ├── db.py # engine + get_session
│ │ ├── logging_config.py # logs sans PII + identifiant de requête
│ │ ├── request_logging.py # middleware : une ligne par requête, X-Request-ID, 500 tracées
│ │ ├── api/ # un router par ressource + schemas.py + deps.py
│ │ ├── auth/ # passwords, magic_link, tokens (JWT), deps (rôles), rate_limit
│ │ ├── email/ # sender (SMTP), templates (texte), html (rendu thémé), check (diagnostic)
│ │ ├── models/ # enums.py, base.py (TimestampMixin, local_now), domain.py
│ │ ├── validation/ # cœur métier pur : models.py, engine.py, loader.py
│ │ ├── <module_métier>/ # même découpage pour chaque calcul isolé ⟨à adapter⟩
│ │ ├── i18n/ # t(key, **params) + locales/fr.json
│ │ ├── bootstrap.py # instance vierge : 1re entité racine + 1er compte admin
│ │ ├── seed.py # jeu de démonstration réaliste et déterministe
│ │ └── migrations/
│ └── tests/ # conftest.py (vraie base), factories.py, test_<module>.py
└── frontend/
├── Dockerfile · Dockerfile.prod · nginx.conf
├── tailwind.config.js # palette et primitives du design
├── scripts/smoke.tsx # rendu headless + vérifications structurelles
└── src/
├── api/ # types.ts (interface Api), schema.ts (miroir des schémas backend),
│ # http-client.ts, mock/ (même surface, en mémoire), hooks.ts, session.ts
├── app/ # AppShell.tsx, router.tsx
├── auth/ # auth-context, RequireAuth, roles (homePath par rôle)
├── components/ # ui.tsx (primitives), state.tsx (chargement/erreur), charts.tsx
├── i18n/ # t(key, params) + locales/fr.json
├── lib/ # logique pure testable (calculs, dates, env, cx)
└── pages/ # un écran par fichier, sous-dossiers si gros écran
```
### Principes d'architecture backend
- **Multi-tenant par une entité racine** (ici `Edition`) : toute entité
métier porte son `<racine>_id`, toutes les routes sont préfixées
`/<racines>/{id}/...` et la dépendance `get_<racine>` renvoie 404
`error.not_found` si elle n'existe pas. **⟨à adapter⟩** (organisation,
projet, saison…).
- **Clés primaires UUID**, champs d'audit `created_at`/`updated_at` sur
toutes les tables sauf associations (`TimestampMixin`).
- **Énumérations en `VARCHAR` + `CHECK`** (`native_enum=False`), jamais
d'ENUM natif PostgreSQL (migrations plus simples).
- **`ON DELETE CASCADE` au niveau base** sur les liens vers la racine et
entre entités : la suppression RGPD d'une personne est un vrai *hard
delete* qui emporte tout ce qui la concerne.
- **Dates naïves en heure locale** (`local_now()`), interprétées
Europe/Paris. Pas de mélange UTC/local.
- **Modules isolés pour la logique non triviale** : `models.py` (types
purs, dataclasses), `engine.py` (fonction pure), `loader.py` (seul accès
base). Testés sans HTTP.
- **Un router par ressource**, schémas Pydantic `XCreate` / `XRead` /
`XUpdate` dans `api/schemas.py`. En `PATCH` : `None`/absent = inchangé,
`[]` = vider une liste.
- **Secrets jamais renvoyés** par l'API (`xxx_set: bool` à la place ;
chaîne vide en écriture = effacer).
- **Tâches de fond** (`BackgroundTasks`) pour les emails, en *best effort* :
un échec SMTP est journalisé mais ne fait jamais échouer l'appel HTTP.
- **Nettoyage paresseux** plutôt que tâches planifiées (purge des données
périmées au passage d'une requête), tant que le volume le permet.
- **Instance vierge et démo** : `bootstrap.py` (CLI, crée la racine + le
1er compte) et `seed.py` (jeu réaliste avec des cas intéressants :
trous, conflits, données de chaque fonctionnalité).
### Principes d'architecture frontend
- **Une interface `Api` unique, deux implémentations** : client HTTP réel
et **mock en mémoire de même surface** (`VITE_API_MODE=mock|http`). Le
front se développe et se démontre sans backend ; passer de l'un à l'autre
ne change aucun écran. Le mock calcule les mêmes résultats que le backend.
- **`schema.ts` = miroir de `backend/app/api/schemas.py`**, gardé
synchrone à la main.
- **Toutes les données serveur passent par React Query** (`hooks.ts`), jamais
de `fetch` dans les pages.
- **Logique pure dans `src/lib/`**, souvent miroir d'une fonction backend
(ex. `festivalDay` ↔ `day_boundary`) : la règle est écrite à l'identique
des deux côtés et le `CLAUDE.md` le signale.
- **Auth** : JWT en `localStorage`, `Authorization: Bearer`, une 401
déconnecte ; `<RequireAuth>` sur toutes les pages métier ; page d'accueil
et onglets visibles selon le rôle.
- **Variables Vite figées au build** (`VITE_*`) : l'image de prod est
construite pour un environnement donné.
- **Téléchargements** (PDF, Excel) : blob → `<a download>` généré côté
client.
- **Chaque erreur affiche sa référence** (`X-Request-ID`) sous le message,
pour retrouver la ligne de log correspondante.
---
## 4. Sécurité et RGPD (non négociables)
- **Deux systèmes d'authentification, une même session JWT** :
- mot de passe (bcrypt, ≥ 10 caractères) pour les administrateurs ;
- **lien magique** (jeton aléatoire haché SHA-256 en base, usage unique,
TTL court) pour les autres comptes ; réponse `202` constante, sans jamais
révéler si un compte existe.
- **Rôles** appliqués côté backend par endpoint, documentés dans une
**matrice ressource × rôle** dans le `CLAUDE.md`. **⟨à adapter⟩**
(ici `COORDINATOR` / `LEAD` / `VOLUNTEER`).
- **Limiteurs de tentatives** en mémoire (connexion, lien magique,
formulaires publics, sondages vers API tierces) — `429
error.too_many_requests`.
- **Démarrage refusé en production** avec un `JWT_SECRET` par défaut, vide
ou trop court.
- **Pages publiques protégées par jeton** dans l'URL (`secrets.token_urlsafe`,
comparaison `secrets.compare_digest`, 404 générique), régénérable pour
révoquer un lien qui a fuité ; hCaptcha optionnel (désactivé si la clé est
absente).
- **Fichiers importés revalidés** (décodage réel d'image avec PIL, pas
seulement le `Content-Type`) ; exports Excel protégés contre l'injection de
formule (CWE-1236).
- **Aucune donnée nominative dans les logs** (ni nom, ni email, ni
téléphone, ni sujet d'email personnalisable) : on logue des identifiants.
Journal d'accès d'uvicorn **désactivé** (il écrivait les query strings,
donc les jetons).
- **Logs de diagnostic** : une ligne par requête (méthode, route *modèle*
+ identifiants, statut, durée, code d'erreur métier, compte) ; WARNING en
4xx, ERROR + trace complète en 5xx ; `X-Request-ID` propagé jusque dans les
tâches de fond. Erreur 422 : emplacement + type, jamais la valeur saisie.
- **Rétention paramétrable** des données nominatives sur l'entité racine ;
endpoint de suppression complète d'une personne.
- **En-têtes de sécurité** dans nginx et Caddy ; backend et frontend
n'écoutent que sur `127.0.0.1`.
- **Emails** : SMTP auto-hébergé, `EMAIL_ENABLED=false` en dev/tests (simple
journalisation), `multipart/alternative` (texte brut = source de vérité
des tests + HTML au thème de l'appli, tableaux et styles en ligne, logo en
pièce jointe inline par Content-ID plutôt qu'une URL qui fuiterait un
jeton). Commande de diagnostic SMTP dédiée.
---
## 5. Style visuel (design « néo-brutaliste »)
Identité forte, lisible, sans bibliothèque de composants : **bordures
franches noires, ombres portées dures sans flou, libellés en capitales, fond
crème, accents saturés**.
### Palette (`tailwind.config.js`)
| Jeton | Valeur | Usage |
|-------|--------|-------|
| `cream` | `#F5EFE1` | fond général |
| `ink` | `#14110E` | encre, bordures, ombres |
| `paper` | `#FFFDF7` | panneaux, cartes |
| `sand` / `sand-line` / `sand-dark` | `#EFE8D6` / `#E0D8C4` / `#E7E0CE` | barres secondaires, séparateurs |
| `yellow` / `yellow-soft` / `yellow-hover` | `#FFD23F` / `#FFE68A` / `#FFDF77` | action principale, état incomplet |
| `orange` / `orange-dark` | `#FF5C39` / `#E0421F` | urgence, danger |
| `green` | `#0E9F6E` | succès, survol de l'action principale |
| `blue` / `blue-strong` | `#A8C4FF` / `#2A6BFF` | information |
| `mauve` / `mauve-hover` | `#CBA6E3` / `#BE93D9` | dépassement / surplus |
### Typographie
- `font-display` : **Archivo** (400 → 900), titres et boutons en
`font-black`/`font-extrabold`, `uppercase`, `tracking-wide`, petites
tailles (11–12 px).
- `font-mono` : **DM Mono**, pour les heures, chiffres, codes.
- Chargées depuis Google Fonts dans `index.html`.
### Primitives (`components/ui.tsx`)
- **Ombres** : `shadow-brutal-sm` (2 px), `brutal` (3 px), `brutal-md`
(4 px), `brutal-lg` (6 px) — toutes `Npx Npx 0 #14110E`.
- **`Panel`** : `border-[3px] border-ink bg-paper` + ombre portée.
- **`PanelHeader`** : bandeau `bg-ink text-cream` en capitales (variante
hachurée `bg-hatch-ink text-yellow`).
- **`Btn`** : `border-2 border-ink`, capitales, variantes `solid` (jaune →
vert au survol), `ghost` (crème → jaune), `danger` (crème → orange), `dark`
(encre → jaune).
- **Trames d'état** en `backgroundImage` : hachures orange (critique), mauve
(surplus), encre (brouillon), sable (fermé), pointillés.
- Infobulles (`HelpTip`) en `position: fixed` positionnées par JS
(`useTooltipPosition`), bornées à l'écran — jamais en `absolute` dans un
conteneur qui défile.
- Graphiques SVG maison, palette vérifiée daltonisme/contraste sur `paper`,
**chaque graphique doublé d'une vue tableau**.
- Écrans mobiles dédiés aux rôles « terrain » (consultation simple), écrans
denses pour l'administration.
Aucun texte dans les primitives : les libellés viennent toujours de l'i18n.
---
## 6. Conventions de code
- **Code, base, fichiers en anglais** ; français uniquement dans les
locales, les commentaires de contexte métier et les commits.
- **i18n partout, même en monolingue** : backend `app.i18n.t(key,
**params)` (`str.format`), frontend `t(key, params)` (`{nom}`).
- **Commentaires qui expliquent le *pourquoi*** (contraintes, bug évité,
choix non évident), pas le *quoi*.
- **Tests backend** : vraie base PostgreSQL (créée par
`docker/postgres-init`), chaque test dans une transaction annulée
(savepoints, `join_transaction_mode="create_savepoint"`), fixtures
`autouse` qui coupent le SMTP et réinitialisent les limiteurs,
`factories.py` pour les données. Un fichier `test_<module>.py` par module
ou router ; tests d'autorisation dédiés (`test_authz.py`).
- **Tests frontend** : `typecheck` + `lint` + `smoke` (rendu SSR des écrans
avec le mock, et vérifications structurelles quand un bug visuel a déjà eu
lieu — ex. positions de grille rendues).
- **Migrations** numérotées (`0001`, `0002`…), une par étape qui touche le
modèle, vérifiées par `alembic check`.
- **Pas de nouvelle dépendance sans raison écrite** (commentaire dans
`pyproject.toml` si contrainte de version).
---
## 7. Commandes types
| Action | Commande |
|--------|----------|
| Tout lancer avec la démo | `make demo` |
| Instance vierge | `make blank EMAIL=… PASSWORD=… NAME=…` |
| Dev | `docker compose up --build` (API : http://localhost:8000/docs, front : http://localhost:5173) |
| Tests backend | `docker compose run --rm backend uv run pytest` |
| Lint / format | `docker compose run --rm backend uv run ruff check .` / `ruff format .` |
| Migration | `docker compose run --rm backend uv run alembic revision --autogenerate -m "…"` |
| Appliquer / vérifier | `… alembic upgrade head` puis `… alembic check` |
| Front | `npm run typecheck && npm run lint && npm run smoke` |
---
## 8. Méthode de travail avec Claude
1. **Étape 1 — squelette** : monorepo, Docker Compose, backend qui démarre
(`/health`), frontend vide, CI/CD, `CLAUDE.md` initial avec glossaire.
2. **Étape 2 — modèle de données complet** + migration initiale.
3. **Ensuite** : import des données existantes (analyse puis exécution,
mode simulation, rapport ligne par ligne, détection de doublons), CRUD,
cœur métier en TDD, seed de démo, authentification, puis fonctionnalités
une par une.
4. **À chaque étape** : code + tests + migration + mise à jour du
`CLAUDE.md` (modèle, règles, journal) + front (mock compris) → montrer le
résultat → attendre la validation → commit atomique en français.
5. **Revues de sécurité régulières** avant toute ouverture au public, notées
comme étapes à part entière.