Commit Graph
62 Commits
Author SHA1 Message Date
AdamThomasandClaude Sonnet 5 89e74e5ec7 Revue de sécurité avant ouverture au public
Audit de l'application avant sa première publication sur internet.
Correctifs immédiats, peu coûteux et sans changement de comportement
en dev :

- Le backend refuse de démarrer en `ENVIRONMENT=production` si
  `JWT_SECRET` est vide, trop court ou égal à la valeur par défaut
  versionnée dans `.env.example` — sinon n'importe qui pourrait forger
  un jeton de session valide (y compris pour un compte COORDINATION)
  sans jamais s'authentifier. Testé (`test_config.py`).
- Avertissement au démarrage si `EMAIL_ENABLED=false` en production :
  dans ce cas le jeton de lien magique est journalisé en clair (confort
  de dev, cf. `api/auth.py`) — un identifiant de connexion en clair dans
  les logs du conteneur en public serait un vrai problème.
- `frontend/nginx.conf` : plus de journal d'accès (la route
  `/connexion?token=…` y ferait apparaître le jeton de lien magique en
  clair), en-têtes `X-Content-Type-Options`, `X-Frame-Options`,
  `Referrer-Policy` (empêche aussi la fuite de ce même jeton vers
  Google Fonts via l'en-tête Referer), `server_tokens off`.
- `deploy/Caddyfile.benevoles` : mêmes en-têtes côté reverse proxy
  public, plus `Strict-Transport-Security`.
- `deploy/README.md` : check-list sécurité avant ouverture (secret,
  SMTP, ne jamais mettre en production la base de démo `app.seed` — le
  mot de passe du compte coordination y est versionné en clair —,
  limites connues comme l'absence de rate-limiting sur `/auth/*`).

Points vérifiés sans modification nécessaire : autorisation par rôle et
cloisonnement par édition cohérents sur tous les endpoints (`owned_or_404`
+ vérification d'édition partout), mots de passe bcryptés, jetons de
lien magique à usage unique haché en base (256 bits d'entropie), pas de
PII/jeton dans les logs applicatifs (RGPD déjà en place), pas
d'injection de gabarit dans les emails (substitution sur liste blanche,
pas de `.format()`), pas d'injection d'en-tête SMTP (`EmailMessage` du
socle Python), pas de `dangerouslySetInnerHTML` côté front, jeton JWT
en `Authorization: Bearer` (pas de cookie ⇒ pas de CSRF), `.env` jamais
versionné, dépendances à jour.

Restent des choix à trancher avec l'utilisateur plutôt qu'à décider
seul (compromis effort/risque) : rate-limiting sur les endpoints
d'authentification, désactivation de `/docs` en production, conteneurs
non-root.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-16 08:14:00 +02:00
AdamThomasandClaude Sonnet 5 ade59b3dc4 Écran référent : repli sur la vue bénévole, tous les créneaux du poste
Deux bugs remontés sur l'écran référent (/mobile-referent), tous deux
révélés par le repli documenté « décocher un poste ne rétrograde jamais
le compte » (§2) :

- Un compte LEAD qui ne référence plus aucun poste (ex. décoché) reste
  LEAD et atterrit donc sur /mobile-referent après son lien magique —
  qui affichait un écran référent vide au lieu de sa vue de bénévole
  classique. `MobileLeadPage` détecte maintenant ce cas et affiche,
  quand le compte est rattaché à une fiche bénévole, le même contenu
  que `/mobile-benevole` (planning : poste, jour, horaire, lieu,
  responsable, téléphone) — extrait dans un composant partagé
  `VolunteerHomeView`.

- Un poste avec plusieurs créneaux n'en montrait qu'un seul (le plus
  proche de l'heure réelle) : le référent ne pouvait pas voir son
  équipe sur les autres créneaux. La carte de chaque poste liste
  maintenant tous ses créneaux, chacun avec sa propre équipe, avec un
  repère « MAINTENANT » (composant partagé `NowLine`, déjà utilisé côté
  bénévole) pour se situer parmi eux.

Non inclus dans ce commit (édités en parallèle par l'utilisateur, laissés
en l'état) : backend/app/seed.py, frontend/tailwind.config.js.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-16 07:50:00 +02:00
AdamThomasandClaude Sonnet 5 9e0be07726 Écran référent : vraie page responsive, référent correctement calculé
Deux bugs sur l'écran référent (/mobile-referent) :

- Il était resté à l'étape maquette (`PhoneFrame`/`DesignNote`, cadre de
  téléphone à taille fixe) alors que la vue bénévole avait déjà été
  convertie en vraie page responsive. Repli sur `positions.data[0]` en
  l'absence de poste géré par le compte connecté : un référent qui
  n'est encore désigné sur aucun poste se voyait quand même affiché
  comme référent du premier poste de l'édition (« Accueil artistes »
  dans les données de démo) au lieu de voir qu'il n'en a aucun.

Réécriture de `MobileLeadPage.tsx` en page responsive (mobile d'abord,
`sm:` pour l'élargissement, comme `MobileVolunteerPage.tsx`) : plus de
maquette de téléphone, une carte par poste réellement géré
(`Position.lead_account_id === account.id`, sans repli), créneau en
cours ou à venir calculé sur l'heure réelle (nouveau hook partagé
`useNowMinutes`, extrait de la vue bénévole), équipe affectée avec
pointage présent/absent (toujours local à l'écran : le pointage sur
site reste hors périmètre, CLAUDE.md §5). État vide explicite si le
compte n'est référent d'aucun poste.

`PhoneFrame`/`DesignNote` n'avaient plus d'autre utilisateur : supprimés
avec leurs clés i18n devenues mortes.

Non inclus dans ce commit (édités en parallèle par l'utilisateur, laissés
en l'état) : backend/app/seed.py, frontend/tailwind.config.js.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 23:42:48 +02:00
AdamThomasandClaude Sonnet 5 a78aad58e2 Vue bénévole mobile : responsable, téléphone et repère « maintenant »
La vue bénévole reprenait les infos essentielles (poste, jour, horaire,
lieu) mais affichait un badge de validation factice (« TU ES ATTENDU
SUR CE CRÉNEAU ») sans plus-value, sans jamais donner le nom ni le
téléphone du référent du poste à contacter — pourtant déjà modélisés
(`Position.lead_account_id`) depuis la gestion des référents.

Un compte VOLUNTEER n'a pas accès à `positions`/`shifts` (403, cf. § 2
matrice des accès) : ces infos ne pouvaient donc pas être jointes côté
front à partir des listes existantes. Nouvel endpoint dédié et
minimal, réservé à la lecture du planning propre du bénévole :

  GET /editions/{id}/volunteers/{vid}/planning

Renvoie, par affectation : poste, lieu, et référent (nom + téléphone,
`null` si le poste n'a pas de référent ou si son compte n'est pas
rattaché à une fiche bénévole) — sans exposer la liste complète des
postes/créneaux de l'édition. Tests : accès (soi-même / autre
bénévole 403 / COORD+LEAD), présence/absence du référent, tri par
heure de début.

Front : retrait du badge de validation, ajout des lignes responsable/
téléphone par créneau, et un repère horizontal « MAINTENANT » qui se
positionne dans la liste triée des créneaux selon l'heure réelle (juste
avant le prochain créneau à venir) pour situer le bénévole dans son
planning.

Non inclus dans ce commit (édités en parallèle par l'utilisateur,
laissés en l'état) : backend/app/seed.py, frontend/tailwind.config.js.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 23:33:06 +02:00
AdamThomasandClaude Sonnet 5 8fcc22115f Gestion des référents depuis la fiche bénévole
Un poste avait bien un `lead_account_id`, lu par l'écran référent
(`/mobile-referent`) et par les droits d'écriture (`ensure_can_write_position`),
mais rien ne permettait de le renseigner : aucun écran ne l'exposait, et
l'API ne validait même pas que la valeur envoyée désignait un compte référent
existant de la bonne édition.

Backend :
- `PATCH /editions/{id}/accounts/{account_id}` (nouveau) : change le `role`
  d'un compte, seulement entre LEAD et VOLUNTEER (lien magique dans les deux
  cas) — 422 `account.role_change_not_allowed` dès que COORDINATOR est
  impliqué (mot de passe en jeu, jamais touché ici). C'est ce qui permet de
  faire passer le compte d'un bénévole en référent.
- `POST`/`PATCH /positions` : `lead_account_id` vérifié — doit désigner un
  compte `Role.LEAD` de la même édition, sinon 422
  `position.lead_must_be_referent_account` (silencieusement accepté
  jusqu'ici, y compris un compte d'une autre édition ou du mauvais rôle).

Frontend (`ConfigVolunteers.tsx`) : nouvelle section « Référent des postes »
sur la fiche du bénévole, juste après l'accès de connexion — mêmes badges à
cocher que les vœux de poste/artiste. Cocher un poste crée ou met à niveau
(LEAD) l'accès de connexion du bénévole automatiquement ; décocher retire
seulement le poste, sans jamais rétrograder l'accès. Un bénévole dont le
compte est déjà COORDINATOR ne peut pas être désigné référent (il a déjà
accès à tout). Miroir complet côté mock (`accounts.update`,
`checkLeadAccount`) pour que la démonstration se comporte pareil.

Vérifié : 204 tests backend pertinents (14 nouveaux : 6 sur `PATCH accounts`,
8 sur la validation `lead_account_id`), ruff, alembic check ; typecheck/
lint/build/smoke frontend ; parcours complet testé en direct sur le jeu de
démo (création de compte, promotion LEAD, rejet COORDINATOR, affectation
référent) avant nettoyage des données de test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 23:19:56 +02:00
AdamThomasandClaude Sonnet 5 db11d9a3ba Page bénévole : vraie page responsive, plus une maquette de téléphone
C'est l'écran d'accueil réel d'un compte bénévole (`homePath`) — consulté sur
son propre téléphone la plupart du temps, mais la page rendait un cadre de
téléphone factice à taille fixe (376×772 px, fausse barre de statut) pensé
pour être visionné sur un écran d'ordinateur, plus une note de choix de
conception destinée à l'équipe, pas au bénévole. Sur un vrai téléphone,
c'était un téléphone dessiné à l'intérieur du vrai téléphone.

- Retiré le cadre (`PhoneFrame`) et la note de conception (`DesignNote`) —
  toujours utilisés par la page référent (`MobileLeadPage.tsx`, non touchée
  ici), donc le composant partagé reste en place.
- Le contenu (bandeau d'accueil + cartes de créneaux) devient la page
  elle-même : pleine largeur sur mobile, centrée avec une largeur de lecture
  confortable (max 640 px) et un peu plus d'air à partir de `sm:` pour un
  écran d'ordinateur. Mêmes repères de conception qu'avant (fonds opaques,
  cibles tactiles ≥ 52 px), déplacés en commentaire de code.
- `AppShell` : la barre d'onglets ne s'affiche plus quand il n'y a qu'un seul
  écran accessible (bénévole, référent) — elle n'a de sens que pour la
  coordination (3 écrans) ; simplifie l'en-tête sur les rôles qui n'ont nulle
  part où naviguer.

Vérifié : typecheck, lint, build, smoke frontend OK.

À noter : `MobileLeadPage.tsx` (référent) a exactement le même défaut
(maquette de téléphone + note de conception) — pas touchée ici, la demande
portait sur la vue bénévole.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 23:03:59 +02:00
AdamThomasandClaude Sonnet 5 9871785d89 Grille : aperçu des bénévoles affectés au survol d'une case
Chaque case ne montrait qu'un nombre (affecté/idéal) ; il fallait cliquer et
ouvrir le panneau pour savoir qui exactement. Une bulle au survol affiche
maintenant les noms des bénévoles déjà affectés à ce créneau, sans clic.

- Construit une table (créneau → noms) à partir des affectations et des
  bénévoles de l'édition (mêmes requêtes déjà utilisées par le panneau
  d'affectation, donc pas de coût réseau supplémentaire une fois en cache).
- Remplace l'ancien `title` natif (lent, sans style) par la même bulle que
  `HelpTip` (bordures franches, apparaît sous la case) ; garde le rappel
  affecté/idéal/minimum en tête de bulle, puis la liste triée des noms, ou
  le message « personne d'affecté » sinon.

Vérifié : typecheck, lint, build, smoke frontend OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 22:54:31 +02:00
AdamThomasandClaude Sonnet 5 efa652fffb Panneau d'affectation : barre de recherche pour retrouver un bénévole
La liste des candidats se limitait aux 10 mieux classés par le score de
chance — un bénévole précis pouvait ne jamais y apparaître. Un champ de
recherche (nom/prénom, insensible aux accents/casse) permet maintenant de le
retrouver et de l'affecter directement, sans être limité au classement.

- Recherche vide : comportement inchangé (top 10 par score de chance).
- Recherche non vide : jusqu'à 20 bénévoles non affectés dont le nom
  correspond, triés par le même score ; message dédié si aucun ne correspond.
- `foldAccents` (accents/casse) déplacé de `ConfigVolunteers.tsx` vers
  `lib/planning.ts` pour être partagé avec le panneau d'affectation.
- `GrillePage.tsx` : `key={shift.shift_id}` sur `AssignmentPanel` pour que la
  recherche (et tout futur état local) reparte à zéro en changeant de
  créneau, plutôt que de persister sur le suivant.

Vérifié : typecheck, lint, build, smoke frontend OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 22:43:10 +02:00
AdamThomasandClaude Sonnet 5 11bc8cbe36 Vœu de compagnon : traité comme réciproque par le solveur
Le vœu reste à sens unique en base (A choisit B sur sa fiche, B n'a rien à
faire) mais si A souhaite B, B doit désormais profiter aussi de la présence
de A — dans les deux sens : affecter A d'abord fait remonter B comme
candidat, et affecter B d'abord fait remonter A, même si B n'a rien demandé.

- Solveur (`loader.py`) : le vœu (A → B) alimente maintenant l'ensemble
  effectif des deux bénévoles (A et B), pas seulement celui du demandeur.
  Aucun changement dans `engine.py` : il ne fait que lire un ensemble déjà
  symétrisé.
- Mock (`buildEffectiveCompanions`) et classement par défaut
  (`buildEffectiveCompanionIds` dans `lib/planning.ts`, utilisé par
  `AssignmentPanel.tsx`) : même symétrisation, pour rester cohérents avec le
  vrai solveur.
- La fiche bénévole (vœux affichés/édités) n'est pas touchée : c'est le seul
  endroit qui reste à sens unique, par construction (la réciprocité n'est
  qu'une lecture, jamais une donnée enregistrée — sinon éditer sa propre
  liste risquerait d'effacer le vœu de quelqu'un d'autre).
- Nouveau test d'intégration : le bénévole qui n'a rien demandé profite quand
  même de la présence de celui qui l'a choisi.
- CLAUDE.md à jour.

Vérifié : 193 tests backend pertinents (dont le nouveau), ruff, alembic
check ; typecheck/lint/build/smoke frontend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 22:32:02 +02:00
AdamThomasandClaude Sonnet 5 166e2c3366 Fiche bénévole : recherche pour les compagnons, badges pour artistes/postes
- Vœu de compagnon : le sélecteur (déjà trié par ordre alphabétique) gagne un
  champ de recherche (insensible aux accents/casse) qui filtre la liste
  déroulante — plus pratique avec 100+ bénévoles.
- Vœux d'artiste et de poste : remplacé le menu déroulant par des badges à
  cocher (`BadgeWishPicker`, réutilise le composant `Chip`) — toutes les
  options sont visibles d'un coup, un clic les bascule, plus besoin d'ouvrir
  une liste pour un petit nombre d'options.

Vérifié : typecheck, lint, build, smoke frontend OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 22:05:52 +02:00
AdamThomasandClaude Sonnet 5 6a4bcf8782 Vœu de compagnon : être affecté avec 0 à N autres bénévoles
Nouveau vœu sur la fiche du bénévole — « je voudrais être affecté avec tel(s)
bénévole(s) » — pris en compte à la fois pour le rendre visible et par le
solveur.

Backend :
- `VolunteerCompanionWish` (`volunteer_id`, `companion_id`, à sens unique,
  `CHECK (volunteer_id <> companion_id)`) — migration 0010.
- API : `wished_companion_ids` sur `VolunteerCreate`/`Update`/`Read`, mêmes
  règles que les autres vœux (ids inconnus → 422 `volunteer.unknown` ; se
  souhaiter soi-même → 422 `volunteer.cannot_wish_self`).
- Solveur (`engine.py`) : bonus (+25 par correspondance) si un des compagnons
  souhaités du candidat est déjà affecté (décision déjà prise) au créneau —
  cumulable si plusieurs compagnons sont présents. Comme les autres termes de
  l'objectif, un bonus/malus, jamais une contrainte.
- Seed de démo : ~20 % des bénévoles ont un vœu de compagnon (couple, amis).

Frontend :
- Fiche bénévole (`ConfigVolunteers.tsx`) : sélecteur de compagnons (ajout et
  édition), avec une bulle d'aide expliquant le sens unique et l'effet sur le
  solveur — c'est ce qui « fait apparaître » le lien entre bénévoles.
- Panneau d'affectation : badge « avec N compagnon(s) souhaité(s) » sur un
  candidat, et bonus correspondant dans le classement par défaut
  (`candidateChanceScore`).
- Mock (`runSolver`) : même bonus, pour que la démonstration se comporte
  pareil que le vrai solveur.

CLAUDE.md à jour (modèle de domaine, section 4, journal).

Vérifié : 194 tests backend (dont 13 nouveaux), ruff, alembic check ;
typecheck/lint/build/smoke frontend ; solveur testé en direct sur le jeu de
démo reseedé (0,25 s, 47 suggestions).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 21:50:30 +02:00
AdamThomasandClaude Sonnet 5 b97b95c606 Solveur : le malus de chaînage n'est pas limité au même poste
Correction : le malus ajouté précédemment ne se déclenchait que si le
bénévole avait tenu le créneau précédent du *même* poste. L'intention était
plus générale — limiter les enchaînements de shifts d'un bénévole, quel que
soit le poste, pas seulement faire tourner un poste en particulier.

- `SolverShift.predecessor_shift_id: str | None` → `predecessor_shift_ids:
  frozenset[str]` : l'ensemble des créneaux (n'importe quel poste) du même
  jour dont la fin coïncide avec le début de celui-ci. Peut contenir
  plusieurs ids (plusieurs postes se terminant à la même heure).
- Loader : le calcul du prédécesseur se fait maintenant sur tous les
  créneaux de l'édition regroupés par jour (comparaison des heures de fin),
  plus par poste.
- Miroir côté mock (`buildPredecessorMap`, `runSolver`).
- Nouveau test d'intégration (`test_api_solver.py`) : deux postes différents,
  confirme que le chaînage s'applique bien d'un poste à l'autre.
- CLAUDE.md à jour.

Vérifié : 185 tests backend (dont le nouveau), ruff, alembic check ;
typecheck/lint/build/smoke frontend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 21:20:41 +02:00
AdamThomasandClaude Sonnet 5 64e2f86647 Solveur : retire le plafond de cumul journalier, ajoute un malus de chaînage
- Retiré : le solveur ne pénalise plus un bénévole qui dépasserait 8 h sur
  la journée (`_daily_overage_minutes`/`_DAILY_OVERAGE_PENALTY_PER_MIN`,
  côté back et mock). Seule la validation le signale encore, à la
  confirmation (`DAILY_HOURS_EXCEEDED`, inchangée). `SolverInput.day_parts`
  et son chargement sont retirés au passage : ils ne servaient qu'à ça.
- Ajouté : malus (-40) si le bénévole a déjà (affectation décidée) le
  créneau précédent de ce même poste, le même jour
  (`SolverShift.predecessor_shift_id`, calculé par le loader à partir de
  tous les créneaux du poste — pas seulement ceux à compléter). Favorise la
  rotation plutôt que d'enchaîner toujours la même personne sur un poste.
  Reste un malus, pas une exclusion : le seul candidat pour un créneau
  urgent est quand même proposé.
- Miroir côté mock (`runSolver`) pour que la démonstration se comporte pareil.
- CLAUDE.md à jour (§4).

Tests : remplacé le test du plafond journalier par un test de non-régression
explicite (le cumul n'est plus pénalisé) + un test isolant le repos (qui,
lui, reste vérifié) ; ajouté 3 tests pour le chaînage (préfère un bénévole
frais, reste proposé si c'est le seul candidat urgent, un créneau existant
non-prédécesseur ne déclenche pas le malus).

Vérifié : 183 tests backend, ruff, alembic check ; typecheck/lint/build/smoke
frontend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 21:12:30 +02:00
AdamThomasandClaude Sonnet 5 e405590bbc Front : donner un accès de connexion à un bénévole (comptes manquants)
Root cause du « je n'ajoute un bénévole avec son mail, je ne reçois aucun
mail » : renseigner un email sur la fiche d'un bénévole ne crée jamais de
compte (`Account`). `POST .../auth/magic-link` cherche un compte par email
et ne fait rien s'il n'en trouve pas (202 quoi qu'il arrive, par conception,
pour ne jamais révéler si un compte existe) — silencieusement, sans erreur.
L'API `POST/GET/DELETE /editions/{id}/accounts` existait déjà côté backend
(testée) mais n'avait jamais été branchée au frontend.

- `api/schema.ts` / `types.ts` / `http-client.ts` / `hooks.ts` : nouvelle
  surface `accounts` (list/create/remove) + `useAccounts`, `useCreateAccount`,
  `useDeleteAccount`.
- `mock-client.ts` : même surface (règles miroir du backend : email unique,
  mot de passe interdit hors COORDINATOR) ; la suppression d'un bénévole
  supprime aussi son compte (cascade RGPD, alignée sur le backend).
- `ConfigVolunteers.tsx` : section « Accès de connexion » sur la fiche du
  bénévole — bouton pour donner l'accès (crée un compte VOLUNTEER, lien
  magique) si un email est renseigné, ou le révoquer si un accès existe déjà.
- Corrigé au passage : `config.vol.addHint` affirmait qu'un email donnait
  l'accès automatiquement — c'était faux, texte mis à jour.
- CLAUDE.md à jour (section authentification).

Vérifié : typecheck, lint, build, smoke (7 routes) OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 20:22:56 +02:00
AdamThomasandClaude Sonnet 5 fb6e26eddb Front : les indications d'usage passent en bulle d'aide (icône « ? »)
Les explications sur comment utiliser un écran (grille des besoins, vue
bénévoles, artistes, import, disponibilités, créneaux types, seuils,
notification par email…) étaient affichées en permanence dans la page,
prenant de la place pour un contenu qu'on ne lit qu'une fois. Elles passent
dans une bulle affichée au survol (ou au focus clavier/tactile) d'un petit
repère « ? » posé à côté du titre ou du champ concerné.

- `HelpTip` (frontend/src/components/ui.tsx) : nouveau composant partagé,
  cohérent avec le design (bordures franches, pas d'arrondi) ; `side`
  ('top'/'bottom') pour éviter que la bulle ne sorte du cadre visible selon
  que le repère est en haut ou en bas de son conteneur.
- Appliqué à GrillePage, VolunteerViewPage, ConfigArtists, ConfigVolunteers
  (import, ajout manuel, disponibilités), ConfigGrid, ConfigRules, ConfigMail.
- Aucun texte déplacé n'a changé : mêmes clés i18n, seul l'endroit où elles
  s'affichent change.

Laissé en l'état (pas des indications d'usage mais des messages d'état) :
bannières identifiants de démo, liens de connexion en dev, états vides, et
la description des rôles (contenu propre des cartes, pas un mode d'emploi).

Vérifié : typecheck, lint, build, smoke (7 routes) OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 20:03:41 +02:00
AdamThomasandClaude Sonnet 5 897a76ca6d Bénévole : l'email n'est plus unique (couple, famille)
Plusieurs bénévoles peuvent légitimement partager une même adresse email —
l'architecture l'interdisait : l'import ignorait silencieusement le second
(« duplicate_email_in_file »), et l'ajout manuel renvoyait un 409. Les deux
sont corrigés.

- Migration 0009 : retrait de la contrainte d'unicité (edition_id, email) sur
  `volunteer` (l'index sur `email` seul est conservé).
- Import (`app/importing/service.py`) : l'upsert se fait désormais sur
  (email, nom + prénom), pas sur l'email seul. Un même email revu avec un nom
  différent dans le fichier crée un second bénévole (nouveau code
  `import.row.shared_email`, WARNING) au lieu d'être ignoré ;
  `duplicate_email_in_file` ne se déclenche plus que sur une ligne
  littéralement répétée (même email ET même nom).
- API (`POST`/`PATCH /volunteers`) : retrait du rejet 409
  `volunteer.duplicate_email`, devenu sans objet (le mock ne l'a d'ailleurs
  jamais appliqué à la création manuelle).
- Mock frontend (`mock-client.ts`) : même correction de l'upsert par import,
  pour rester cohérent avec le backend.
- CLAUDE.md à jour (modèle de domaine, règles d'import, journal).

Contrepartie assumée (documentée) : un email re-importé avec un nom
légèrement différent (faute de frappe corrigée) crée désormais un bénévole
de plus signalé, plutôt que de renommer silencieusement l'existant — même
philosophie que la détection sans email, qui ne fusionne jamais rien
automatiquement.

Vérifié : 179 tests backend, ruff, alembic check ; typecheck/lint/build/smoke
frontend ; reseed de démonstration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 19:38:28 +02:00
AdamThomasandClaude Sonnet 5 fcc42c8d43 Email : ne plus journaliser l'adresse du destinataire en cas de refus SMTP
En cherchant pourquoi les logs d'envoi d'email semblaient invisibles, on a
plutôt trouvé un problème RGPD : `SMTPRecipientsRefused` embarque l'adresse
du destinataire dans ses `args` (`{adresse: (code, message)}`), et
`logger.error(..., exc)` la journalisait telle quelle via `str(exc)` — en
violation directe de la règle « pas de log des noms/emails/téléphones ».

`_smtp_error_summary()` n'en extrait que les codes/messages serveur.

Constaté au passage (pas un bug) : le mécanisme de logs lui-même fonctionne
bien — `docker compose logs -f backend` affiche bien les lignes `app.email`
dès qu'une vraie requête HTTP déclenche l'envoi (testé en direct sur
l'édition de démo).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 19:23:21 +02:00
AdamThomasandClaude Sonnet 5 d5f67b2701 Déploiement : CI/CD GitLab + image frontend de prod + Caddy
Remplace la chaîne GitHub Actions (jamais réellement branchée : aucun
remote git n'existait) par une chaîne GitLab CI/CD, et comble un manque
qu'elle avait déjà : le frontend n'était jusqu'ici jamais construit ni
déployé en production.

- `.gitlab-ci.yml` : tests (backend + frontend) à chaque push ; sur `main`,
  build des images backend et frontend (registre de conteneurs GitLab) puis
  déploiement SSH sur le VPS (pull, migration Alembic, up -d).
- `frontend/Dockerfile.prod` + `frontend/nginx.conf` : build Vite statique
  servi par nginx (repli SPA sur index.html). `VITE_API_MODE`/
  `VITE_EDITION_ID` sont des variables de build, passées en `--build-arg`
  depuis la variable CI/CD `VITE_EDITION_ID`.
- `deploy/docker-compose.prod.yml` : ajout du service `frontend` ; `backend`
  et `frontend` ne publient plus que sur `127.0.0.1` (Caddy, déjà installé
  sur le serveur, devient l'unique porte d'entrée publique).
- `deploy/Caddyfile.benevoles` : bloc de vhost à ajouter au Caddy existant
  (`/api/*` vers le backend avec préfixe retiré, le reste vers le frontend).
- `deploy/README.md` : runbook complet (remote GitLab, variables CI/CD,
  préparation du VPS, création de l'édition de production avant le premier
  build frontend, vhost Caddy).
- CLAUDE.md et README.md mis à jour en conséquence.

Vérifié : `docker compose -f deploy/docker-compose.prod.yml config`,
build réel de l'image frontend de prod, / et une route SPA (/grille)
répondent 200 via nginx.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-14 19:04:28 +02:00
AdamThomasandClaude Sonnet 5 367dfe42eb Grille : colonnes par créneau (pas par heure), largeur adaptative
- `buildLattice` (frontend/src/lib/planning.ts) identifie une colonne par
  (date, heure de début, heure de fin) et non plus par la seule heure de
  début : deux créneaux qui démarrent en même temps mais durent
  différemment ont désormais chacun leur colonne, avec un en-tête « début
  → fin » au lieu du seul début.
- Largeur adaptative par jour (`columnWidthPx`) : peu de créneaux dans une
  journée ⇒ colonnes larges, beaucoup de créneaux ⇒ largeur compacte
  (défilement horizontal) — fini les colonnes étroites par défaut.
- `VolunteerViewPage` mis à jour pour rester synchronisé avec la nouvelle
  clé de colonne (fonction `buildLattice` partagée).
- CLAUDE.md à jour.

Corrige au passage `bg-hatch-mauve` (case en sureffectif) : la classe était
utilisée depuis le commit précédent mais l'utilitaire Tailwind
correspondant n'avait jamais été ajouté à `backgroundImage` — la case
retombait silencieusement sur un fond uni.

typecheck + lint + build + smoke OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-11 20:28:41 +02:00
AdamThomasandClaude Sonnet 5 55f634e25b Grille : les candidats sont classés par score de chance, pas par charge
Dans le panneau d'affectation, la liste des candidats d'un créneau
n'était triée que par disponibilité puis par nombre d'heures déjà
assignées — les vœux n'y jouaient aucun rôle tant qu'on ne cliquait pas
sur « Suggestions (solveur) ».

Nouveau `candidateChanceScore` (`lib/planning.ts`), portage local et
synchrone du scoring du solveur : vœu de poste (bonus), disponibilité
déclarée y compris conflit avec un artiste souhaité (malus), charge déjà
assignée (malus). C'est ce score qui classe désormais la liste par
défaut, dès l'ouverture du détail d'un poste — sans requête réseau ni
clic sur le solveur. Si celui-ci a effectivement tourné, ses candidats
retenus (vraie optimisation jointe) restent prioritaires en tête.

Chaque candidat affiche maintenant, en plus de sa disponibilité, des
badges « Vœu de poste » et « Manquerait un artiste souhaité » quand
applicable — pour que le classement reste lisible.

typecheck / lint / build / smoke au vert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-11 18:38:47 +02:00
AdamThomasandClaude Sonnet 5 282be85498 Jeu de démo : programmation et vœux des bénévoles (manquaient)
Le seed créait déjà des disponibilités, mais aucun Artist ni vœu
(VolunteerPositionWish, VolunteerArtist) — la démo ne montrait donc rien
du solveur ni des vœux : postes/artistes jamais choisis, programmation
vide.

- 3 artistes par jour de festival (pas montage/démontage), horaires
  échelonnés en soirée (18h-19h / 19h30-21h / 21h30-23h).
- ~40 % des bénévoles ont un vœu de poste ; ~15 % de chance par artiste
  d'être un vœu (~1 par bénévole en moyenne sur 6 artistes).
- Summary affiche désormais artists / position_wishes / artist_wishes ;
  tests étendus (présence + déterminisme).

Vérifié en conditions réelles (`python -m app.seed` + appels API) :
6 artistes, 47 vœux de poste, 95 vœux d'artiste sur 120 bénévoles ; le
solveur tourne toujours en ~0,3 s dessus.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-11 18:25:18 +02:00
AdamThomasandClaude Sonnet 5 304b3c1d9e Front : heure de passage des artistes (config + mock du solveur)
- ArtistRead/Create/Update gagnent start_time/end_time.
- Onglet Programmation : chaque artiste a désormais deux champs heure
  (début/fin de passage), éditables comme le nom.
- Mock du solveur : porte le même malus de conflit d'artiste que le
  back-end (un créneau qui chevauche un passage souhaité est pénalisé),
  pour un comportement de démo cohérent.
- Jeu de démo : 2 artistes par jour avec des horaires distincts
  (18h-19h / 20h30-22h) au lieu d'un horaire par défaut implicite.

typecheck / lint / build / smoke au vert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-11 18:14:07 +02:00
AdamThomasandClaude Sonnet 5 fea9d37019 Artiste : heure de passage ; vœux d'artistes pris en compte par le solveur
Artist gagne start_time/end_time (end <= start ⇒ finit le lendemain,
même convention que Shift/DayPart/Availability). Migration 0008.

Le solveur (app/solver/) résout maintenant les fenêtres de passage des
artistes que chaque bénévole veut voir (VolunteerArtist) et pénalise
toute suggestion qui chevaucherait un passage souhaité — le bénévole
raterait l'artiste. Comme le reste du scoring, c'est un terme de
l'objectif, pas un blocage : pour un créneau urgent sans autre
candidat, le solveur propose quand même, quitte à ce que ce soit un
pis-aller.

- SolverVolunteer.wished_artist_windows (fenêtres ShiftInfo précalculées
  par le loader depuis Artist + VolunteerArtist).
- Nouveau malus _ARTIST_CONFLICT_PENALTY dans engine.py.
- Tests : moteur (conflit détecté/ignoré hors chevauchement/toujours
  suggéré en dernier recours) + intégration API bout en bout.

176 tests, ruff et alembic check au vert. CLAUDE.md mis à jour (§2 et §4).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-11 18:13:54 +02:00
AdamThomasandClaude Sonnet 5 e47c29d35b Front : suggestions du solveur en tête de liste dans la grille
- Surface API `solver.run(editionId)` (HTTP + mock) et hooks
  `useRunSolver` (mutation, alimente le cache) / `useSolverSuggestions`
  (lecture seule du cache, ne déclenche jamais de calcul).
- Grille des besoins : bouton « Suggestions (solveur) », affiche le
  nombre de suggestions calculées.
- Panneau d'affectation d'un créneau : les bénévoles suggérés par le
  solveur passent en tête de la liste des candidats (avant le tri par
  disponibilité / charge), avec un badge « Suggéré ». Ne pré-affecte
  jamais personne — la coordination choisit et confirme comme avant.
- Mock : portage glouton (par créneau, du plus urgent au moins urgent)
  du scoring du moteur back-end, pour une démonstration cohérente sans
  CP-SAT côté navigateur.

typecheck / lint / build / smoke au vert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-11 08:16:29 +02:00
AdamThomasandClaude Sonnet 5 803db5e9a9 Solveur de suggestions (OR-Tools CP-SAT) — ne place jamais personne
Nouveau module `backend/app/solver/` (mêmes conventions que
`validation/` : models purs, engine pur testé, loader seul lien avec la
base). Pour chaque créneau encore sous son effectif idéal, calcule une
liste ordonnée de bénévoles candidats — c'est la coordination qui
affecte, dans l'écran habituel ; le solveur fait juste remonter ces
candidats en tête de liste (voir le câblage frontend à suivre).

- Seule contrainte dure : aucun chevauchement de créneaux pour un même
  bénévole (règle SHIFT_OVERLAP), à la fois contre l'existant et entre
  deux suggestions du même calcul. L'effectif idéal est un plafond dur.
- Tout le reste est un terme de l'objectif, pas un blocage : priorité
  forte aux créneaux sous l'effectif minimal, bonus vœu de poste, malus
  disponibilité manquante (réutilise `validation.engine.uncovered_minutes`),
  cumul de 8 h et repos (contre les affectations déjà décidées),
  répartition de la charge.
- `POST /editions/{id}/solver/suggestions` (COORDINATION/référent) :
  recalcule à la demande, ne persiste jamais d'affectation.

Dépendance `ortools`. 18 nouveaux tests (moteur + API), 172 tests au
vert, `ruff` et `alembic check` verts. Testé sur le jeu de démo (120
bénévoles, 100 créneaux) : ~0,3 s.

CLAUDE.md : nouvelle section 4 (solveur), renumérotation 5-10,
compétences retirées de « hors périmètre », journal étape 15.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-11 08:16:17 +02:00
AdamThomasandClaude Sonnet 5 36d108b47d Front : retrait des compétences, disponibilités en fenêtres horaires
Aligne le frontend sur le back-end (étape 14).

- Types : suppression de `skill_codes` / `required_skill_codes` /
  `AvailabilityStatus` / `MISSING_REQUIRED_SKILL` ; ajout de
  `AvailabilityRead` / `AvailabilitySet` et de la surface API
  `availability` (list / replace / listEdition) + hooks associés.
- Fiche bénévole : bloc « compétences » remplacé par un éditeur de
  disponibilités (plages date + heures libres, `PUT` en bloc).
- Assistant d'import : plus de colonnes compétences ni disponibilités
  (mapping réduit à l'identité) ; rapport simplifié.
- Grille des besoins : chaque candidat porte un badge de couverture
  (disponible / partiel / non déclaré) calculé sur ses fenêtres, et la
  liste est triée « disponibles d'abord ». Nouvel util
  `availabilityCoverage` / `uncoveredMinutes` dans `lib/planning`,
  portage du moteur back-end.
- Mock : `state.availability` passe à des fenêtres `AvailabilityRead[]`,
  validation `OUTSIDE_AVAILABILITY` recalculée sur l'union des fenêtres,
  jeu de démo et import CSV adaptés.

typecheck / lint / build / smoke au vert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 23:01:59 +02:00
AdamThomasandClaude Sonnet 5 be51770b51 Retrait des compétences ; disponibilités en fenêtres horaires libres
Compétences supprimées de bout en bout :
- tables `skill`, `volunteer_skill`, `shift_required_skill` ;
- `Volunteer.skill_codes`, `Shift.required_skill_codes`, colonne `skills`
  de l'import, règle de validation `MISSING_REQUIRED_SKILL`, jeu de démo.

Disponibilités repensées : `Availability` passe de (`day_part_id`,
`status` tri-état) à une **fenêtre horaire libre** (`date`, `start_time`,
`end_time` ; `end <= start` ⇒ passe minuit), plusieurs par jour.
- Nouveaux endpoints `GET`/`PUT /editions/{id}/volunteers/{vid}/availability`
  (PUT = remplacement complet, COORDINATION) et
  `GET /editions/{id}/availability` (toute l'édition, pour la grille).
- `OUTSIDE_AVAILABILITY` (toujours WARNING) : compare le créneau à l'union
  des fenêtres du bénévole ce jour-là ; renvoie `date` + `uncovered_minutes`.
  `DayPart` ne sert plus qu'à la borne de « journée » du cumul 8 h.
- L'import ne gère plus les disponibilités (fenêtres saisies dans l'app) :
  `values.py` supprimé, mapping réduit à nom/email/téléphone/commentaire.

Migration 0007 (recrée `availability`). `AvailabilityStatus` supprimé.
158 tests, `alembic check` et `ruff` au vert. Front à suivre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 22:46:45 +02:00
AdamThomasandClaude Sonnet 5 81609eecc5 Front : programmation des artistes et vœux des bénévoles
Aligne le frontend sur le back-end (étape 13).

- Types : retrait de `tshirt_size` / `diet` (Volunteer et mapping
  d'import) ; ajout de `ArtistRead/Create/Update` et des champs
  `artist_ids` / `wished_position_ids` sur la fiche bénévole.
- Surface API `artists` (HTTP + mock) et hooks
  `useArtists` / `useCreateArtist` / `useUpdateArtist` / `useDeleteArtist`.
- Nouvel onglet Configuration « Programmation » : liste d'artistes
  éditable, colonne par jour de l'édition (création, renommage,
  suppression ; 409 doublon, 422 nom vide).
- Fiche bénévole & formulaire d'ajout manuel : champs t-shirt / régime
  retirés, sélecteurs « Artistes à voir » et « Postes souhaités »
  (puces retirables, 422 si id étranger).
- Mock : jeu de démo enrichi (2 artistes par jour, vœux aléatoires),
  cascade sur suppression d'artiste / de poste, import CSV sans
  t-shirt / régime.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 22:22:45 +02:00
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
AdamThomasandClaude Sonnet 5 609514369a Bénévole : retrait de la taille de t-shirt et du régime alimentaire
Champs `tshirt_size` et `diet` supprimés du modèle `Volunteer`, des schémas
d'API, de l'import CSV/Excel (mapping, suggestions, exécution) et du jeu de
démonstration. Énum `TshirtSize` et helper `parse_tshirt_size` supprimés.
Migration 0005. Clé i18n `import.row.invalid_tshirt_size` retirée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 22:07:00 +02:00
AdamThomasandClaude Sonnet 5 ccf3438eef Bénévoles : ajout manuel, import de fichier guidé et gestion complète
Écran « Bénévoles & import » remanié autour des trois méthodes d'ajout
demandées (la 3e — formulaire d'inscription en ligne — reste à faire) :

- Ajout manuel : formulaire complet (prénom, nom, email, téléphone,
  taille de t-shirt, régime, commentaire, compétences) ; l'erreur 409
  `volunteer.duplicate_email` est affichée.
- Import de fichier : assistant en 3 étapes (fichier → association des
  colonnes + colonnes de disponibilité optionnelles → simulation puis
  import réel) avec aperçu et rapport ligne par ligne.
- Gestion : recherche par nom / email, fiche éditable étendue (régime et
  commentaire ajoutés), suppression RGPD en cascade inchangée.

Le bouton « créer 4 exemples » (maquette) est retiré. Traductions
`config.vol.*` et miroir des messages `import.*` du back-end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 21:55:59 +02:00
AdamThomasandClaude Sonnet 5 df8d6733db Import bénévoles : couche API (analyse + exécution) et mock CSV
- `PlanningApi.imports.analyze/execute` : envoi `multipart/form-data`
  (le client HTTP laisse le navigateur poser le Content-Type).
- Types miroir des schémas back-end (`ImportAnalyzeResponse`,
  `ImportMapping`, `ImportReport`, `ImportRowReport`, `ImportIssue`…).
- Hooks `useAnalyzeImport` / `useExecuteImport` ; l'exécution réelle
  invalide les bénévoles et le planning.
- Implémentation mock (mode démo) : lecture CSV, suggestion de mapping,
  simulation/exécution avec rapport ligne par ligne. Excel renvoie
  `import.error.xlsx_demo_only`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 21:55:50 +02:00
AdamThomasandClaude Sonnet 5 06f8736c33 Grille & vue bénévole : message quand il n'y a pas de données
- components/state : composant EmptyNote (encadré pointillés).
- GrillePage : si aucun créneau, message au lieu de la grille vide.
- VolunteerViewPage : si aucun bénévole, message ; si un filtre ne
  renvoie rien, message à la place du tableau (avant, l'en-tête collant
  restait affiché).

typecheck + lint + build + smoke OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 21:22:10 +02:00
AdamThomas 385c46c241 Vite : retirer le préfixe /api avant de forwarder au backend
Le proxy dev transmettait /api/editions/... tel quel → 404 (les routes du
backend sont sous /editions/...). Ajout d'un `rewrite` qui enlève /api.
2026-09-10 21:18:10 +02:00
AdamThomasandClaude Sonnet 5 b894b1d270 Makefile : make demo et make blank en une commande
- make demo  : db + backend + frontend + jeu de démonstration
- make blank : idem mais édition vierge + 1er compte coordination (params
  surchargeables : EMAIL, PASSWORD, NAME, START, END)
Chaque cible applique les migrations, prépare les données et bascule le
frontend sur l'API réelle (VITE_* dans .env). `make down` / `make logs`.

app/bootstrap.py : commande sous-jacente de `make blank`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 21:14:00 +02:00
AdamThomasandClaude Sonnet 5 6c4297b5fc Ajout de python -m app.bootstrap : instance vierge sans jeu de démo
Crée une édition + le premier compte coordination (mot de passe) + 3 créneaux
types par défaut, et affiche l'UUID à mettre dans VITE_EDITION_ID. Documenté
dans README (« Vraie instance, sans jeu de démo »).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 21:05:14 +02:00
AdamThomasandClaude Sonnet 5 3bc0cb0770 Config : retrait du bouton ad hoc « ajouter un créneau »
Les créneaux viennent des créneaux types (génération à la création d'un poste
+ « Générer les créneaux »). Le bouton par jour n'a plus lieu d'être.
Nettoyage : hooks/clés i18n devenus inutiles.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 21:02:21 +02:00
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
AdamThomasandClaude Sonnet 5 cee581b832 Grille : case mauve quand un créneau est en sureffectif
- GrillePage : quand assigned > ideal_staff, la case passe en mauve (glyphe ▼)
  et prend le pas sur l'état « complet ».
- Nouvelle entrée de légende « SUREFFECTIF ».
- tailwind : couleurs mauve / mauve-hover.
- mock : quelques créneaux volontairement en sureffectif pour la démo.

typecheck + lint + build + smoke OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 20:16:17 +02:00
AdamThomas 72e23185ac CLAUDE.md : journal étape 11 (édition éditable) 2026-09-10 20:03:06 +02:00
AdamThomasandClaude Sonnet 5 e0a0c56307 Édition éditable (dates) + robustesse de l'écran Postes & créneaux
Back-end :
- GET /editions/{id} (tout compte de l'édition) et PATCH /editions/{id}
  (COORDINATION) : name, year, start_date, end_date, retention_days.
  Refus si end_date < start_date (edition.dates_inconsistent). Tests.

Front-end :
- api : EditionRead/Update, editions.get/update (http + mock), hooks
  useEdition / useUpdateEdition.
- ConfigGrid : le bouton « ajouter un poste » sélectionne le poste créé et
  affiche l'état (pending) + les erreurs (ErrorNote) — avant, un échec ou
  même un succès passaient inaperçus. Renommage du poste + choix de la
  catégorie + suppression dans l'en-tête. Une colonne par jour de l'édition
  (même vide) → on peut ajouter un créneau n'importe quel jour.
- ConfigRules : formulaire d'édition (nom + dates) → c'est là qu'on ajoute
  ou retire des jours ; compteur de jours ; retour d'enregistrement.
- i18n mis à jour.

144 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:02:50 +02:00
AdamThomasandClaude Sonnet 5 9521034207 Emails / logs : trace de remise + logging racine fiable
- deliver_many : log INFO « Emails remis : N/M — sujet=… » sur chaque lot
  (sans destinataire ni corps). Avant, un envoi réussi ne journalisait rien.
- configure_logging : basicConfig(force=True) pour que les logs app.* sortent
  dans la sortie du conteneur même quand uvicorn configure le logging avant
  l'import de app.main.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 19:42:34 +02:00
AdamThomasandClaude Sonnet 5 b84f62dfab Emails : robustesse SMTP + diagnostic
- config.py : retrait des identifiants OVH réels remis en dur ; défauts
  neutres (email_enabled=false). Les vraies valeurs vont dans .env.
- open_smtp : le port 465 force le SSL implicite (SMTPS) même si SMTP_SSL=false
  — c'était la cause des envois qui ne partaient pas (connexion en clair sur un
  port TLS -> timeout).
- deliver_many : les erreurs de connexion / auth / rejet sont journalisées
  avec le message du serveur et ne remontent plus (l'appel HTTP n'échoue pas).
- Lien magique : envoi en tâche de fond (BackgroundTasks) comme les
  notifications ; ne bloque plus la réponse.
- app/email/check.py : `python -m app.email.check <destinataire>` — affiche la
  config effective et envoie un email de test, avec la cause exacte en cas
  d'échec.
- Tests : fixture autouse forçant EMAIL_ENABLED=false (aucun test ne touche un
  vrai SMTP, quel que soit le .env local) ; test_full_flow lit l'email capturé
  au lieu des logs. 137 tests OK.
- .env.example : notes port 465 / From = boîte authentifiée / commande check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 19:31:02 +02:00
AdamThomasandClaude Sonnet 5 6b08dcc953 Frontend : écran Messages branché sur l'envoi d'emails
- api : PlanningNotificationRequest/Result dans schema.ts, méthode
  notifications.sendPlanning (http + mock), hook useSendPlanningNotification
- mock : compte les destinataires avec/sans email, émet des liens magiques
  quand {lien_connexion} est présent
- ConfigMail : plus une maquette. Éditeur objet/corps, variables cliquables
  ({prenom} {nom} {nb_creneaux} {nb_heures} {creneaux} {lien_connexion}),
  boutons « Envoyer le récapitulatif » et « Envoyer avec lien de connexion »,
  retour d'envoi (queued / sans adresse) et ErrorNote
- i18n config.mail.* mis à jour ; onglet « MESSAGES » (plus « (MAQUETTE) »)

typecheck + lint + build + smoke : OK

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 08:13:28 +02:00
AdamThomasandClaude Sonnet 5 b268e308ab Étape 10 : envoi d'emails par SMTP
- app/email/ : sender (smtplib, STARTTLS ou SSL, connexion réutilisée pour un
  lot) + templates (rendu, textes dans locales/fr.json). EMAIL_ENABLED=false
  (défaut) ⇒ journalisé seulement, sans destinataire ni corps (RGPD).
- Lien magique : POST .../auth/magic-link envoie désormais l'email
  (PUBLIC_BASE_URL + magic_link_path + ?token=). Helper app/auth/magic_link.py
  partagé. Sans SMTP, le jeton non nominatif reste journalisé pour le dev.
- Récapitulatif de planning : POST /editions/{id}/notifications/planning
  (COORDINATION). subject/body sont des modèles ({prenom}, {nom}, {nb_creneaux},
  {nb_heures}, {creneaux}, {lien_connexion} → lien magique personnel émis).
  Cible tous les bénévoles avec email ou une liste. Envoi en BackgroundTasks.
- Config : PUBLIC_BASE_URL, EMAIL_ENABLED, SMTP_* ; .env.example,
  docker-compose (dev + prod), deploy/README.
- 9 tests (sender activé/désactivé, rendu, notifications : contenu, gabarit par
  défaut, lien magique embarqué, sous-ensemble, réservé COORDINATION).
  137 tests au total ; ruff + alembic check verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 08:08:55 +02:00
AdamThomasandClaude Sonnet 5 977bd4dc34 Frontend : nettoyage des reliquats du statut d'affectation
Le gros de la suppression a été fait dans 8d333de ; il restait des libellés :
- i18n : « CONFIRMÉS / IDÉAL » → « AFFECTÉS / IDÉAL », note de bas de grille
  sans « proposés », clé mobileVol.slotConfirmed → slotAssigned
- components/ui.tsx : tons de Badge « confirmed » / « proposed » (inutilisés)
- ConfigMail : texte d'email par défaut sans « confirmer chaque créneau »

typecheck + lint + build + smoke : OK

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 07:58:12 +02:00
AdamThomasandClaude Sonnet 5 8d333deb23 Frontend : suppression du statut d'affectation (suite Étape 9)
Le back-end (Étape 9) a retiré AssignmentStatus : une affectation est valide
dès sa création, la retirer = supprimer la ligne.

- schema.ts : AssignmentRead/Create/Update sans `status` ;
  MatrixShift.{confirmed, proposed} → `assigned`.
- mock : générateur sans statut (plus de rollStatus / DECLINED) ; matrice
  = compte unique ; create → validation toujours exécutée ; PATCH ne porte
  plus que source/locked ; runValidation compte toutes les affectations.
- lib/planning : suppression de isActive() et statusLabel().
- Grille + panneau : cellule « affectés / idéal », plus de +proposés ni de
  badge confirmé/proposé ni de bouton « confirmer ».
- Vue par bénévole : une seule trame d'affectation (bleu).
- Mobile bénévole : plus de bouton « JE CONFIRME » ni de bannière.
- Mobile référent : plus de libellé de statut par équipier.
- Onglet 03 « Confirmations » supprimé (route, page, nav, ROUTE_ROLES) —
  il n'a plus d'objet. Coordination : onglets 01, 02, 06.
- i18n : clés de statut / de l'écran Confirmations retirées.
- Vérifs : build tsc + vite, eslint (0 problème), npm run smoke.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwswqSChc3Rwtu61pRXiY2
2026-09-10 07:51:11 +02:00
AdamThomasandClaude Sonnet 5 6e16db4b7e Frontend : connexion (mot de passe + lien magique) et onglets par rôle
Authentification alignée sur le back-end (Étapes 7/8) :
- src/api : schema auth (AccountRead, TokenResponse…), session.ts (jeton
  Bearer partagé + callback 401), méthodes auth du client HTTP
  (POST .../auth/login, .../auth/magic-link, .../auth/verify, GET /auth/me)
  et du mock (comptes coordination + référents + bénévoles, tokens magiques,
  sessions).
- src/auth : AuthProvider/useAuth (jeton en localStorage, réhydratation via
  /auth/me, déconnexion sur 401), RequireAuth (garde + contrôle de rôle),
  HomeRedirect, table ROUTE_ROLES.
- Pages : /connexion (lien magique, avec lien de démo affiché en mode mock),
  /connexion/coordination (email + mot de passe).
- Router : toutes les routes métier passées derrière <RequireAuth> ;
  redirection vers l'accueil du rôle.
- AppShell : onglets filtrés par rôle (01/02/03/06 coordination, 04 bénévole,
  05 référent), email + rôle + bouton Déconnexion.
- Mobile bénévole : cible le bénévole du compte connecté ; dégradation
  propre quand l'API refuse le détail des créneaux à un bénévole.
- Mobile référent : cible le poste dont le compte est référent.
- i18n : clés auth / rôles.
- Vérifs : build tsc + vite, eslint (0 problème), npm run smoke (flux
  login / lien magique + rendu des 6 écrans par rôle et des pages publiques).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwswqSChc3Rwtu61pRXiY2
2026-09-10 07:43:00 +02:00
AdamThomasandClaude Sonnet 5 464635656a Étape 9 : suppression du statut d'affectation (plus de confirmation)
Une affectation est valide dès sa création ; la retirer = supprimer la ligne.

- Modèle : suppression de AssignmentStatus, ACTIVE_ASSIGNMENT_STATUSES et de
  Assignment.status ; migration 0003 (drop_column, alembic check vert)
- Loader de validation : toutes les affectations du bénévole comptent, plus de
  filtre par statut
- API : AssignmentCreate/Update/Read sans status ; création → validation
  toujours exécutée (blocage si violation bloquante) ; PATCH ne porte plus que
  source/locked
- Matrice : MatrixShift.{confirmed, proposed} → {assigned} (compte unique)
- Seed : affectations sans statut, résumé « Affectations » (260)
- Tests mis à jour (matrice, loader, modèle, API) ; 128 tests au total
- CLAUDE.md : glossaire, modèle, sémantique de validation, périmètre

Frontend : les références au statut (propositions, badges confirmé/proposé,
écran de confirmation mobile, mock, schema.ts) restent à retirer séparément.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 07:36:06 +02:00
AdamThomasandClaude Sonnet 5 8a879ca5c3 Étape 8 : connexion par mot de passe pour la coordination
Deux systèmes d'authentification, une même session JWT :
- COORDINATION : login e-mail + mot de passe (bcrypt), POST .../auth/login ;
  POST .../auth/change-password pour le compte courant
- RÉFÉRENTS / BÉNÉVOLES : lien magique inchangé, mais refusé pour un compte
  qui a un mot de passe

- Account.password_hash (nullable) + migration 0002 ; propriété auth_method
  ("password" / "magic_link") exposée dans AccountRead
- app/auth/passwords.py : hash/verify bcrypt, longueur mini 10
- Création de compte : password requis pour COORDINATOR, interdit sinon
- seed : la coordination de démo a un mot de passe, affiché dans le résumé
- 12 tests (login ok/ko, séparation des deux systèmes, change-password,
  validation à la création) ; 129 tests au total ; alembic check vert

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-09 23:42:31 +02:00