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:
co-authored by
Claude Opus 5.5
parent
85e841ae4a
commit
5b493c9737
+362
@@ -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.
|
||||
Reference in New Issue
Block a user