From 5b493c97371b53de3387ee389bcc4d2f33fbacbb Mon Sep 17 00:00:00 2001 From: AdamThomas Date: Tue, 27 Oct 2026 18:05:16 +0100 Subject: [PATCH] =?UTF-8?q?Ajout=20de=20BLUEPRINT.md=20:=20socle=20r=C3=A9?= =?UTF-8?q?utilisable=20pour=20une=20nouvelle=20appli?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Philosophie, stack, architecture, style et conventions de l'appli, débarrassés du domaine métier. Co-Authored-By: Claude Opus 5.5 --- BLUEPRINT.md | 362 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 BLUEPRINT.md diff --git a/BLUEPRINT.md b/BLUEPRINT.md new file mode 100644 index 0000000..cddaba8 --- /dev/null +++ b/BLUEPRINT.md @@ -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 +│ │ ├── / # 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_.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 `_id`, toutes les routes sont préfixées + `//{id}/...` et la dépendance `get_` 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 ; `` 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 → `` 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_.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.