Remplace le simple lien Yapla statique par une vraie intégration API SumUp :
checkout hébergé créé à la soumission du formulaire, statut de paiement
(PENDING/PAID/FAILED/EXPIRED) tenu à jour par un webhook SumUp et visible sur
la fiche du bénévole. La cotisation n'est plus un montant unique : la
coordination configure autant de types de paiement qu'elle veut (libellé +
montant, ex. « Cotisation classique », « Mécénat »), et le bénévole en
choisit un à l'inscription — le libellé et le montant choisis sont figés sur
le bénévole, indépendamment d'une modification ultérieure de la config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La cellule « Affectés / idéal » (figée en haut à gauche) et le libellé
« N postes » partageaient le même z-index (6) — une égalité entre deux
éléments sticky peut se départager de façon imprévisible pendant un
défilement selon le navigateur. Par ailleurs, l'infobulle de survol
d'une cellule de créneau (z-20) n'était pas isolée dans son propre
contexte d'empilement et rivalisait donc directement avec les en-têtes
figés (max z-6) : un survol déclenché par la molette pendant un
défilement (le curseur reste immobile, ce sont les cellules qui
défilent dessous) pouvait la faire passer au-dessus de l'en-tête le
temps de sa transition de disparition.
Hiérarchie refaite sans égalité (coins figés 20/21 > en-têtes 11/12 >
libellés de poste 10 > cellules, chacune isolée via `relative z-0`).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Chaque cellule de la grille (label de poste, en-têtes, créneaux, cases
vides) recevait sa position via l'ordre d'émission des enfants, en
comptant sur le fait que chaque ligne totalise exactement le nombre de
tranches de 30 min d'un jour pour que l'auto-flow CSS reste aligné. Un
poste dont les créneaux ne tombaient pas exactement sur ce total (deux
créneaux adjacents mais non alignés sur 30 min, courant depuis la
création de créneaux à la main) décalait tous les postes suivants —
leur nom de poste n'apparaissait plus sur la bonne ligne.
Chaque cellule reçoit maintenant sa position de grille explicitement
(gridColumn/gridRow calculés), immunisé contre ce genre de décalage :
l'anomalie d'un poste reste locale à sa propre ligne.
Nouveau test de non-régression (scripts/smoke.tsx:checkGridLayout) :
inspecte les gridRow réellement rendus plutôt que la simple absence de
crash — un décalage CSS n'est pas détectable par un rendu SSR ordinaire.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FormTemplate.yapla_url (lien vers la page de cotisation Yapla, doit
commencer par http(s)://, PATCH .../form comme description/is_open)
fait apparaître un encart « Payer ma cotisation » sous la case
WILLING_TO_PAY_FEE, sur le formulaire public. Configurable depuis le
panneau « Personnalisation » de l'écran /inscriptions. La case reste
une simple déclaration : rien ne vérifie que le paiement a eu lieu.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sous le référent, la liste des artistes qui jouent ce jour-là (heure de
passage + nom), tous postes confondus — repère utile même hors de la
régie (affluence prévisible au bar, sécurité...). Triée par horaire,
omise si aucun artiste ne joue ce jour-là.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FormTemplate porte désormais un logo (image, PUT/GET .../form/logo — GET
public comme .../form/public, affiché sur /inscription) et un texte
d'introduction libre (PATCH .../form, description désormais optionnelle
comme is_open). Écran de configuration (/inscriptions) : nouveau panneau
« Personnalisation » pour importer le logo et rédiger le texte. Le
formulaire public affiche le logo en grand au-dessus du panneau et le
texte juste sous le titre.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La photo ajoutée (fichier ou prise depuis l'appareil) est recadrée en carré
côté navigateur avant l'envoi (centré, tronque le surplus, réencodée en
JPEG ≤ 1024 px de côté) — elle est donc toujours carrée en base, sans
recadrage serveur, ce qui profite aussi aux vignettes des badges et des
feuilles de poste. Un aperçu agrandi (180 px) s'affiche dès que la photo
est choisie.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vignette carrée (14 mm, proportions préservées) à côté du nom de chaque
bénévole affecté, à côté de son téléphone — case vide si le bénévole n'a
pas de photo. Complète l'affichage déjà en place sur l'écran référent et
l'écran bénévole (étape 22).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Un poste ne génère plus de créneaux à sa création : la coordination crée
elle-même ceux dont elle a besoin, avec ses propres horaires de début et
de fin (ConfigGrid.tsx). La génération depuis les créneaux types reste
disponible en option (bouton « Générer les créneaux »).
La grille des besoins devient un vrai axe des temps : une colonne = 30 min,
bornée par jour à la plage réellement utilisée, avec les cases fusionnées
pour correspondre aux horaires de chaque créneau (buildTimeGrid /
placeInTimeGrid). La « Vue bénévoles » garde son ancien modèle (une colonne
par créneau distinct), plus adapté à son tableau bénévole × créneau.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nom + téléphone du référent du poste (Position.lead_account_id), juste
sous le sous-titre (jour/lieu) — « Référent : à désigner » si aucun.
Même source que le référent affiché à l'écran bénévole
(volunteers.py:get_volunteer_planning).
Vérifié : pytest (287 passés, dont les nouveaux tests avec/sans
référent), ruff, alembic check — tous verts (pas de changement
frontend, la feuille reste un simple téléchargement de PDF).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Une page par poste et par jour, listant les créneaux avec les bénévoles
affectés et leur téléphone — à imprimer et afficher sur place. Aucune
nouvelle entité : dérivé de Position/Shift/Assignment/Volunteer
existants (contrairement aux badges, pas de personnalisation à
configurer). Un créneau à besoin 0/0 sans affectation n'apparaît pas.
Backend : app/sheets/pdf.py (ReportLab, Platypus — SimpleDocTemplate/
Table plutôt que le canvas bas niveau des badges, plus adapté à un
rapport texte multi-pages), POST /editions/{id}/position-sheets/generate
(COORDINATION, {position_ids?} — absent = tous les postes).
Frontend : bouton « Feuilles de poste » dans la grille des besoins,
respecte le filtre de catégorie déjà actif à l'écran.
Vérifié : pytest (284 passés), ruff, alembic check, tsc --noEmit,
eslint, vite build, smoke (flux de bout en bout en mode démonstration)
— tous verts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Retire « constructeur de formulaire » des exclusions du CLAUDE.md (§ 5) —
décision explicite prise en conversation.
Un formulaire par édition (COORDINATION choisit les champs actifs parmi
neuf : email, téléphone, vœux de poste — max 2, vœux d'artiste — un par
soir de festival, vœu de compagnon en texte libre, disponibilité/
cotisation déclaratives, photo, commentaire ; nom/prénom toujours
collectés) ouvert/fermé à volonté, avec un lien à partager
(/inscription, aucun compte requis).
Backend : FormTemplate/FormField (migration 0013), quatre nouveaux champs
sur Volunteer (photo, companion_wish_note, self_declared_available,
willing_to_pay_fee), app/api/forms.py — les deux seuls endpoints publics
de toute l'API (GET .../form/public, POST .../form/public/submit),
protégés par hCaptcha (app/forms/captcha.py, désactivé sans
HCAPTCHA_SECRET_KEY) et un filet de sécurité global par édition
(form_submission_limiter). resolve_owned_ids/set_artists/
set_wished_positions déplacés vers app/api/_common.py, partagés avec
volunteers.py.
Frontend : écran de configuration (/inscriptions) avec lien copiable et
bascule ouvert/fermé, formulaire public (/inscription, hors RequireAuth)
avec widget hCaptcha chargé dynamiquement, capture photo native
(<input capture>), vœux de poste/artiste avec les mêmes règles que le
back-end. Réponses du formulaire visibles et éditables depuis la fiche
bénévole existante (ConfigVolunteers.tsx).
Vérifié : pytest (273 passés), ruff, alembic check, tsc --noEmit, eslint,
vite build, smoke (rendu SSR de chaque écran + flux de bout en bout du
formulaire en mode démonstration) — tous verts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Le format du badge n'est plus fixé à CR80 : il est déduit des pixels de
l'image du recto à l'import (résolution supposée 300 dpi,
app/badges/pdf.py:image_size_mm), stocké sur BadgeTemplate (migration
0012). L'éditeur (BadgesPage.tsx) et la génération PDF s'alignent sur ce
format réel ; CR80 reste le repère par défaut tant qu'aucune image n'est
importée. Le verso est ajusté sans déformation (preserveAspectRatio).
- Le champ « Postes affectés » liste désormais une ligne par créneau
(triées chronologiquement) : nom du poste + horaire, et le référent du
poste s'il y en a un et que ce n'est pas le bénévole badgé lui-même.
Texte multi-lignes pris en charge par la génération PDF (découpe sur
`\n`, hauteur de ligne calculée en mm) et par l'aperçu de l'éditeur.
- Corrige au passage un bug d'unités dans le positionnement vertical du
texte (police en points confondue avec des millimètres).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Recto personnalisable (fond image + champs positionnés : nom, poste(s)
affectés, catégorie, rôle) via un éditeur visuel glisser-déposer ; verso
fixe, la même image pour tous les bénévoles. Nouvel écran « Badges »
(COORDINATION uniquement, /badges).
Backend : modèle BadgeTemplate/BadgeField (migration 0011), génération
PDF isolée (app/badges/pdf.py, ReportLab — planches recto suivies des
planches verso correspondantes pour une impression recto-verso), route
POST /editions/{id}/badges/generate, tests (module pur + API).
Frontend : client HTTP (upload multipart, réponses binaires images/PDF),
client mock (mode démonstration), éditeur de positionnement en
millimètres (coordonnées partagées avec le backend), écran de génération
avec sélection des bénévoles et téléchargement.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Suite de la revue de sécurité (commit précédent) : ni /auth/login (mot
de passe COORDINATION) ni /auth/magic-link n'avaient de limite de
tentatives — aucun verrou de compte, aucun throttling, avant ouverture
au public.
`app/auth/rate_limit.py` : limiteur en mémoire (un seul processus
uvicorn, cf. Dockerfile), à créneau glissant, par clé (édition, email)
plutôt que par IP — le VPS ne transmet pas encore l'IP réelle du
client à travers Caddy, et le risque réel ici est de deviner le mot de
passe d'un compte connu (peu de comptes protégés par mot de passe),
pas une attaque distribuée sur des emails au hasard.
- `/auth/login` : 5 tentatives/minute → 429 `error.too_many_requests`.
- `/auth/magic-link` : 3 demandes/5 min (évite aussi le spam d'emails
vers une victime ou vers le SMTP).
Fixture `_reset_rate_limiters` (autouse) pour ne pas faire fuiter l'état
d'un test à l'autre. Testé : dépassement, indépendance des clés,
expiration de la fenêtre, scénarios bout en bout sur les deux
endpoints.
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
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
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
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
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
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
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
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
- 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
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
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
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
- `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
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
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
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
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
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
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
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
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
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
- 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
- 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
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
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
- app/auth/ : jetons (lien magique aléatoire haché SHA-256, JWT HS256),
dépendances get_current_account / current_account_in_edition / require_roles
+ require_coordinator / require_staff, et ensure_can_write_position/_shift
- Endpoints app/api/auth.py : POST .../auth/magic-link (202 constant, pas de
divulgation), POST .../auth/verify (JWT, jeton à usage unique), GET /auth/me
- Envoi d'email stubé : le lien est journalisé (logger app.auth, identifiants
seulement, aucune donnée nominative)
- app/api/accounts.py : CRUD des comptes réservé à la COORDINATION
- Rôles appliqués sur toute l'API :
COORDINATION accès complet ; RÉFÉRENT lecture globale + écriture sur ses
seuls postes/créneaux/affectations ; BÉNÉVOLE sa fiche et ses affectations,
aucune écriture
- imports.py aligné sur la dépendance get_edition (404 avant 403)
- Secret JWT porté à >= 32 octets (config, .env.example, docker-compose)
- seed : crée un compte VOLUNTEER pour chaque bénévole avec email (104 comptes)
- conftest : fixtures make_account / coordinator / as_account / raw_client ;
la suite existante s'exécute en COORDINATION
- 13 tests d'authentification + de rôles ; 119 tests au total ; alembic check
toujours vert (Account / MagicLinkToken déjà dans la migration 0001)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
- app/seed.py, exécutable via python -m app.seed, idempotent et déterministe
(graine fixe), sans donnée nominative réelle
- Édition « Festival des Lucioles » 2026 : 120 bénévoles, 15 postes,
4 jours (montage/festival/démontage), 3 demi-journées, 8 compétences,
~100 créneaux, ~280 affectations actives, disponibilités tri-état
- ~12 créneaux volontairement sous l'effectif minimal (trous d'effectif)
- 3 affectations à avertissement (compétence absente, hors disponibilité,
repos insuffisant), 0 violation bloquante
- Passe de vérification finale via le moteur de validation : le résumé imprime
le décompte des avertissements par règle et le nombre de bloquantes
- covered_slots rendue publique dans validation.engine (réutilisée par le seed)
- 2 tests (dataset réaliste, déterminisme/idempotence) ; 106 tests au total
Fin du premier incrément backend.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
- CRUD complet, scopé par édition, sur Position, Shift, Volunteer, Assignment
(routers app/api/*, schémas Pydantic, dépendance get_edition partagée)
- Volunteer : gestion des compétences par codes (création à la volée),
suppression = effacement complet en cascade (RGPD)
- Shift : contrôle fin > début, poste de la même édition, compétences requises
- Assignment : toute écriture active passe par le moteur de validation ;
violation bloquante -> 422 error.validation_failed, avertissements renvoyés
dans la réponse. Endpoint POST .../assignments/validate en simulation.
app/validation/loader.py construit la ValidationInput depuis la base.
- GET /editions/{id}/matrix : postes -> créneaux -> {min, ideal, confirmed,
proposed}, source de l'écran principal
- 30 tests d'API (CRUD, 404/409/422, cascade, blocage, avertissements, matrice,
loader) ; 104 tests au total ; alembic check toujours vert
- Clés de traduction shift.* / assignment.* / volunteer.* ajoutées
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
- Fichier unique, processus en deux temps : POST /editions/{id}/import/analyze
(colonnes détectées, aperçu, mapping suggéré, demi-journées de l'édition)
puis POST /editions/{id}/import/execute (mapping validé)
- Parser CSV (délimiteur et encodage devinés, BOM) et XLSX (openpyxl),
détection XLSX par nombre magique
- Vocabulaire des valeurs de disponibilité (tri-état, accents/casse) et des
tailles de t-shirt ; compétences créées à la volée dans le référentiel
- Rapport ligne par ligne : codes de traduction + paramètres, jamais de
phrase. Doublons : upsert sur email, signalement sur (nom, prénom) sans
email, doublons intra-fichier. Mode simulation (rollback, rien de persisté)
- 40 tests : parser, valeurs, service (tous les cas), endpoints
- Dépendances : python-multipart, openpyxl
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM