Un référent rattaché à une fiche bénévole voit aussi ses propres créneaux
(vue bénévole) dans un second onglet de son écran, ouvert d'office quand il
tient un créneau de bénévole hors de ses postes ; « TOI » dans l'équipe
pointée.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Étape 55 : écrans /mobile-benevole et /mobile-referent d'après la maquette
(design/Planning bénévole mobile) — créneau en cours ou prochain mis en
avant, planning par journée de festival avec frise horaire, compteurs et
pointage du référent par créneau.
Étape 56 : message de l'organisation (Edition.volunteer_message, onglet
« Message aux bénévoles ») et message de chaque poste (Position.message,
saisi par le référent), migration 0029, affichés sur l'écran bénévole.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La photo recouvrait le nom et le texte libre (champs dessinés dans l'ordre
alphabétique de leur type). BadgeField.z_order (migration 0028, champs
existants renumérotés photo dessous) fixe l'ordre de dessin du PDF et de
l'aperçu ; boutons « Premier plan / Avancer / Reculer / Arrière-plan »
dans l'inspecteur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tri des lignes par nom ou par total d'heures affectées, croissant ou
décroissant (boutons « Trier par » et clic sur l'en-tête « TOTAL H ») ;
à total égal, l'ordre par nom est conservé.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Créateur de badges refait d'après la maquette : champs sur le recto et le
verso (BadgeField.face, verso placé en miroir dans le PDF pour le
recto-verso), fond de zone, réduction automatique du texte, photo ovale,
initiales sans photo, nom sur deux lignes (migration 0026) ; éditeur en
trois colonnes (champs, aperçu recto/verso avec de vrais bénévoles,
inspecteur), formats prédéfinis, annuler, flèches, zoom, repères de
découpe, dépôt d'image sur l'aperçu, contrôle de tous les badges avant
génération.
Nombre de badges par bénévole (Volunteer.badge_count, 1 par défaut,
migration 0027) : champ sur la fiche, autant d'exemplaires sur les
planches, total affiché à la génération, colonne dans l'export Excel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le texte des champs passe à la ligne pour tenir dans la largeur de son
cadre (wrap_text) au lieu de déborder. Les créneaux affectés s'affichent
en tableau : le jour de festival en en-tête, puis une colonne horaire et
une colonne poste (plus de référent sur le badge) ; même tableau dans
l'aperçu de l'éditeur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Texte libre commun à tous les badges, saisi dans l'éditeur
(BadgeField.text, migration 0025) ; le « Texte du badge » de la fiche le
remplace pour un bénévole. Champs ramenés à l'intérieur du badge et champ
sélectionné au premier plan : la photo restait bloquée hors d'un badge
plus étroit que le CR80 (bornes de glisser-déposer négatives).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Section « Photo » sur la fiche bénévole (ajouter / remplacer / retirer),
PUT/DELETE /editions/{id}/volunteers/{vid}/photo (COORDINATION),
validation de l'image partagée avec le formulaire public (read_photo).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Champ « Photo du bénévole » (PHOTO) : photo du formulaire recadrée au
cadre, orientation EXIF appliquée, réduite à 300 dpi ; cadre vide sans
photo. Champ « Texte libre » (FREE_TEXT) alimenté par le nouveau « Texte
du badge » de la fiche bénévole (Volunteer.badge_text, migration 0024),
repris dans l'export Excel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
En-tête (onglets sur une seconde ligne en tablette, menu déroulant en
mobile), panneau d'affectation et fiche bénévole en tiroir par-dessus sous
lg: (plein écran en mobile), colonne des postes de la grille rétrécie,
liste des bénévoles réduite en mobile, écran « Postes & créneaux » empilé,
aperçu de l'éditeur de badges mis à l'échelle (glisser-déposer tactile),
ligne des types de paiement repliée, marges réduites — desktop inchangé.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le chevauchement de créneaux (SHIFT_OVERLAP) n'est plus bloquant : il
remonte en avertissement et l'affectation est enregistrée. Le solveur ne
l'exclut plus mais applique un fort malus au bénévole déjà affecté
ailleurs au même moment (proposé seulement sous l'effectif minimal, faute
de mieux) ; il n'est jamais reproposé sur un créneau où il est déjà.
Même malus dans le classement par défaut du panneau et dans le mock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Une ligne de log par requête (route modèle, statut, durée, code d'erreur,
compte), identifiant X-Request-ID repris dans chaque log et affiché à
l'utilisateur sous une erreur, exceptions non gérées tracées (500 JSON)
- Journal d'accès d'uvicorn coupé : il exposait le jeton du formulaire public
- Logs métier : paiement SumUp, connexion, import, plannings, solveur
- Nom du bénévole dans la description du checkout SumUp
- Code et identifiant de transaction SumUp enregistrés au paiement
(migration 0023), affichés sur la fiche et dans l'export Excel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Disponibilités déclarées supprimées (table `availability`, migration 0022,
API, éditeur de la fiche bénévole, badge de couverture du panneau
d'affectation, colonnes des statistiques) ainsi que les règles
OUTSIDE_AVAILABILITY, DAILY_HOURS_EXCEEDED et INSUFFICIENT_REST, les malus
de disponibilité et de repos du solveur et le filtre « Surchargés » de la
vue bénévoles. Seul le chevauchement reste contrôlé. La case « je suis
disponible » du formulaire public est conservée.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La « journée de festival » commence désormais au début du plus long creux
laissé par les créneaux types sur 24 h (l'heure où le dernier se termine),
et non plus au début de celui de plus petit `order` : un créneau type
d'ordre 0 déplacé à 21:00 faisait commencer chaque journée des grilles à
21:00, jusqu'à 03:00 le lendemain. Même règle côté backend
(validation.engine.day_boundary, réutilisée par la génération de créneaux)
et frontend (dayBoundaryFromDayParts).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nouvel onglet « Statistiques » (/statistiques, COORDINATION), alimenté
par GET /editions/{id}/stats (app/api/stats.py, recalculé à chaque appel,
aucune nouvelle entité) :
- inscriptions : total, 7 derniers jours, évolution cumulée par jour
- réponses : part des bénévoles ayant renseigné chaque information,
artistes les plus demandés
- cotisations : en ligne / sur place / aucune, montants par type,
mécénat, formulaires remplis sans paiement abouti
- besoins face aux bénévoles, par journée de festival : heures × personnes
demandées vs disponibilités déclarées vs affectées ; par poste, places
pourvues (plafonnées à l'effectif idéal de chaque créneau) et rapport
vœux / place
Graphiques SVG maison (components/charts.tsx) avec infobulles au survol
et au clavier, vue tableau pour chacun, palette validée (daltonisme,
contraste), mise en page vérifiée sur ordinateur et mobile. Le mock
calcule les mêmes chiffres et étale les inscriptions du jeu de démo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jusqu'ici seul un paiement en ligne apparaissait ; un bénévole qui paiera
sur place ne se distinguait pas d'un bénévole sans cotisation. Mode déduit
des champs existants (lib/fee.ts:feeMode, exporting/xlsx.py:_payment_mode),
sans migration :
- fiche : section « Cotisation » toujours visible — paiement en ligne
(type, montant, statut, mécénat et adresse du reçu) ou case « Paiera sur
place » modifiable à la main
- liste : colonne « Cotisation » (Sur place / En ligne ✓ / Mécénat ✓ / —)
- export Excel : colonne « Mode de paiement »
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Un créneau saisi « 00:00–01:00 » sur la carte du samedi était enregistré
le samedi à 00:00 (nuit de vendredi à samedi), donc affiché la veille dans
la grille des besoins. Une heure de début avant le début de journée
(premier créneau type) désigne désormais la nuit qui suit : dimanche
00:00, à la suite de la soirée du samedi.
Même « journée de festival » partout (validation/engine.py:day_boundary,
festival_day ; miroir front lib/planning.ts:festivalDay) : écran Postes &
créneaux, feuilles de poste (une seule page pour une soirée qui passe
minuit) et chaînage du solveur (00h–01h enchaîne 23h–00h).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- nouveau modèle PDF du cerfa
- case « Virement, prélèvement, carte bancaire » cochée (paiement SumUp)
- date du versement et date de signature repositionnées
- message de confirmation du paiement : annonce l'email de confirmation
et le reçu fiscal pour le mécénat
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sans identifiants SumUp (code marchand + clé API) ou sans aucun type de
paiement, le formulaire public n'affiche qu'une simple case « cotisation »
et crée le bénévole directement : ni choix du type de paiement, ni étape
d'adresse mécénat, ni paiement en ligne. Rien ne le signalait, si bien
qu'une option « Mécénat » configurée semblait ne pas fonctionner.
L'écran affiche désormais l'état Actif / Inactif sous « Types de
paiement », avec la raison.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FormPaymentOption.free_amount (migration 0021) : case « Montant libre »
sur l'écran Inscriptions ; le montant configuré devient le minimum et le
bénévole saisit le sien sur le formulaire public (payment_amount, virgule
ou point, 2 décimales max, plafond 10 000 € contre une faute de frappe —
422 form.invalid_free_amount sinon). Validé côté client avant l'envoi,
le serveur restant seul juge. Le montant choisi est celui du checkout
SumUp, recopié sur la fiche, l'email de confirmation et le cerfa.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Volunteer.mecenat_receipt_number (migration 0020, unique) : année du
paiement + rang sur 5 chiffres (202600001…), une seule numérotation pour
toutes les éditions, repartant à 1 chaque année. Attribué à la
confirmation du paiement, ou au premier export pour les bénévoles payés
avant (dans l'ordre des paiements), puis jamais réattribué — un reçu
regénéré garde son numéro. Attribution sérialisée par un verrou
pg_advisory_xact_lock (webhook et sondage peuvent confirmer deux
paiements à la fois). Imprimé centré dans la case « Numéro d'ordre du
reçu » en page 1 du cerfa.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quand un paiement « mécénat » est confirmé, l'email de confirmation porte
en pièce jointe le reçu fiscal (cerfa 11580*05) rempli du bénévole, avec
une phrase l'annonçant dans le corps. Généré en tâche de fond : un échec
de génération envoie l'email sans pièce jointe plutôt que rien.
- build_message/send acceptent des pièces jointes
- app/mecenat/receipts.py : passage Volunteer → MecenatReceipt, partagé
avec l'export coordination
- modèle cerfa remplacé par une version pré-remplie avec l'association
(organisme bénéficiaire, cases 200/978, signature) ; position de la
date du versement ajustée
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Composant RequiredMark et prop `required` sur FieldLabel ; marqués
uniquement les champs réellement exigés à la soumission (nom, prénom,
email/téléphone/disponibilité/cotisation s'ils sont activés, étape
d'adresse mécénat), avec une légende « * champ obligatoire » en tête du
formulaire. Champ photo déplacé avant la disponibilité, libellé précisé
(« pour ton badge »).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Lien d'inscription protégé par un jeton (FormTemplate.access_token,
/inscription/<token>), avec régénération depuis l'écran de configuration.
- Reçus fiscaux mécénat (cerfa 11580*05) : un PDF par bénévole ayant payé en
mécénat, donateur/montant (chiffres et toutes lettres en français,
app/mecenat/number_words.py)/date complétés par-dessus le modèle officiel.
- Jeu de démonstration : bénévoles avec cotisation classique ou mécénat payée.
- Infobulles (HelpTip et détail de cellule de la grille des besoins)
repositionnées en position: fixed via JS, bornées à l'écran.
- Revue de sécurité : logo des emails en pièce jointe inline au lieu d'une
URL (fuyait le jeton d'inscription), sujet retiré du log de remise des
emails (pouvait porter un nom de bénévole), photo du formulaire public
revalidée par décodage réel avant stockage, export Excel protégé contre
l'injection de formule, sondage du statut de paiement limité en
fréquence, pyjwt mis à jour.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Formulaire public — paiement en ligne :
- Le bénévole n'est plus compté en base tant qu'un paiement en ligne choisi
n'est pas confirmé (PendingFormSubmission, promu en Volunteer à la
confirmation, webhook ou vérification en direct).
- Type de paiement « mécénat » : étape d'adresse postale avant le paiement,
conservée sur le bénévole pour un reçu fiscal (cerfa) ultérieur.
- Page de confirmation au retour de SumUp, avec relance possible après
échec/expiration sans ressaisir le formulaire.
- « Je paierai sur place » remplace « je ne participe pas » ; tous les
champs activés par la coordination sont désormais obligatoires.
- La fiche bénévole n'affiche plus l'état des cases à cocher, seulement le
résultat réel du paiement (libellé, montant, statut).
Emails :
- Email de confirmation d'inscription à la création du bénévole (tient lieu
de reçu de paiement quand une cotisation est payée en ligne).
- Mise en forme HTML au thème de l'application (logo de l'édition en
en-tête) pour tous les emails envoyés, texte brut conservé en repli.
Export Excel des bénévoles (COORDINATION) : coordonnées, vœux résolus en
noms lisibles, cotisation et mécénat.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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