Étape 1 : squelette du projet
- Monorepo backend/ + frontend/ + deploy/ - Backend FastAPI/SQLModel/Alembic/uv qui démarre (health check, config, i18n, base de test PostgreSQL, pytest) - docker-compose de développement (db + backend hot-reload) - Chaîne CI/CD GitHub Actions : lint + tests, build image, push GHCR, déploiement SSH sur le VPS à chaque push main - CLAUDE.md : vocabulaire du domaine, conventions, commandes Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
# CLAUDE.md — Gestion des bénévoles
|
||||
|
||||
Application web de gestion des bénévoles pour un festival associatif (50–200
|
||||
bénévoles, 4 jours montage/démontage inclus). Interface en français, code en
|
||||
anglais.
|
||||
|
||||
Ce fichier est le contrat de travail. Il est tenu à jour à chaque étape.
|
||||
|
||||
---
|
||||
|
||||
## 1. Vocabulaire du domaine (glossaire FR ↔ EN)
|
||||
|
||||
Le métier parle français, le code et la base parlent anglais. Toute entité,
|
||||
colonne, route et variable utilise la colonne « Code / base ».
|
||||
|
||||
| Métier (brief) | Code / base |
|
||||
|------------------------------------|-----------------------------------------------|
|
||||
| Édition | `Edition` |
|
||||
| Bénévole | `Volunteer` |
|
||||
| Compétence | `Skill` |
|
||||
| Poste | `Position` |
|
||||
| Référent d'un poste | `Position.lead_account_id` → `Account` |
|
||||
| Créneau | `Shift` |
|
||||
| Disponibilité | `Availability` |
|
||||
| Affectation | `Assignment` |
|
||||
| Compte (utilisateur connecté) | `Account` |
|
||||
| Découpage de journée (paramétrable)| `DayPart` (`code`, `label`, `start_time`, `end_time`, `order`) |
|
||||
| Lien magique | `MagicLinkToken` |
|
||||
|
||||
Énumérations :
|
||||
|
||||
| Métier | Code |
|
||||
|------------------------------------|-----------------------------------------------|
|
||||
| Statut d'affectation | `PROPOSED` / `CONFIRMED` / `DECLINED` / `CANCELLED` |
|
||||
| Origine d'affectation | `SOLVER` / `MANUAL` |
|
||||
| Verrouillée | `Assignment.locked: bool` |
|
||||
| Rôle | `COORDINATOR` / `LEAD` / `VOLUNTEER` |
|
||||
| Disponibilité (tri-état) | `AVAILABLE` / `IF_NEEDED` / `UNAVAILABLE` |
|
||||
| Catégorie de poste | `BAR` / `TICKETING` / `WELCOME` / `PARKING` / `CLEANING` / `CATERING` / `SETUP` / `STAGE` / `OTHER` |
|
||||
|
||||
### Distinction critique (ne jamais confondre, ni en base ni dans l'UI)
|
||||
|
||||
- **`Availability`** = une *déclaration* du bénévole : « je peux venir tel
|
||||
jour, telle demi-journée ».
|
||||
- **`Assignment`** = une *décision* de l'organisation : « tu tiens le bar
|
||||
samedi 18 h – 22 h ».
|
||||
|
||||
Deux tables distinctes, deux écrans distincts, deux vocabulaires distincts.
|
||||
|
||||
---
|
||||
|
||||
## 2. Modèle de domaine
|
||||
|
||||
- **`Edition`** : année, `start_date`, `end_date`, `retention_days`
|
||||
(durée de rétention des données nominatives, paramétrable), liste de
|
||||
`DayPart`.
|
||||
- **`DayPart`** : découpage d'une journée propre à l'édition (matin,
|
||||
après-midi, soirée… nombre et bornes libres). Une `Availability` référence
|
||||
un `DayPart` + une date.
|
||||
- **`Volunteer`** : `email` (identifiant fonctionnel unique **par édition**,
|
||||
nullable pour les bénévoles créés à la main), `first_name`, `last_name`,
|
||||
`phone`, `tshirt_size`, `diet`, `comment`. Pas de date de naissance.
|
||||
- **`Skill`** : référentiel par édition (SST, permis B, régie son, langues…).
|
||||
N-N avec `Volunteer`. N-N avec `Shift` (compétences requises).
|
||||
- **`Position`** : `name`, `location`, `category`, `lead_account_id`.
|
||||
- **`Shift`** : `position_id`, `start_at`, `end_at` (peut finir le lendemain),
|
||||
`min_staff`, `ideal_staff`, compétences requises (N-N `Skill`).
|
||||
- **`Assignment`** : `volunteer_id`, `shift_id`, `status`, `source`, `locked`.
|
||||
- **`Account`** : `email`, `role`, `edition_id`, `volunteer_id` (nullable).
|
||||
- **`MagicLinkToken`** : `account_id`, `token_hash`, `expires_at`, `used_at`.
|
||||
|
||||
Multi-édition : toute entité métier porte un `edition_id`.
|
||||
|
||||
### Rôles et accès
|
||||
|
||||
- **`COORDINATOR`** : accès complet, construit et publie les plannings.
|
||||
- **`LEAD`** (référent) : lecture sur toute l'édition, écriture limitée à ses
|
||||
propres postes.
|
||||
- **`VOLUNTEER`** : accès à ses seules données.
|
||||
|
||||
Authentification par lien magique envoyé par email, sans mot de passe. L'envoi
|
||||
d'email est **stubé** pour l'instant (le lien est écrit dans les logs). Un
|
||||
bénévole sans email n'a pas de `Account` et ne se connecte pas ; il est géré
|
||||
entièrement par la coordination.
|
||||
|
||||
---
|
||||
|
||||
## 3. Moteur de validation des affectations — cœur du produit
|
||||
|
||||
Module isolé `backend/app/validation/`. **Tests écrits avant l'implémentation**,
|
||||
un cas passant + un cas déclenchant par règle.
|
||||
|
||||
Retourne une liste de violations typées `BLOCKING` ou `WARNING`. Chaque
|
||||
violation est un **code** + des paramètres structurés, jamais une phrase (la
|
||||
traduction se fait via `locales/fr.json`).
|
||||
|
||||
| Code | Sévérité | Règle |
|
||||
|---------------------------|------------|-------------------------------------------------------------|
|
||||
| `SHIFT_OVERLAP` | `BLOCKING` | Deux créneaux d'un même bénévole se chevauchent. |
|
||||
| `OUTSIDE_AVAILABILITY` | `WARNING` | Un `DayPart` couvert par le créneau est `UNAVAILABLE` ou sans déclaration. |
|
||||
| `DAILY_HOURS_EXCEEDED` | `WARNING` | > 8 h 00 d'affectations cumulées sur une même journée. |
|
||||
| `INSUFFICIENT_REST` | `WARNING` | < 8 h 00 entre la fin d'un créneau et le début du suivant. |
|
||||
| `MISSING_REQUIRED_SKILL` | `WARNING` | Le bénévole n'a pas une compétence requise par le créneau. |
|
||||
|
||||
Sémantique figée :
|
||||
|
||||
- Statuts pris en compte pour chevauchement / cumul / repos :
|
||||
`PROPOSED` + `CONFIRMED`. `DECLINED` et `CANCELLED` sont ignorés.
|
||||
- « Journée » (cumul 8 h) : du début du premier `DayPart` du jour au début du
|
||||
premier `DayPart` du lendemain.
|
||||
- Cumul : `> 8 h 00` strict (8 h 00 pile passe).
|
||||
- Repos : `< 8 h 00` strict.
|
||||
- Créneaux jointifs (`end_at == start_at`) : **pas** un chevauchement.
|
||||
- Une violation `BLOCKING` empêche l'enregistrement, sans dérogation.
|
||||
- Une violation `WARNING` est retournée à l'appelant mais n'empêche rien
|
||||
(l'équipe connaît des exceptions légitimes).
|
||||
|
||||
---
|
||||
|
||||
## 4. Périmètre
|
||||
|
||||
### Premier incrément (en cours)
|
||||
|
||||
1. Modèle de données complet + migrations Alembic.
|
||||
2. Import CSV/Excel (un seul fichier bénévoles + disponibilités), en deux
|
||||
temps : analyse (colonnes détectées, aperçu) puis exécution avec mapping
|
||||
validé. Rapport ligne par ligne. Détection des doublons : upsert sur
|
||||
`email` ; sans email, détection sur (`last_name`, `first_name`) signalée
|
||||
sans upsert automatique. Mode simulation qui ne persiste rien.
|
||||
3. API CRUD sur `Position`, `Shift`, `Volunteer`, `Assignment`.
|
||||
4. Moteur de validation (section 3).
|
||||
5. Endpoint matrice d'une édition :
|
||||
`[{ position, shifts: [{ shift, min_staff, ideal_staff, confirmed, proposed }] }]`.
|
||||
6. Jeu de données de démonstration : 120 bénévoles, 15 postes, 4 jours,
|
||||
quelques trous d'effectif, 2–3 avertissements actifs.
|
||||
|
||||
### Hors périmètre (le modèle doit juste pouvoir les accueillir sans refonte)
|
||||
|
||||
Solveur OR-Tools · envoi d'emails / SMS · constructeur de formulaire ·
|
||||
pointage sur site · fonctions d'IA.
|
||||
|
||||
---
|
||||
|
||||
## 5. Arborescence
|
||||
|
||||
```
|
||||
.
|
||||
├── CLAUDE.md
|
||||
├── README.md
|
||||
├── docker-compose.yml # dev : db + backend (hot reload)
|
||||
├── .env.example
|
||||
├── .github/workflows/deploy.yml # CI/CD : build → GHCR → SSH sur le VPS
|
||||
├── deploy/
|
||||
│ ├── docker-compose.prod.yml
|
||||
│ └── README.md # secrets requis, setup VPS
|
||||
├── docker/postgres-init/ # création de la base de test au 1er boot
|
||||
├── backend/
|
||||
│ ├── pyproject.toml # géré par uv
|
||||
│ ├── Dockerfile
|
||||
│ ├── alembic.ini
|
||||
│ ├── app/
|
||||
│ │ ├── main.py # instance FastAPI
|
||||
│ │ ├── config.py # settings (pydantic-settings)
|
||||
│ │ ├── db.py # engine + session
|
||||
│ │ ├── logging_config.py # RGPD : pas de PII dans les logs
|
||||
│ │ ├── api/ # routers
|
||||
│ │ ├── models/ # tables SQLModel
|
||||
│ │ ├── validation/ # moteur de règles (cœur du produit)
|
||||
│ │ ├── i18n/
|
||||
│ │ │ ├── __init__.py # t(key, **params)
|
||||
│ │ │ └── locales/fr.json
|
||||
│ │ └── migrations/ # Alembic (env.py, versions/)
|
||||
│ └── tests/
|
||||
│ ├── conftest.py # vraie base de test PostgreSQL
|
||||
│ └── ...
|
||||
└── frontend/ # React + Vite + TS + Tailwind (étape ultérieure)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Commandes
|
||||
|
||||
Aucune dépendance locale requise hors Docker.
|
||||
|
||||
| Action | Commande |
|
||||
|-------------------------------|-------------------------------------------------------------------------|
|
||||
| Lancer en dev | `docker compose up --build` |
|
||||
| API + docs interactives | http://localhost:8000/docs |
|
||||
| Tests backend | `docker compose run --rm backend uv run pytest` |
|
||||
| Couverture | `docker compose run --rm backend uv run pytest --cov=app` |
|
||||
| Lint | `docker compose run --rm backend uv run ruff check .` |
|
||||
| Format | `docker compose run --rm backend uv run ruff format .` |
|
||||
| Créer une migration | `docker compose run --rm backend uv run alembic revision --autogenerate -m "..."` |
|
||||
| Appliquer les migrations | `docker compose run --rm backend uv run alembic upgrade head` |
|
||||
| Charger le jeu de démo | *(étape ultérieure)* `docker compose run --rm backend uv run python -m app.seed` |
|
||||
|
||||
---
|
||||
|
||||
## 7. Conventions
|
||||
|
||||
- **Code, base, noms de fichiers en anglais.** Le français vit dans les
|
||||
fichiers de traduction et les commits.
|
||||
- **Aucun texte visible par l'utilisateur en dur.** Backend : `app.i18n.t(key)`
|
||||
+ `locales/fr.json`. Frontend : fichier i18n dédié. Vrai même en monolingue.
|
||||
- **RGPD :**
|
||||
- Pas de log des noms, emails, téléphones. On logue des identifiants.
|
||||
- `Edition.retention_days` : durée de rétention paramétrable.
|
||||
- Endpoint de suppression complète d'un bénévole : *hard delete* en cascade.
|
||||
- **Dates et heures :** stockées naïves, interprétées en heure locale
|
||||
Europe/Paris (heure murale). Les règles « 8 h », « journée », « demi-journée »
|
||||
sont toutes en heure locale.
|
||||
- **Validation TDD :** tests avant implémentation, un cas passant + un cas
|
||||
déclenchant par règle.
|
||||
- **Git :** un commit atomique par étape validée, message en français.
|
||||
- **Étapes courtes :** après chaque étape, montrer le résultat et attendre la
|
||||
validation avant de continuer.
|
||||
|
||||
---
|
||||
|
||||
## 8. Déploiement
|
||||
|
||||
- Dépôt GitHub. VPS avec Docker.
|
||||
- `.github/workflows/deploy.yml` : sur chaque push `main` → build de l'image
|
||||
backend → push sur `ghcr.io/<owner>/<repo>-backend` → SSH sur le VPS →
|
||||
`docker compose -f docker-compose.prod.yml pull && up -d`.
|
||||
- Secrets requis (voir `deploy/README.md`) : `SSH_HOST`, `SSH_USER`,
|
||||
`SSH_KEY`, `DEPLOY_PATH`, `GHCR_TOKEN`.
|
||||
- Le VPS porte un fichier `.env` (non versionné) avec `POSTGRES_USER`,
|
||||
`POSTGRES_PASSWORD`, `POSTGRES_DB`, `JWT_SECRET`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Journal des étapes
|
||||
|
||||
| Étape | Description | État |
|
||||
|-------|-------------------------------------------------------|----------|
|
||||
| 1 | Squelette : monorepo, Docker, CI/CD, backend qui démarre | à valider |
|
||||
Reference in New Issue
Block a user