Files
benevoles/CLAUDE.md
T
AdamThomasandClaude Sonnet 5 24caece7df Bénévole : vœux d'artistes (entité Artist) et vœux de postes
Nouvelle entité `Artist` (nom + jour de passage), CRUD
`/editions/{id}/artists` (lecture ouverte à tout compte de l'édition,
écriture COORDINATION), unicité (édition, nom, date).

La fiche bénévole porte deux listes de vœux, sur le modèle de
`skill_codes` (None = inchangé, [] = vider) :
- `artist_ids` : artistes que le bénévole veut voir (`volunteer_artist`) ;
- `wished_position_ids` : postes qu'il voudrait tenir
  (`volunteer_position_wish`), distinct de l'affectation.

Un id étranger à l'édition renvoie 422 (`artist.unknown` /
`position.unknown`). Suppression d'un artiste : cascade sur les vœux.
Migration 0006. Jeu de démonstration et frontend inchangés (le front
suivra).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 22:12:55 +02:00

21 KiB
Raw Blame History

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
Artiste programmé Artist (name, performance_date)
Vœu « je veux voir cet artiste » VolunteerArtist (N-N Volunteer ↔ Artist)
Vœu « je voudrais ce poste » VolunteerPositionWish (N-N Volunteer ↔ Position)
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
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

Défini dans backend/app/models/ (enums.py, base.py, domain.py). Clés primaires UUID. Champs d'audit created_at / updated_at sur toutes les tables sauf les tables d'association.

  • Edition : year, name (opt.), start_date, end_date, retention_days (durée de rétention des données nominatives en jours, NULL = pas de purge).
  • DayPart (« créneau type ») : découpage d'une journée propre à l'édition (nombre et bornes libres). code, label, start_time, end_time (end_time < start_time ⇒ passe minuit), order. Unique (edition_id, code) ; code figé après création (clé de l'import des disponibilités). CRUD GET/POST/PATCH/DELETE /editions/{id}/day-parts (COORDINATION pour l'écriture).
  • Volunteer : email (unique par édition, nullable pour les bénévoles créés à la main — plusieurs NULL autorisés), first_name, last_name, phone, comment. Pas de date de naissance, ni de taille de t-shirt / régime alimentaire. Porte trois listes de vœux, exposées par la fiche (skill_codes, artist_ids, wished_position_ids ; None/absent = inchangé, [] = vider).
  • Skill : référentiel par édition (code, label). N-N Volunteer (volunteer_skill). N-N Shift (shift_required_skill).
  • Artist : programmation de l'édition (name, performance_date). Unique (edition_id, name, performance_date). La coordination gère la liste ; elle est présentée par jour. Les bénévoles y choisissent les artistes qu'ils veulent voir (volunteer_artist). CRUD GET/POST/PATCH/DELETE /editions/{id}/artists (lecture : tout compte de l'édition ; écriture : COORDINATION).
  • Vœux de postes : volunteer_position_wish (« postes que le bénévole voudrait tenir »), distinct de Assignment (décision) et de Position. lead_account_id (référent).
  • Position : name, location, category (PositionCategory), lead_account_id (→ Account, SET NULL à la suppression du compte).
  • Shift : position_id, day_part_id (nullable ; le créneau type dont il est issu, SET NULL si le type est supprimé), starts_at, ends_at (peut finir le lendemain), min_staff, ideal_staff, compétences requises (N-N Skill). À la création d'un Position, un Shift à 0/0 est généré par jour de l'édition et par DayPart ; POST /editions/{id}/positions/{pid}/generate-shifts recrée les manquants (après ajout d'un créneau type ou extension des dates).
  • Availability : volunteer_id, date, day_part_id, status (AvailabilityStatus). Unique (volunteer_id, date, day_part_id).
  • Assignment : volunteer_id, shift_id, source (AssignmentSource, défaut MANUAL), locked (défaut false). Unique (volunteer_id, shift_id). Pas de statut : l'affectation est valide dès sa création (aucune étape de confirmation) ; la retirer, c'est supprimer la ligne.
  • Account : email (unique par édition), role (Role), edition_id, volunteer_id (nullable, unique).
  • MagicLinkToken : account_id, token_hash (unique), expires_at, used_at.

Conventions techniques :

  • Multi-édition : toute entité métier porte un edition_id.
  • Énumérations stockées en VARCHAR + CHECK (native_enum=False), pas de type ENUM natif PostgreSQL.
  • Suppression en cascade au niveau base (ON DELETE CASCADE) sur tous les edition_id et sur les liens vers volunteer, shift, position, skill, artist, account. La suppression complète d'un bénévole (RGPD) efface donc ses Availability, Assignment, liens de compétence, vœux (artistes, postes) et son Account.
  • Dates/heures naïves, heure locale Europe/Paris (TZ du conteneur + tzdata). app.models.local_now() pour les horodatages.

Rôles et accès (app/auth/)

  • COORDINATOR : accès complet, construit et publie les plannings.
  • LEAD (référent) : lecture sur toute l'édition, écriture limitée aux postes dont il est lead_account_id (et à leurs créneaux et affectations).
  • VOLUNTEER : lecture de sa seule fiche et de ses seules affectations ; aucune écriture.

Matrice appliquée par les endpoints :

Ressource COORDINATOR LEAD VOLUNTEER
volunteers liste/écriture ✔ liste seule —
volunteers/{id} lecture ✔ ✔ soi-même
positions/shifts lecture ✔ ✔ —
positions/shifts écriture ✔ ses postes —
assignments écriture ✔ ses postes —
assignments lecture ✔ ✔ les siennes
artists lecture / écriture ✔ / ✔ ✔ / — ✔ / —
matrix, import, accounts ✔ / ✔ / ✔ ✔ / — / — —

Authentification — deux systèmes selon le compte, une même session JWT HS256 (Authorization: Bearer <jwt>, TTL session_ttl_hours) à l'arrivée. Le compte porte password_hash : défini ⇒ mot de passe, NULL ⇒ lien magique (Account.auth_method = "password" / "magic_link").

Mot de passe — comptes COORDINATOR :

  1. POST /editions/{id}/auth/login {email, password} → JWT + AccountRead. Échec = 401 auth.invalid_credentials (aucune divulgation).
  2. POST /editions/{id}/auth/change-password {current_password, new_password} (compte courant). Hachage bcrypt, longueur mini MIN_LENGTH (10).

Lien magique — comptes LEAD / VOLUNTEER (sans mot de passe) :

  1. POST /editions/{id}/auth/magic-link {email} → 202 constant. Si un compte sans mot de passe correspond, un MagicLinkToken est créé (jeton aléatoire, haché SHA-256 en base, TTL magic_link_ttl_minutes).
  2. Envoi d'email stubé : le lien est écrit dans les logs (app.auth, INFO, jeton=… — identifiants seulement, aucune donnée nominative).
  3. POST /editions/{id}/auth/verify {token} → JWT + AccountRead. Jeton à usage unique.

GET /auth/me → compte courant.

La coordination crée les comptes via POST /editions/{id}/accounts : un COORDINATOR exige un password (≥ 10) ; un LEAD / VOLUNTEER le refuse (lien magique). Un bénévole sans email n'a pas de Account.

Emails (app/email/)

Envoi par SMTP (auto-hébergé). EMAIL_ENABLED=false (défaut dev/tests) ⇒ les emails sont seulement journalisés (app.email, INFO, sans destinataire ni corps — RGPD). Config : SMTP_HOST/PORT/USER/PASSWORD/STARTTLS/SSL/FROM, PUBLIC_BASE_URL (pour les liens). Ne jamais mettre de vraies valeurs dans config.py : elles vivent dans .env (non versionné).

  • Remise « best effort » : une erreur SMTP (connexion, auth, rejet) est journalisée avec le message du serveur mais ne fait pas échouer l'appel HTTP.

  • Le port 465 implique le SSL (SMTPS) : open_smtp bascule automatiquement, même si SMTP_SSL=false / SMTP_STARTTLS=true. Port 587 = STARTTLS.

  • Beaucoup de fournisseurs (OVH…) exigent une From = boîte authentifiée ou alias validé.

  • Diagnostic : docker compose run --rm backend uv run python -m app.email.check <destinataire> — affiche la config effective et la cause exacte d'un échec.

  • Lien magique : POST .../auth/magic-link envoie l'email contenant PUBLIC_BASE_URL + magic_link_path + ?token=. Sans SMTP, le jeton (non nominatif) reste journalisé pour le dev.

  • Récapitulatif de planning : POST /editions/{id}/notifications/planning (COORDINATION), corps { subject?, body?, volunteer_ids? }. subject/body sont des modèles avec {prenom}, {nom}, {nb_creneaux}, {nb_heures}, {creneaux}, {lien_connexion} (si présent ⇒ un lien magique personnel est émis pour le compte du bénévole). volunteer_ids absent ⇒ tous les bénévoles de l'édition qui ont un email. Envoi en tâche de fond (BackgroundTasks, une connexion SMTP réutilisée). Réponse : { queued, skipped_no_email, recipients }.


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 :

  • Toutes les affectations du bénévole comptent pour chevauchement / cumul / repos (une affectation = une décision, toujours active).
  • « 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, assigned }] }].
  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 · SMS · constructeur de formulaire · pointage sur site · fonctions d'IA. (L'envoi d'emails par SMTP est en place — voir § 2.)


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 + schemas Pydantic + deps
│   │   │                           #   health, auth, accounts, editions,
│   │   │                           #   day_parts, imports, artists,
│   │   │                           #   volunteers, positions, shifts,
│   │   │                           #   assignments, matrix, notifications
│   │   ├── auth/                   # mot de passe + lien magique + JWT + rôles
│   │   ├── email/                  # SMTP (sender) + rendu des emails (templates)
│   │   ├── importing/              # import CSV/Excel (parser, values, service)
│   │   ├── models/                 # tables SQLModel
│   │   ├── validation/             # moteur pur (models, engine) + loader (base)
│   │   ├── bootstrap.py            # instance vierge : édition + 1er compte
│   │   ├── seed.py                 # jeu de démonstration (python -m app.seed)
│   │   ├── 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
Vérifier migration ↔ modèle docker compose run --rm backend uv run alembic check (après upgrade head)
Instance vierge (édition + 1er compte) docker compose run --rm backend uv run python -m app.bootstrap --name … --start … --end … --email … --password …
Charger le jeu de démo docker compose run --rm backend uv run python -m app.seed (après alembic upgrade head)

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, PUBLIC_BASE_URL et les variables EMAIL_ENABLED / SMTP_* (voir deploy/README.md).

9. Journal des étapes

Étape Description État
1 Squelette : monorepo, Docker, CI/CD, backend qui démarre validée
2 Modèle de données complet + migration Alembic initiale validée
3 Import CSV/Excel (analyse + exécution, rapport, doublons, simulation) validée
4 Moteur de validation des affectations (pur, TDD, 5 règles) validée
5 API CRUD (Position, Shift, Volunteer, Assignment) + matrice + validation branchée validée
6 Jeu de données de démonstration (app/seed.py) validée
7 Authentification : lien magique + JWT + application des rôles validée
8 Second système : connexion mot de passe (bcrypt) pour la coordination validée
9 Suppression du statut d'affectation (plus d'étape de confirmation) validée
10 Envoi d'emails par SMTP : lien magique + récapitulatif de planning validée
11 GET/PATCH /editions/{id} (nom, dates) — ajouter/retirer des jours ; UX config front validée
12 Créneaux types (DayPart CRUD) + génération auto des créneaux 0/0 à la création d'un poste ; case « à définir » / « sureffectif » sur la grille à valider
13 Bénévole : retrait taille de t-shirt / régime ; entité Artist (CRUD, par jour) + vœux des bénévoles (artistes, postes) à valider

Reste hors périmètre (modèle prêt à les accueillir) : solveur OR-Tools, SMS, constructeur de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans + connexion en place (voir frontend/).