Files
benevoles/CLAUDE.md
T
AdamThomasandClaude Sonnet 5 f58ec00e30 Créneaux types : paramétrage + génération auto des créneaux
Le paramétrage des créneaux se fait dans « Règles & édition » : la coordination
définit des créneaux types (DayPart). À la création d'un poste, un créneau à
besoin 0/0 est généré pour chaque jour de l'édition et chaque créneau type ;
les coordinateurs ajustent ensuite min/idéal.

Back-end :
- DayPart CRUD (GET/POST/PATCH/DELETE /editions/{id}/day-parts). Code figé
  après création (slugifié depuis le libellé si absent), unique par édition.
- Shift.day_part_id (nullable, SET NULL) + migration 0004.
- create_position génère les créneaux ; POST .../positions/{pid}/generate-shifts
  recrée les manquants (idempotent, ne touche pas aux besoins déjà réglés).
- 13 tests (day-parts, génération, extension d'édition, ajout de type).

Front-end :
- api/hooks/mock : dayParts CRUD + positions.generateShifts.
- ConfigRules : panneau « Créneaux types » (libellé + horaires éditables,
  ajout, suppression).
- ConfigGrid : bouton « Générer les créneaux » sur l'en-tête du poste.
- GrillePage : case « à définir » (gris, glyphe ·) quand min=idéal=0, avec
  entrée de légende.

155 tests back OK ; typecheck + lint + build + smoke front OK.

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

19 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
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, tshirt_size, diet, comment. Pas de date de naissance.
  • Skill : référentiel par édition (code, label). N-N Volunteer (volunteer_skill). N-N Shift (shift_required_skill).
  • 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, account. La suppression complète d'un bénévole (RGPD) efface donc ses Availability, Assignment, liens de compétence 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
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, 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)
│   │   ├── 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)
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

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/).