Commit Graph
91 Commits
Author SHA1 Message Date
AdamThomas 0be5daa89c fix: update ci 2026-09-29 18:31:08 +02:00
AdamThomasandClaude Sonnet 5 26a10cd595 Ajustements manuels : textes du formulaire, opacité des en-têtes de grille
ci-cd / backend-test (push) Failing after 41m59s
ci-cd / build-backend (push) Skipped
ci-cd / frontend-test (push) Failing after 45m57s
ci-cd / build-frontend (push) Skipped
ci-cd / deploy (push) Skipped
Formulaire public : libellés retravaillés (photo, disponibilité, cotisation),
suppression du libellé RGPD affiché sur le bouton de suppression d'un
bénévole. Grilles (besoins, vue bénévoles) : en-têtes « Affectés/idéal » et
« Nom » en pleine opacité ; z-index de l'infobulle au survol d'un créneau
relevé (z-[100]). .gitignore : ignore le fichier TODO personnel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 20:23:49 +02:00
AdamThomasandClaude Sonnet 5 5f7718adfd Grille des besoins : corrige le chevauchement de la ligne d'en-tête
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>
2026-09-28 19:34:34 +02:00
AdamThomasandClaude Sonnet 5 8b9c0ac498 Corrige la grille des besoins : postes manquants au-delà du premier
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>
2026-09-28 19:04:38 +02:00
AdamThomasandClaude Sonnet 5 5f80f167c8 Formulaire public : encart de paiement de la cotisation via Yapla
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>
2026-09-23 21:10:38 +02:00
AdamThomasandClaude Sonnet 5 8f8350aa1b Feuilles de poste : ajouter la programmation du jour (horaires artistes)
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>
2026-09-23 21:01:48 +02:00
AdamThomasandClaude Sonnet 5 957a004623 Formulaire public : logo et texte d'introduction personnalisables
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>
2026-09-23 20:46:52 +02:00
AdamThomasandClaude Sonnet 5 48d3deec65 Formulaire public : photo recadrée en carré, aperçu agrandi
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>
2026-09-23 20:26:01 +02:00
AdamThomasandClaude Sonnet 5 f6fbfaf82a Feuilles de poste : ajouter la photo des bénévoles affectés
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>
2026-09-22 20:06:51 +02:00
AdamThomasandClaude Sonnet 5 15109acfe5 Créneaux à la main + grille des besoins en axe des temps (30 min)
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>
2026-09-22 18:44:20 +02:00
AdamThomasandClaude Sonnet 5 f1d88302a4 Feuilles de poste : ajouter le référent en en-tête
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>
2026-09-22 18:21:26 +02:00
AdamThomasandClaude Sonnet 5 2a2a0c23ca Formulaire public : phrases des cases à cocher plus chaleureuses
« Case « je suis disponible »" / « Case « je veux bien payer une
cotisation »" étaient en fait le libellé de configuration (écran
coordination) réutilisé tel quel comme texte de la case à cocher côté
formulaire public — d'où le ton administratif. Clés dédiées
(publicForm.field.*) avec une formulation à la première personne, plus
adaptée à quelqu'un qui remplit le formulaire.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:16:49 +02:00
AdamThomasandClaude Sonnet 5 40127a2252 Étape 22 : feuilles de poste (PDF), complément des badges
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>
2026-09-21 21:27:01 +02:00
AdamThomasandClaude Sonnet 5 111d1e5c4b Afficher la photo du bénévole sur son écran et sur l'écran référent
Photo transmise via le formulaire public (Volunteer.photo) : nouveau
composant partagé VolunteerAvatar (photo si disponible, sinon repli sur
le carré d'initiales existant, même gabarit dans les deux cas).

- Écran bénévole (VolunteerHomeView) : photo dans le bandeau d'accueil,
  à côté du message de bienvenue.
- Écran référent (MobileLeadPage) : photo devant le nom de chaque membre
  de l'équipe, à la place du carré d'initiales.

GET .../volunteers/{id}/photo n'était accessible qu'à COORDINATION/
RÉFÉRENT ; ouvert aussi à l'intéressé lui-même (même garde que
get_volunteer), sinon l'écran bénévole ne pouvait jamais charger sa
propre photo.

Vérifié : pytest (274 passés, dont le nouveau test d'accès à sa propre
photo), ruff, tsc --noEmit, eslint, vite build, smoke — tous verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 21:10:04 +02:00
AdamThomasandClaude Sonnet 5 82088f3b0c Étape 21 : formulaire public d'inscription des bénévoles
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>
2026-09-21 20:24:32 +02:00
AdamThomasandClaude Sonnet 5 55c5d539a8 Badges : permettre d'agrandir le badge proportionnellement à l'image
Le format déduit de l'image importée (300 dpi supposés, étape
précédente) donne un badge minuscule pour une image à résolution web —
ajout d'un contrôle « largeur du badge (mm) » dans l'écran de
personnalisation : la hauteur suit proportionnellement (ratio de
l'image). PUT /editions/{id}/badges/template/size écrase la taille
déduite de l'image ; la réimporter la recalcule à nouveau.

Vérifié : pytest (250 passés), ruff, tsc --noEmit, eslint, vite build,
smoke — tous verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 20:51:01 +02:00
AdamThomasandClaude Sonnet 5 bc165757ca Badges : corrections de lint (E501) et couverture du smoke test
Lignes trop longues détectées par ruff (badges.py, tests) ; ajout de
/badges au smoke test frontend (rendu SSR de chaque écran protégé).

Vérifié dans ce commit : pytest (248 passés), ruff check, alembic
check, tsc --noEmit, eslint, vite build, smoke — tous verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 20:37:24 +02:00
AdamThomasandClaude Sonnet 5 579ea5d9db Badges : format dérivé de l'image importée, postes avec horaire et référent
- 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>
2026-09-20 20:35:11 +02:00
AdamThomasandClaude Sonnet 5 b7fc80a7dc Étape 20 : planches de badges bénévoles (PDF)
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>
2026-09-20 20:05:03 +02:00
AdamThomasandClaude Sonnet 5 4bcdafca96 Grille : ordonner les créneaux de nuit après minuit selon la vraie journée
Un créneau type commençant avant la frontière du jour (le plus petit
`order`, ex. « Nuit » 00 h–03 h après « Soirée » 19 h–00 h) était généré
sur la même date calendaire que la soirée elle-même au lieu du lendemain,
et s'affichait donc avant 19 h/22 h/23 h dans la grille des besoins au
lieu d'après 23 h. Corrigé dans la génération des créneaux
(_shiftgen.py, même frontière que validation.engine._day_boundary) et
dans le regroupement/tri de la grille (planning.ts : `dayBoundary`
calculé depuis les créneaux types).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 22:59:17 +02:00
AdamThomas f565feb3bc Déclenche le pipeline
ci-cd / backend-test (push) Successful in 53s
ci-cd / frontend-test (push) Successful in 32s
ci-cd / build-backend (push) Successful in 1m7s
ci-cd / build-frontend (push) Successful in 21s
ci-cd / deploy (push) Successful in 14s
2026-09-16 22:18:51 +02:00
AdamThomas db578fbda2 Déclenche le pipeline
ci-cd / frontend-test (push) Successful in 31s
ci-cd / build-backend (push) Failing after 6s
ci-cd / build-frontend (push) Failing after 6s
ci-cd / deploy (push) Skipped
ci-cd / backend-test (push) Successful in 52s
2026-09-16 22:16:10 +02:00
AdamThomasandClaude Sonnet 5 4a60c9ec34 Jeu de démo : restaurer les 15 postes avec catégories et lieux corrects
ci-cd / build-backend (push) Failing after 7s
ci-cd / build-frontend (push) Failing after 6s
ci-cd / deploy (push) Skipped
ci-cd / backend-test (push) Successful in 52s
ci-cd / frontend-test (push) Successful in 32s
La liste POSITIONS avait été remplacée par 16 postes tous classés BAR
et sans lieu, faisant échouer test_seed.py (assert 16 == 15).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 22:09:20 +02:00
AdamThomas ff2d490c94 Déclenche le pipeline
ci-cd / backend-test (push) Failing after 52s
ci-cd / build-backend (push) Skipped
ci-cd / frontend-test (push) Successful in 32s
ci-cd / build-frontend (push) Failing after 11s
ci-cd / deploy (push) Skipped
2026-09-16 22:05:20 +02:00
AdamThomas 056a1ee7da Déclenche le pipeline
ci-cd / backend-test (push) Failing after 52s
ci-cd / build-backend (push) Skipped
ci-cd / frontend-test (push) Failing after 42s
ci-cd / build-frontend (push) Skipped
ci-cd / deploy (push) Skipped
2026-09-16 21:59:03 +02:00
AdamThomas 5a3e11b77e update ci
ci-cd / backend-test (push) Canceled after 0s
ci-cd / frontend-test (push) Canceled after 0s
ci-cd / build-backend (push) Canceled after 0s
ci-cd / build-frontend (push) Canceled after 0s
ci-cd / deploy (push) Canceled after 0s
2026-09-16 21:49:34 +02:00
AdamThomas ba3ffbebf9 Déclenche le pipeline
ci-cd / backend-test (push) Failing after 13s
ci-cd / build-backend (push) Skipped
ci-cd / frontend-test (push) Failing after 39s
ci-cd / build-frontend (push) Skipped
ci-cd / deploy (push) Skipped
2026-09-16 21:28:04 +02:00
AdamThomas 010cac8adc gitea ci
ci-cd / backend-test (push) Failing after 7m57s
ci-cd / build-backend (push) Skipped
ci-cd / frontend-test (push) Failing after 32s
ci-cd / build-frontend (push) Skipped
ci-cd / deploy (push) Skipped
2026-09-16 21:09:28 +02:00
AdamThomasandClaude Sonnet 5 0ede409841 Limiteur de tentatives sur /auth/login et /auth/magic-link
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
2026-09-16 08:19:27 +02:00
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