PRD — Application de gestion du planning d'entraînement (Club Triathlon)
Statut : cadrage fonctionnel figé, V1 livrée. Ce document reste volontairement agnostique de la stack — les choix techniques et leurs contreparties vivent dans le cadrage technique.
Ce document est la source de vérité produit pour la V1. Il décrit l'état cible sans historique de versions et sans choix d'implémentation. Toute proposition qui s'en écarte doit être discutée explicitement avant codage.
1. Vision et objectifs
1.1 Vision
Remplacer le tableur Google Sheets actuel par une PWA qui offre aux adhérents, coachs et bureau une vue claire et toujours à jour du planning d'entraînement, avec un système d'inscription fiable.
1.2 Objectifs métier
- Réduire la friction : qu'un adhérent sache à tout moment quelles séances ont lieu, où, avec qui.
- Permettre aux coachs d'anticiper la fréquentation et d'éviter le sur-effectif (notamment en piscine).
- Désengorger les groupes WhatsApp des annulations / modifications de dernière minute.
- Garder la maîtrise du contenu : code maintenable par le club, réutilisable par d'autres clubs (AGPL).
1.3 Critères de succès (proxy)
80 % des adhérents actifs consultent l'app ≥ 1×/semaine après 2 mois.
70 % des inscriptions aux séances passent par l'app.
- Diminution mesurable du « est-ce qu'il y a piscine ce soir ? » sur WhatsApp.
1.4 Distribution et licence
- AGPL-3.0, code public,
LICENSE+NOTICE(attributions) à la racine. - Modèle one-instance-per-club. Pas de multi-tenant, jamais. Un autre club déploie sa propre instance. Le code ne contient ni
tenant_idni isolation logique multi-club.
2. Personas et rôles
| Persona | Profil | Besoins principaux |
|---|---|---|
| Adhérent / athlète | Membre licencié, âge variable | Voir son planning, s'inscrire/désinscrire, recevoir notifs |
| Parent garant | Représentant légal d'un athlète mineur | Recevoir les notifs et agir au nom de son ou ses enfants |
| Coach | Encadrant d'un ou plusieurs groupes | Créer/modifier les séances, s'inscrire comme encadrant, déclarer ses qualifications |
| Bureau / admin | Élu·e du club | Gérer comptes, paramètres club, stats |
Rôles cumulables globalement : un même User peut avoir athlete + coach + admin. Pas de hiérarchie.
Exclusivité par séance : sur une séance donnée, un utilisateur ne peut pas être à la fois dans coaches[] et dans les inscriptions athlète. Voir §4.11 pour la mécanique de bascule.
Parent garant ≠ rôle : c'est une relation vers un ou plusieurs User enfants, à distinguer de roles[]. Un parent peut cumuler n'importe quel rôle propre. Capacité, quota et préférences notif sont calculés indépendamment pour parent et enfant.
Périmètre exclu V1 : invités / non-adhérents (pas d'accès séance d'essai).
3. Périmètre V1 et hors-V1
3.1 Dans la V1
- Authentification 3 méthodes (email/MDP, magic link, Google).
- Comptes mineurs en 3 phases avec parent garant (P1 → P2 → P3).
- Gestion des adhérents : import CSV initial + ajout one-shot en cours de saison.
- Suppression de compte avec tampon minimum de 7 jours.
- Bascule de saison par désactivation manuelle de l'accès athlète.
- Catalogues paramétrables (catégories d'âge, disciplines, types d'épreuve, tags de quota, qualifications, lieux), seedés au déploiement.
- Catégories d'âge multi-rattachement (M:N).
- Planning hebdomadaire avec 3
kinddeSession:training,competition,club_event. - Générateur de séances récurrentes (
SessionTemplate, admin uniquement). - Suppression définitive d'une séance annulée, admin uniquement (nettoyage d'un doublon ou d'une saisie erronée).
- Inscription/désinscription libre jusqu'au début, bloquée après.
- Capacité + waitlist (
capacity,quota_exceeded) avec visibilité publique. - Quota fair-share par tag avec 3 mécanismes de promotion (A auto place libérée, B auto silencieuse sur libération propre quota, C déblocage manuel coach).
- Override coach (forçage avec motif optionnel).
- Encadrement coach par inscription (M:N) + qualifications agrégées sur la fiche séance.
- Contenu de séance (texte enrichi WYSIWYG + 1 pièce jointe ≤ 5 Mo).
- Compétitions (intention déclarée, lien organisateur externe).
- Débrief de compétition rédigé par les membres participants (texte enrichi), éditable par l'auteur, archivable/réactivable/éditable par l'admin.
- Réaction « j'aime » sur un débrief, avec notification regroupée à l'auteur (§4.12.5).
- Lien externe vers un album photos (compétitions et événements club), saisi par coach/admin.
- Événements club (AG, soirée, sortie loisirs…).
- Parcours OpenRunner Pro (embed iframe) et/ou GPX (parsing client, ≤ 5 Mo).
- Météo prévisionnelle (cible Open-Meteo) sur séances géocodées dans la fenêtre J-16.
- Flag « j'offre l'apéro » sur les inscrits actifs.
- Notifications push web + email transactionnel UE, matrice de préférences + pause globale ; choix à la sauvegarde d'une séance (annuler / silence / notifier) avec option d'envoi immédiat vs différé.
- Gestion des envois sortants côté bureau : consultation/filtre de la file de notifications, rattrapage (annulation d'envois en attente), envoi manuel immédiat, rejeu des échecs.
- Dashboard stats bureau + export XLSX.
- Paramètres club (identité, fuseau, catalogues, lien d'invitation, bascule de saison).
- Deux journaux séparés :
AuditLog(gouvernance) etActivityLog(opérationnel), accès admin uniquement. - Pages d'information (notes club) éditables par l'admin, avec visibilité par niveau de rôle et épinglage en bannière d'accueil (ajout post-cadrage, cf. §4.19).
- Export des séances vers l'agenda personnel : ajout ponctuel d'une séance et abonnement tenu à jour (ajout post-cadrage, cf. §4.21).
- Allures course : VMA courante du membre, allures par zone, estimation depuis un résultat de course et projection de temps (ajout post-cadrage, cf. §4.22).
3.2 Hors V1 (V2+)
- Cotisations / paiements en ligne.
- Suivi de performance individuel (Strava, courbes de progression, historique des chronos ou des VMA). Une valeur de référence courante sans historique (VMA, §4.22) n'en relève pas : c'est un paramètre d'affichage des allures.
- Inscription collective gérée par le club (dossard, tarif groupé, covoiturage).
- Section invités / séance d'essai.
- App mobile native.
- Intégration FFTri / vérification automatique de licence.
- Pointage présence / no-show formel (en V1 le statut
participatingsur séance passée fait foi). - Compteur de fiabilité athlète.
- Plusieurs tags de quota par séance.
- Récap hebdomadaire email.
- Apple Sign-In.
- Stockage du téléphone et du certificat médical (minimisation RGPD).
- Compteur / classement public d'apéros offerts.
- Débrief sur
training/club_event(V1 = compétition uniquement). - Commentaires / fil de discussion sous un débrief (la réaction « j'aime » est en V1, §4.12.5).
- Hébergement ou intégration des photos dans l'app (V1 = simple lien externe vers un album tiers ; aucune galerie native).
4. Spécifications fonctionnelles
4.1 Authentification et comptes
4.1.1 Méthodes d'auth
- Email + mot de passe : disponible pour tout compte avec email. Reset par lien email à usage unique, TTL 15 min.
- Magic link : méthode d'auth complète à part entière, utilisable seule (passwordless) ou en complément d'un MDP. À chaque connexion, l'adhérent choisit Google / MDP (si défini) / « recevoir un lien ». Sert également de mécanisme « mot de passe oublié ». TTL 15 min, usage unique. Un code à usage unique accompagne le lien, de même durée et de même portée : il répond aux contextes où le lien ne peut pas ouvrir la session attendue (application installée dont le contexte de navigation est distinct de celui du navigateur). Consommer l'un invalide l'autre. Le nombre d'essais du code est borné.
- Google OAuth 2.0 / OIDC : chaque club self-hosté configure son propre client OAuth dans sa Google Cloud Console (à documenter dans le guide d'installation). Données récupérées et stockées : email + name uniquement (pas de photo). Le nom Google n'écrase pas un nom déjà saisi.
Méthodes proposées par le club. Le magic link et Google sont activables par instance (§4.17) : un club qui n'a pas fait la démarche Google Cloud Console, ou qui ne veut pas de connexion sans mot de passe, les retire de l'écran de connexion. Le bouton Google n'est de toute façon proposé que si un client OAuth est effectivement configuré — sans quoi il ne mènerait qu'à une erreur. Email + mot de passe n'est pas désactivable : c'est la voie garantie, ce qui borne le risque de rendre l'application inaccessible. Couper un moyen ne peut jamais priver un compte actif de son dernier accès (§4.1.2) : la coupure est refusée tant que c'est le cas. Un lien déjà envoyé cesse d'être honoré dès la coupure, sans être consommé — il redevient valide si le club réactive la méthode avant son expiration.
4.1.2 Linking multi-méthodes
- Un même email peut être lié à plusieurs méthodes sur un seul compte.
- Liaison automatique sur email vérifié : si un compte inscrit en email/MDP se connecte ensuite via Google avec
email_verified=true, les méthodes sont fusionnées sans double prompt. - Si l'email Google n'est pas vérifié, refus + message expliquant de se connecter avec la méthode initiale puis de lier Google depuis le profil.
- L'utilisateur peut révoquer ses méthodes liées depuis son profil, en conservant toujours au moins une méthode active.
4.1.3 Flow de création
- Admin importe un CSV (nom, prénom, email, catégorie, date de naissance, parent garant éventuel). L'email est facultatif pour les mineurs en phase 1 (cf. §4.2) : pas d'invitation, le
Userenfant est créé sans credential. - Pour les adhérents avec email : invitation par email avec lien d'activation valide 30 jours (durée configurable admin, régénérable). Un renvoi invalide le lien précédent.
- L'adhérent choisit sa méthode d'auth à l'activation. La définition d'un mot de passe est optionnelle.
L'email saisi par l'admin est réputé vérifié. L'adresse vient du dossier de licence et l'admin engage sa responsabilité ; exiger en plus une confirmation par l'adhérent n'apporterait rien, et priverait d'accès tout compte n'ayant pas encore cliqué (le lien magique et Google exigent l'un comme l'autre un email vérifié). Risque assumé : une adresse mal saisie donne à un tiers la prise du compte, sans confirmation. Contreparties : l'adresse est visible à la saisie, l'invitation part à cette adresse, et la création est tracée.
Envoi en masse. Une création unitaire envoie l'invitation immédiatement ; un import de rentrée la met en file (exigence non-fonctionnelle : aucun geste d'administration ne doit dépendre d'un nombre d'envois synchrones). Une action d'administration permet de (ré)inviter les comptes jamais activés, sans jamais solliciter deux fois un adhérent déjà entré ou disposant d'une invitation valide.
Voie complémentaire : la page admin « Ajouter un adhérent » permet la création one-shot en cours de saison (mêmes champs, même invitation).
4.1.4 Bootstrap du premier admin
Au déploiement, un email de bootstrap admin est configuré (mécanisme arrêté au cadrage technique : variable d'env, fichier de config, équivalent). Le premier user s'inscrivant avec cet email reçoit automatiquement le rôle admin. Reproductible pour tout fork OSS.
4.1.5 Édition du profil utilisateur
L'utilisateur édite seul :
- Identité : nom, prénom. L'admin peut également les corriger depuis la fiche adhérent (§4.17.1) — même coexistence que pour l'email (§4.1.3), et seul recours pour un pupille P1, qui n'a pas de compte pour le faire lui-même.
- Préférences de notifications : matrice §4.15 + toggle pause global.
- Mot de passe : changement avec MDP actuel, ou définition initiale si arrivé via magic link / Google sans MDP. Retrait possible également (le compte redevient passwordless), dès lors qu'il reste au moins une méthode active (§4.1.2).
- Méthodes d'auth liées : voir et révoquer.
Un changement de mot de passe déconnecte par défaut les autres appareils (choix décochable) ; une définition initiale n'en déconnecte aucun — ajouter une méthode n'est pas un signal de compromission. Un retrait, comme une réinitialisation par email, les déconnecte sans condition.
L'admin uniquement (depuis la fiche utilisateur §4.17) :
- (Ré)envoi de l'invitation d'activation, et déclenchement d'une réinitialisation de mot de passe. L'admin ne peut ni consulter ni choisir le mot de passe d'un adhérent, et aucun mot de passe provisoire n'est prévu : le secret ne transite que par la boîte mail du titulaire. Détenir le facteur d'authentification d'un tiers rendrait l'usurpation possible et indétectable.
- Email (avec mail de confirmation au nouvel email pour validation).
- Date de naissance (impacte la catégorie principale et la bascule de saison).
- Catégories (rattachements manuels, surclassements).
- Rôles (
athlete/coach/admin). - Qualifications (en complément de la saisie coach).
- Flag
athleteAccessSuspended(cf. §4.4).
4.1.6 Visibilité de l'historique d'inscriptions
- L'athlète voit son propre historique (futures + passées).
- Coach et admin voient l'historique complet de n'importe quel athlète depuis sa fiche.
- Parent garant voit l'historique de ses enfants en phases 1 et 2.
- Pas de visibilité croisée sur l'historique d'un autre athlète (un athlète ne voit pas la chronologie d'un autre). Les listes par séance (cf. §4.9) sont en revanche visibles à tous les membres connectés.
4.2 Cycle de vie des comptes mineurs (P1 → P2 → P3)
Tout athlète mineur (< 18 ans) a un parent garant lié. Consentement RGPD implicite via la création du parent garant. Le statut de parent garant est une relation vers un ou plusieurs User enfants, pas une valeur dans roles[].
Trois phases distinctes. Les transitions sont explicites et tracées en AuditLog ; il n'y a aucune bascule automatique liée à l'âge.
P1 — Jeune enfant sans email
Userenfant existe en base (identité, naissance, catégorie, historique inscriptions) sans credential d'auth,emailnul.- Le parent agit au nom de l'enfant depuis une UI « mes enfants » (consultation planning, inscription/désinscription).
- Notifs uniquement au parent (l'enfant n'a pas de canal propre).
P2 — Adolescent supervisé
- L'enfant a son propre email et son propre compte. Il se connecte et s'inscrit lui-même.
- Lien de tutelle actif. Le parent conserve :
- Réception de toutes les notifs en parallèle (push + email sur ses canaux).
- Inscription / désinscription pour l'enfant.
- Consultation de l'historique complet d'inscriptions de l'enfant.
- Le parent ne peut pas modifier les préférences notif de l'enfant ni imposer un workflow d'approbation.
P3 — Athlète adulte autonome
- Le lien de tutelle est rompu (cf. §4.2.2).
- Identité et historique conservés sans rupture.
- Le parent ne reçoit plus de notifs, ne voit plus l'historique, ne peut plus agir.
- Parent orphelin (plus aucun lien de tutelle, pas lui-même
athlete/coach/admin) : aucune action automatique ; auto-suppression possible via §4.3.
4.2.1 Transition P1 → P2 : activation du compte autonome
Le parent OU l'admin saisit l'email de l'enfant → invitation envoyée (30j) → à l'activation, l'enfant choisit sa méthode d'auth. Lien de tutelle conservé automatiquement. Le routage des notifs bascule du mono-destinataire (parent seul) vers le double (enfant + parent).
Validation d'email — confiance simple V1. Pas d'OTP croisé. L'envoi puis le clic sur le lien valent validation implicite. Mitigation pédagogique côté UI : message « l'email doit appartenir à l'enfant ». Risque résiduel (parent qui saisit son propre email → compte enfant captif) assumé.
4.2.2 Transition P2 → P3 : rupture du lien de tutelle
Manuelle uniquement. Déclencheurs possibles :
- L'enfant lui-même (depuis P2 uniquement — interdit en P1 sinon il serait captif).
- Le parent garant.
- L'admin.
Confirmation forte avant clic : modale avec conséquences et case à cocher d'accusé de réception — la notification part aux deux comptes et ne se dédit pas (cf. motif §4.4).
Effet immédiat : lien supprimé, entrée AuditLog guardianship_severed, notif unique aux deux destinataires pour information, l'enfant passe en P3.
Pas de rappel automatique aux 18 ans civils en V1 (risque RGPD résiduel assumé).
4.3 Suppression de compte
Deux voies convergent vers le même état final (compte supprimé, journaux anonymisés) après un tampon minimum bloquant de 7 jours et une confirmation manuelle admin. Le tampon est un délai minimum, pas maximum : le compte reste « éligible à suppression » indéfiniment tant que l'admin n'a pas cliqué.
Voie 1 — Demande à l'initiative de l'athlète
- Bouton « Supprimer mon compte » dans le profil → notif à l'admin.
- L'admin valide ou refuse avec motif.
- À la validation :
isActive=false,User.deletionRequestedAtposé. Pendant le tampon, données et lien de tutelle conservés ; l'athlète ne peut plus se connecter. - Annulation possible pendant le tampon par l'admin (et par l'athlète qui se rétracte via email à l'admin) →
isActive=true,deletionRequestedAt=null, AuditLogaccount_deletion_cancelled.
Voie 2 — Suppression à l'initiative de l'admin
- Sur la fiche utilisateur, bouton « Supprimer ce compte » avec modale de confirmation initiale (saisie du nom complet + double validation).
- Cas d'usage : athlète parti du club sans demander, doublon, compte de test.
- Même flow tampon 7j + confirmation manuelle.
Convergence à J+7
À partir de now() ≥ deletionRequestedAt + 7j, le bouton « Confirmer la suppression définitive » sur la fiche utilisateur devient cliquable (grisé avant J+7, côté UI ET serveur). Au clic : modale de confirmation forte → suppression effective (credentials + données personnelles) + anonymisation des journaux (cf. §4.18). AuditLog account_deleted avec actorId = admin qui confirme (jamais system).
Pas de job auto de suppression en V1. Trois signaux passifs aident l'admin à identifier les comptes à traiter :
- Bandeau d'alerte douce sur le dashboard admin dès qu'au moins un compte est éligible.
- Compteur en en-tête de la page Adhérents.
- Filtre dédié « Statut suppression » sur la page Adhérents.
Aucune notification active (push, email, in-app) n'est envoyée à J+7.
4.4 Bascule de saison : suspension/réactivation manuelle des athlètes
L'app ne stocke pas les n° de licence FFTri (minimisation RGPD ; l'admin gère sur l'extranet fédéral externe). Le mécanisme est purement applicatif : suspendre l'accès athlète, le réactiver au fur et à mesure de la vérification des licences.
Suspension en masse
Bouton admin « Désactiver tous les athlètes pour la nouvelle saison » (paramètres club §4.17). Effet :
- Périmètre : tous les
Userayantathletedansroles[](inclut les coachs et admins qui sont aussi athlètes). User.athleteAccessSuspended = true.- Inscriptions futures auto-annulées : toutes les
Registrationfutures (participatingouwaitlist, sur les 3kind) basculent encancelled(soft flag). Ces annulations déclenchent les promotions auto mécanisme A (cf. §4.10).ActivityLogregistration_cancelledavecactorId = system. - Capacité d'encadrement / d'administration préservée : le flag est séparé de
isActiveet n'affecte que la capacité d'inscription comme athlète. Un coach reste coach, un admin reste admin. - Confirmation forte avant clic : modale avec compteurs (« Tu vas suspendre N comptes et annuler M inscriptions futures »), case à cocher de double validation, motif optionnel.
AuditLog: 1 entrée globalebulk_athlete_deactivationavec compteur affecté et motif optionnel (pas d'entrées individuelles).- Pas d'email automatique de suspension de masse : geste routinier annuel communiqué hors-app (briefing, mail du président, AG).
- Bannière in-app persistante à la prochaine connexion d'un athlète suspendu : « Ton accès athlète est suspendu pour le démarrage de la saison [N]. Contacte l'admin pour réactivation. Tes inscriptions futures ont été annulées. ». Affichée tant que
athleteAccessSuspended = true.
Suspension individuelle
Pendant de la réactivation ci-dessous, sur la fiche utilisateur : bouton « Suspendre l'accès athlète » (adhérent actif, hors procédure de suppression). Mêmes effets que la suspension en masse mais sur un compte — flag posé, inscriptions futures annulées avec promotions en cascade, motif optionnel consigné, AuditLog. Même confirmation forte : compteur d'inscriptions concernées et case à cocher d'accusé de réception, les athlètes promus depuis la file étant, eux, notifiés.
Réactivation individuelle
Sur la fiche utilisateur, bouton « Réactiver l'accès athlète » :
athleteAccessSuspended = false.- Email transactionnel à l'utilisateur : « Ton accès athlète est réactivé pour la saison [N]. » (les inscriptions précédemment annulées ne sont pas restaurées — l'utilisateur doit ré-effectuer ses choix).
AuditLogaccount_activated.
Périmètre exact du flag athleteAccessSuspended
La suspension affecte uniquement la capacité d'inscription comme athlète.
Reste accessible : connexion, consultation planning (4 vues), contenu de séance, agendas d'événements club, fiches compétitions, listes d'inscrits, parcours/GPX, météo, stats personnelles, historique perso, compteurs quota, édition du profil (identité, préfs notif, MDP, méthodes d'auth liées), demande de suppression de compte, UI parent garant.
Bloqué : inscription self-service aux 3 kind ; inscription par le parent garant si l'enfant est suspendu.
Bypass coach : un coach peut forcer une inscription via override §4.10 sur un athlète suspendu (cas : licence renouvelée, admin pas encore passé à la réactivation). AuditLog override_quota avec motif optionnel.
Notifs métier conservées pendant la suspension (matrice §4.15 + pause globale appliquées normalement). En pratique, comme les Registration futures sont soft-cancelled, seules les notifs « vie du club » (création de compétition, contenu de séance) restent fonctionnellement émises.
4.5 Catégories d'âge
Catalogue paramétrable (admin) : label, borne d'âge min, borne d'âge max (inclusives). Géré depuis les paramètres club (cf. §4.17) : ajout, renommage, édition des bornes, archivage.
Seed au déploiement : référentiel FFTri (mini-poussins, poussins, pupilles, benjamins, minimes, cadets, juniors + deux catégories adultes par défaut : adulte 20–39, master 40+). Entièrement reconfigurable par l'admin.
Pas de chevauchement entre catégories actives (validation à la saisie). Garantit qu'à tout âge correspond une et une seule catégorie principale dérivable.
Multi-catégorie M:N : un athlète peut appartenir à plusieurs catégories. La catégorie principale est dérivée automatiquement (date de naissance + année sportive sept→août). Les autres sont ajoutées manuellement par l'admin (surclassement, double rattachement).
Bascule de saison — action admin manuelle (pas de cron) : La bascule n'est pas automatique : le club se met d'accord avant de l'enclencher. Un bouton admin « Démarrer la nouvelle année sportive » (paramètres club §4.17), avec modale de confirmation affichant les compteurs (« N catégories principales recalculées, M surclassements seront effacés »), déclenche en une transaction :
- Recalcul de la catégorie principale de tous les athlètes (date de naissance + nouvelle année sportive sept→août).
- Reset annuel des surclassements manuels : tous les rattachements
UserCategorynon-principaux sont supprimés. Seule la principale recalculée subsiste. L'admin re-surclasse chaque saison. - Inscriptions futures grandfathered (conservées même si la nouvelle catégorie ne correspond plus à la séance).
AuditLog season_rollover(1 entrée globale,actorId= admin déclencheur).
Rappel passif (pas de notif active) : à partir du 1er septembre, tant que la bascule n'a pas été déclenchée, un bandeau d'alerte douce sur le dashboard admin invite à démarrer la nouvelle année sportive. Aucune action automatique liée à la date. Entre le 1er sept et le clic admin, les athlètes ayant changé de tranche d'âge conservent temporairement leur catégorie principale de l'année écoulée (sans incidence sur les inscriptions, grandfathered).
Athlète sans catégorie active (cas limite) : voit dans son planning toutes les séances actives (fallback ouvert) mais ne peut s'inscrire à aucune — bouton désactivé avec message « Aucune catégorie attribuée à ton compte — contacte l'admin ». L'admin voit ces comptes via le filtre dédié de la page Adhérents.
Archivage (soft delete) : une catégorie référencée par au moins un athlète, séance passée ou future, ne peut pas être supprimée définitivement. L'admin l'archive (disparaît des sélections, conservée sur les références historiques). Suppression dure possible si zéro référence. AuditLog category_archived.
Ciblage par séance : par défaut, une nouvelle séance cible toutes les catégories actives ; le coach peut restreindre. Un athlète voit dans son planning toutes les séances dont le ciblage inclut au moins une de ses catégories.
Pas de groupes de niveau (A/B/C) en V1 — reporté V2.
4.6 Catalogues paramétrables (vue d'ensemble)
Pattern commun : seed au déploiement, renommage rétroactif (référence par ID — l'affichage est mis à jour sur les références historiques), archivage soft delete si la valeur est référencée, suppression dure possible si zéro référence.
| Catalogue | Qui crée | Seed | Cardinalité par Session |
|---|---|---|---|
| Catégories d'âge (§4.5) | Admin uniquement | Référentiel FFTri | M:N |
| Disciplines (§4.6.1) | Coachs au fil de l'eau, admin gère/archive | Natation, Course à pied, Vélo, Enchaînement, PPG, Autre | 1 (training, competition), 0 (club_event) |
| Types d'épreuve (§4.6.2) | Admin uniquement | Triathlon, Duathlon, Aquathlon, Course à pied, Trail, Autre | 1 (competition uniquement), 0 sinon |
| Tags de quota (§4.10) | Admin uniquement | aucun | 0 ou 1 (training uniquement) |
| Qualifications (§4.11.3) | Admin uniquement | BF1-5, BNSSA, MNS, PSC1, PSE1, AFPS | N (M:N via UserQualification) |
| Zones d'allure (§4.22) | Admin uniquement | grille générique (Z1…Z5) | 0 (citées en texte libre dans les consignes) |
| Lieux (§4.13.4) | Coachs au fil de l'eau, admin gère/archive | aucun | 0 ou 1 par Session, ou une adresse ponctuelle géocodée propre à la séance (exclusifs) + précision libre locationText |
4.6.1 Disciplines
- Référencées par
Session.disciplineIdetSessionTemplate.disciplineId— sur lestraininguniquement (§4.7). - Coachs créent au fil de l'eau depuis le sélecteur de séance (raccourci « + Nouvelle discipline »). Admin gère/archive depuis les paramètres club.
- Garde-fou minimum 1 active : l'archivage et la suppression de la dernière discipline active sont bloqués (UI + serveur). Garantit qu'une création de
training(seulkindoùdisciplineest obligatoire, §4.7) reste toujours possible. AuditLog discipline_modifiedà chaque opération.- Le filtre planning « discipline » liste les disciplines actives + les archivées référencées sur la fenêtre affichée.
4.6.2 Types d'épreuve
Référencés par
Session.eventTypeId, uniquement pourkind = competition.Gérés par l'admin uniquement depuis les paramètres club (ajout, renommage, archivage). Les coachs ne créent pas de type d'épreuve au fil de l'eau (geste de paramétrage réservé à l'admin).
Renommage rétroactif par ID : l'affichage est mis à jour sur toutes les compétitions existantes sans rupture de référence.
Archivage soft-delete si au moins une compétition référence le type ; suppression dure possible si zéro référence.
Garde-fou minimum 1 actif : l'archivage et la suppression du dernier type actif sont bloqués (UI + serveur) — même logique que §4.6.1. Le type d'épreuve étant obligatoire à la création d'une
competition, une liste vide rendrait la création impossible.Note : ce garde-fou est décrit par symétrie avec les Disciplines (§4.6.1). Si la politique souhaitée est de tolérer temporairement une liste vide (ex. purge d'un type obsolète avant ajout du remplacement), il peut être assoupli — à arbitrer au cadrage admin/UX.
AuditLog event_type_modifiedà chaque opération.
4.7 Modèle Session (training | competition | club_event)
Modèle unique avec un discriminator kind.
Champs communs
kind(training|competition|club_event).title.disciplineId(FK Discipline ; obligatoire pourtraining, absente descompetitionetclub_event— une compétition se qualifie par soneventTypeId, un événement club par rien). Le sélecteur n'est proposé que surtraining; changer dekinden cours de saisie efface la discipline déjà choisie.startAt,durationMin.locationId?+locationText?(lelocationTextpeut surcharger unLocationchoisi).capacity?(optionnelle).categoryIds[](par défaut : toutes les catégories actives).createdBy(FK User, immuable après création, tracé pour audit).coaches[](M:N, voir §4.11 — interprété « organisateurs » pourclub_event, « accompagnateurs » pourcompetition).sourceTemplateId?(FKSessionTemplateinformative — purement audit, aucune propagation comportementale).cancelledAt?/cancelledBy?(soft flag annulation).
Champs spécifiques training
quotaTagId?(0 ou 1 tag, voir §4.10).contentMarkdown(texte enrichi WYSIWYG, voir §4.12).contentAttachment(1 pièce jointe ≤ 5 Mo, formats PDF / PNG / JPG / WebP).
Champs spécifiques competition
eventTypeId(FK vers le catalogue Types d'épreuve §4.6.2 — ex. Triathlon, Duathlon, Aquathlon, Trail). Obligatoire pour unecompetition.distance(texte libre).externalUrl.photosAlbumUrl?(optionnel — lien externe vers un album photos partagé, voir §4.12.5).registrationOpensAt?(optionnel) : ouverture des inscriptions chez l'organisateur, date avec heure facultative (souvent inconnue). Sur les courses prisées, l'inscription ouvre des mois avant la course et se remplit en quelques jours : la date de la course ne suffit pas. Modifiable à tout moment, au plus tard le jour de la course. Ce n'est qu'une information : l'inscription reste faite par chacun sur le site de l'organisateur (§3.2).- État affiché (fiche, à côté du lien organisateur ; vue Courses du planning — pas sur l'accueil, réservé aux débriefs §4.12.5) : « ouvrent le … », « aujourd'hui », « ouvertes » pendant 3 jours, puis « peut-être complet ». Sans heure, l'ouverture vaut toute la journée. Rien sur une course annulée ou passée.
- Notification à la catégorie ciblée (§4.15.2), 15 min avant l'heure quand elle est connue, sinon à 9 h heure club le jour même. Une seule fois ; replanifiée si la date change, jamais envoyée sur une compétition annulée, ni en retard (une date saisie déjà passée, ou une échéance manquée, ne déclenche rien).
La granularité de distance (S/M/L, Ironman 70.3, semi, sprint…) vit dans
distance, pas dans le type d'épreuve. Le type décrit la nature de la compétition ;distanceen précise le format quantitatif, librement saisi.
Champs spécifiques club_event
agenda(texte enrichi WYSIWYG).externalUrl?(optionnel : visio, formulaire externe, page d'info).photosAlbumUrl?(optionnel — lien externe vers un album photos partagé, voir §4.12.5).
Champs parcours (tous kind, optionnels — voir §4.13)
routeOpenrunnerEmbedUrl?.routeOpenrunnerPublicUrl?.routeOpenrunnerId?(dérivé ducodeopaque).route?— référence à un parcours de la bibliothèque (§4.20). Le GPX et ses métriques ne sont plus portés par la séance : un même parcours sert plusieurs séances, et le supprimer d'une séance ne le détruit pas.
Évolution 2026-08-02. Les champs
routeGpxFile?etrouteStats?portés par la séance sont remplacés par cette référence. Motif : le club refait les mêmes boucles toute l'année, et un GPX par séance imposait de re-déposer le même fichier à chaque fois, sans vue d'ensemble ni possibilité de retrouver un parcours passé. Voir §4.20.
Création
- Coachs et admins peuvent créer une
Sessionstandalone (formulaire individuel) sur les 3kind. createdBy≠ encadrant : la personne qui crée n'est pas nécessairement danscoaches[](voir §4.11.1).
Modification d'une séance avec inscriptions
- Inscriptions conservées (pas de reconfirmation requise).
- Choix coach à la sauvegarde : dialog de confirmation à trois issues dès qu'un champ structurant a changé —
- (a) Annuler les changements : revient à la fiche sans rien sauvegarder.
- (b) Sauvegarder en silence : enregistre sans envoyer de notification.
- (c) Sauvegarder et notifier (push + email aux inscrits) — défaut suggéré. Champs structurants déclenchant le dialog :
startAt,durationMin,locationId/locationText,capacity,quotaTagId,categoryIds, annulation.
- Option « envoi prioritaire » (case à cocher sous (c)) : envoie la notification immédiatement au lieu de la mettre en file pour l'envoi différé par lots (cf. §4.15.1). Décochée par défaut.
- Notif « ajout/modification de contenu » (texte, parcours, météo) : sauvegarde silencieuse par défaut (compléments silencieux), avec la même possibilité de notifier — économise une notif sur des correctifs mineurs.
Annulation d'une séance
- Qui : n'importe quel coach + admin.
- Quand : tant que le créneau n'est pas terminé (
startAt + durationMin). La borne est la fin et non le début : une séance annulée sur place — orage, gymnase fermé — l'est quelques minutes après l'heure, et les inscrits doivent être prévenus. Passé la fin, la séance a eu lieu : l'annuler l'effacerait rétroactivement des statistiques de fréquentation et notifierait les inscrits d'une annulation sans objet. Pendant du garde-fou de restauration ci-dessous, qui borne au début — entre le début et la fin du créneau, l'annulation est donc définitive, et la confirmation l'annonce. - Confirmation forte avant clic : modale avec conséquences et case à cocher d'accusé de réception chiffrant les inscrits prévenus (cf. motif §4.4) ; le libellé annonce en plus le caractère définitif dès que le créneau a commencé.
- Effet immédiat : statut séance
cancelled, libère le quota de tous lesparticipating(déclenche mécanisme B §4.10), inscriptions passent encancelled(soft flag), notif aux inscrits toujours envoyée — l'annulation n'offre pas le choix de notifier ou non, contrairement à la modification (§ ci-dessus) : l'événement est trop structurant. À ne pas confondre avec la case d'accusé de réception ci-dessus, qui arme le bouton sans jamais conditionner l'envoi,AuditLog cancel_session.
Réversibilité — restauration d'une séance annulée
Tant que startAt n'est pas dépassé, un coach ou admin peut réactiver la séance depuis sa fiche :
- Restauration naïve : toutes les inscriptions soft-cancelled à l'annulation sont restaurées en
participating(resp.waitlistavec leur raison initiale). - Conflit quota : si entre-temps un athlète restauré a profité du quota libéré ou s'est ré-arbitré ailleurs et que la restauration le ferait dépasser son quota, override silencieux :
AuditLog override_quotaautomatique (actorId = systemou coach, motif « restauration après annulation »). Le quota est secondaire face à la priorité de remettre les inscrits initiaux. - Notif aux athlètes restaurés : push + email « la séance [X] a été réactivée, ton inscription est rétablie ».
- Garde-fou : restauration impossible une fois
startAtdépassé. Au-delà, l'annulation est définitive.
Suppression définitive
Pendant de l'annulation, pour le cas qu'elle ne couvre pas : la séance qui n'aurait jamais dû exister — doublon, erreur de saisie. L'annulation la laisserait affichée « annulée » indéfiniment, ce qui est faux : rien n'a été annulé, la ligne est de trop.
- Qui : admin uniquement. Seul geste de l'application qui détruise pour de bon.
- Quand : uniquement sur une séance déjà annulée. L'ordre n'est pas une formalité — c'est l'annulation qui prévient les inscrits, libère les quotas et gare les apéros. La suppression n'a donc personne à notifier : la séance a déjà disparu des plannings.
- Garde-fou : refusée tant qu'un débrief est rattaché. Un débrief est du texte écrit par un membre ; l'effacer sans que personne l'ait lu serait une perte, là où une inscription sur un doublon n'en est pas une. Même doctrine que les parcours (§4.20), dont l'usage interdit la suppression dure. L'écran explique le blocage au lieu de le laisser découvrir au clic.
- Confirmation forte : dialog avec conséquences chiffrées et case à cocher d'accusé de réception, comme l'annulation — mais pour l'autre raison. Ici rien n'est envoyé : c'est l'irréversibilité seule qui exige l'accusé. Refus gardé côté serveur.
- Effet immédiat : la séance est effacée ; inscriptions, encadrement, catégories ciblées et flags apéro partent avec elle.
AuditLog delete_session. - Ce qui survit : les journaux (audit et activité) sont conservés, détachés de la séance disparue. La trace de la suppression porte le titre et le créneau en clair — sans quoi elle ne désignerait plus rien.
- Notifications : celles déjà reçues restent lisibles dans les cloches — titre et créneau y sont figés — mais ne renvoient plus vers la séance. Une notification a réellement été envoyée : l'effacer réécrirait l'histoire, la laisser pointer le vide mènerait à une page morte. Celles encore en file partent quand même, en renvoyant vers le planning : quelqu'un qui vient d'apprendre l'annulation doit la recevoir. Le dialog chiffre les deux séparément, la cloche et la file n'étant ni le même volume ni le même effet.
- Pas de restauration. Ni corbeille, ni délai de grâce — contrairement à la suppression de compte (§4.3), dont le tampon de 7 jours protège une personne et ses données. Une séance n'a pas cet enjeu, et le tampon rendrait le nettoyage inutilisable.
Édition en lot
Pas en V1. Pour propager un changement sur plusieurs séances futures, édition séance par séance, ou annulation en lot + regénération depuis un SessionTemplate mis à jour.
Inscription anticipée
Pas de limite haute : un athlète peut s'inscrire à n'importe quelle distance dans le futur. L'horizon est borné de fait par la plage de génération des SessionTemplate.
Vue « Courses » du planning
Une compétition se prépare sur plusieurs mois, là où le planning se lit à la semaine. Le sélecteur de vues du planning porte donc un quatrième segment, Courses (Jour / Semaine / Mois / Courses), sur mobile comme sur desktop. C'est une liste, pas une grille : ni flèches précédent / suivant ni titre de période, et les filtres par type et par discipline n'y ont pas cours.
- À venir : toutes les
competitionà partir d'aujourd'hui (une course du jour y reste jusqu'à minuit, heure club), sans borne haute — en juin, on prépare les courses de septembre. Tri par date croissante. - Passées : celles de la saison en cours seulement (§4.4), de la plus récente à la plus ancienne. Chaque ligne indique le nombre de débriefs actifs et la présence d'un album photos ; elle ouvre directement l'onglet Débriefs quand il y en a. C'est l'archive des débriefs ; la nouveauté reste mise en avant sur l'accueil (§4.12.5).
- Chaque ligne : nom, date, type d'épreuve, distance et lieu ; pour une course à venir, l'état des inscriptions chez l'organisateur quand la date d'ouverture est connue ; le nombre de membres du club qui y participent et leurs noms selon la convention §4.9.4 ; l'intention du sujet consulté (« Tu participes » / « Tu y étais », liste d'attente).
- Les compétitions annulées restent visibles et marquées comme telles.
- Mêmes règles que le reste du planning pour le filtrage par catégorie (§4.5), la vue parent (§4.2) et « Mes inscriptions ».
4.8 Générateur de séances récurrentes (SessionTemplate)
Pas de récurrence iCal (RRULE/EXDATE) en V1. Modèle alternatif : générateur persisté qui produit des Session indépendantes en base.
Périmètre d'accès : admin uniquement pour la création / édition / archivage d'un SessionTemplate. Justification : un template produit N Session à l'enregistrement (impact potentiel sur des dizaines de séances) — geste de pilotage de saison. Les coachs continuent de créer librement des Session standalone.
Champs du template
label, kind (essentiellement training en V1), disciplineId (mêmes règles que sur la séance, §4.7 : training uniquement), dayOfWeek (1..7), startTimeOfDay, durationMin, locationId?/locationText?, capacity?, quotaTagId?, categoryIds[], defaultCoachIds[] (pré-affectation), generationStartDate, generationEndDate (obligatoire — pas de récurrence infinie), createdBy (= admin), status (active / archived).
Comportement à l'enregistrement
- Le système génère immédiatement N
Sessionindépendantes. - Chaque
Sessioncréée a sa propre identité, son proprecreatedBy(= admin), ses propres champs. Session.coaches[]est initialisé àdefaultCoachIds[], modifiable ensuite séance par séance.- Aucun lien retour comportemental :
Session.sourceTemplateId?purement audit, jamais utilisé pour propager une modif. - Notif récapitulative unique par coach présent dans
defaultCoachIds[](push + email) : « [admin] t'a affecté comme coach sur N séances [titre] du [date début] au [date fin] ». Évite le spam (ex. 30 séances × 3 coachs = 3 notifs, pas 90). N entréesActivityLog coach_registeredsont quand même créées (traçabilité fine).
Réutilisation
Le template est conservé en base, relançable (nouvelle saison, prolongation) — la regénération crée de nouvelles Session sans écraser les précédentes.
- L'édition d'un template ne génère rien : elle met à jour le template seul, aucune
Sessionn'est créée ni modifiée (corollaire de l'absence de lien retour comportemental ci-dessus).generationStartDate/generationEndDatey valent plage de référence des prochaines générations. - La relance ne crée que les occurrences absentes de la plage demandée : rejouer une plage déjà générée ne produit aucune
Session, et le nombre annoncé avant l'action est celui des séances réellement manquantes.
4.9 Inscriptions, capacité, liste d'attente
4.9.1 Règles uniformes sur les 3 kind
- Inscription et désinscription libres jusqu'au début de la séance. Pas de délai d'ouverture, pas de délai limite.
- Inscription / désinscription après le début bloquées sur les 3
kind. Préserve le proxy de présence du quota fair-share côtétraining(cf. §4.10) et fige la liste des « déclarés présents » côtécompetition/club_event. - Désinscription = soft flag (
status = 'cancelled'). - Pour les compétitions et les événements club, l'inscription est une intention déclarée (pas une inscription officielle au sens de l'organisateur, qui se fait via
externalUrlou hors-app).
4.9.2 Statut unifié et sémantique
Registration.status ∈ { participating, waitlist, cancelled } — uniforme sur les 3 kind. La sémantique d'engagement ferme (place réservée par le club pour un training, soumis à capacité + quota + blocage post-début) vs intention déclarée (competition / club_event) est portée par Session.kind + l'absence de quotaTagId et la capacité optionnelle, pas par la valeur de status.
Les libellés UI/notifs côté athlète restent en français naturel (« ton inscription est confirmée », « tu participes à la compétition X ») — indépendants de la valeur enum interne.
4.9.3 Capacité, waitlists
- Capacité atteinte : bouton « rejoindre la liste d'attente »,
status = waitlist,waitlistReason = capacity. - Quota dépassé (
trainingavecquotaTagId) : bandeau d'avertissement « Tu as déjà N séance(s) [T] cette semaine. Tu seras placé en liste d'attente. Continuer ? » → si confirmé,status = waitlist,waitlistReason = quota_exceeded. - Tri unique des deux waitlists : par
timestamp d'entrée en waitlist ASC(FIFO pur). Pas d'autre critère en V1. - Position waitlist + raison visibles à l'athlète concerné, à jour à chaque chargement / pull-to-refresh (cf. §6).
4.9.4 Visibilité des listes par séance
Pour toute Session des 3 kind, tout membre connecté du club voit :
- La liste
participating(les inscrits actifs). - Les deux waitlists
capacityetquota_exceededaffichées séparément dans leur ordre FIFO timestamp visible (le rang individuel est donc dérivable par tous).
Pas d'accès public anonyme (authentification club requise).
Niveau d'affichage :
- Entre athlètes : prénom + initiale. Extension automatique de l'initiale en cas d'homonymie sur une même séance (« Marc Sa. » / « Marc Sm. ») — l'algorithme étend à la longueur minimale nécessaire pour différencier.
- Coachs encadrants (sur la fiche séance, partout) : prénom + nom complet (cf. §4.11.4).
- Coachs / admins consultant : prénom + nom complet partout.
Exclusion : le compteur « N séances faites cette semaine » par inscrit reste vue coach/admin uniquement (info de pilotage du fair-share — éviter la stigmatisation). Le badge d'override coach (cf. §4.10) reste également limité aux coachs/admins.
4.9.5 Sérialisation atomique (exigence non-fonctionnelle)
Sur la dernière place disponible, deux athlètes qui cliquent « s'inscrire » en quasi-simultanéité sont traités séquentiellement sur la base de leur timestamp serveur d'arrivée : le premier obtient participating, le second bascule en waitlist capacity. Pas de double-acceptation, jamais. Implémentation libre (verrou pessimiste, transaction sérialisable, file persistante, compare-and-swap…) ; garantir par tests E2E.
4.9.6 Conflit horaire
Si l'athlète s'inscrit à une séance qui chevauche une autre où il est déjà inscrit : alerte + confirmation utilisateur, pas bloquant.
4.9.7 Inscription par un coach (cas légitime, athlète peu à l'aise avec l'app)
- Athlète actif, sous quota : inscription normale en
participating(ouwaitlist capacitysi plein). Notif à l'athlète.ActivityLog inscription_by_coach. - Athlète actif, quota déjà atteint : dialog explicite au coach « Cet athlète a déjà N/N séance(s) [T] cette semaine. Choisis : (a) Placer en
waitlist quota_exceeded(cohérent fair-share) / (b) Forcer (override §4.10) ». Le coach décide. Si override,AuditLog override_quotaavec motif optionnel. - Athlète avec
athleteAccessSuspended = true: self-service bloqué, mais bypass coach via override §4.10 possible.
4.10 Quota fair-share et mécanismes A/B/C
Mécanisme central pour éviter qu'un même athlète monopolise les places sur des séances à capacité fortement limitée (ex. natation).
4.10.1 Concepts
- Tag de quota : étiquette configurable attachée à une
Session(ex.piscine,home-trainer-collectif,endurance). 0 ou 1 tag par séance en V1. Universel : pas de restriction de discipline portée par le tag. maxPerWeek: nombre d'inscriptionsparticipatingautorisées par tag, par athlète, par semaine (entier ≥ 1, paramétrable, défaut 1).- Semaine : lundi 00:00 → dimanche 23:59 dans le fuseau du club (paramétrable §4.17).
- Compteur hebdo unique par tag, additionnant les inscriptions toutes disciplines confondues (ex. tag
endurancemaxPerWeek=2plafonne à 2 séances « endurance » dans la semaine, qu'elles soient course, vélo ou natation).
Exigence non-fonctionnelle : l'évaluation du quota est en temps quasi-constant à l'inscription, indépendamment du nombre d'inscriptions historiques de l'athlète.
4.10.2 Modèle hybride
- Check d'inscription : compte les inscriptions
participatingde la semaine (futures + passées) pour décider d'accepter ou de placer en waitlist. - Priorité de promotion depuis waitlist : compte les inscriptions
participatingsur séances passées uniquement (proxy de présence en l'absence de pointage V1).
4.10.3 Algorithme à l'inscription
soit T = tag de quota de la séance (s'il y en a un, sinon null)
soit N = maxPerWeek(T)
soit C = nb d'inscriptions 'participating' de l'athlète sur les séances
portant le tag T de la semaine courante (futures + passées)
si T est null :
appliquer la logique standard (capacité + waitlist FIFO)
fin
si C < N :
si capacité disponible → status = 'participating'
sinon → status = 'waitlist', reason = 'capacity'
sinon (C >= N) :
si le quota de la séance est débloqué (mécanisme C) :
si capacité disponible → status = 'participating' (sans bandeau,
avec confirmation si une autre place est rendue, §4.10.4 C)
sinon → status = 'waitlist', reason = 'quota_exceeded'
sinon :
afficher bandeau d'avertissement à l'athlète
si confirmé → status = 'waitlist', reason = 'quota_exceeded'
sinon → annule l'inscriptionSur une séance débloquée et pleine, l'athlète hors quota rejoint la file quota_exceeded, pas la file capacity : entre hors-quota, l'ordre d'arrivée est conservé, et celui qui attendait avant le déblocage n'est pas doublé par celui qui s'inscrit après.
4.10.4 Trois mécanismes de promotion
Mécanisme A — Auto-promotion sur place libérée (désistement d'autrui, annulation…)
À chaque place libérée :
si la file 'capacity' de la séance est non vide :
promouvoir le 1er FIFO timestamp → status = 'participating'
notifier l'athlète promu (push + email)
répéter tant qu'il reste des places ET des athlètes en 'capacity'
sinon, si le quota de la séance est débloqué (mécanisme C) :
promouvoir le 1er FIFO timestamp de la file 'quota_exceeded'
(même notification, même répétition)
sinon :
la file 'quota_exceeded' n'est PAS touchée par ce mécanisme.« Place libérée » couvre le désistement, l'annulation d'une inscription et la hausse de capacité d'une séance.
Mécanisme B — Auto-promotion silencieuse sur libération de son propre quota
Si l'athlète A se désinscrit d'une séance du tag T cette semaine (ou si la séance est annulée), pour chacune de ses inscriptions waitlist quota_exceeded sur une autre séance du même tag dans la même semaine :
- Cas (a) — capacité disponible : bascule auto en
participating. Notif push à A : « ton inscription à [X] est confirmée ».ActivityLog auto_promoted_self_quotaavecresultingStatus = 'participating'. - Cas (b) — capacité pleine : migration de
waitlist quota_exceeded→waitlist capacity. Le quota n'est plus saturé pour A, c'est la capacité qui bloque. A conserve sonregisteredAtinitial (FIFO timestamp ASC pur) et devient éligible à une promotion mécanisme A. Notif push à A : « ton quota est libéré sur [X] mais la capacité est pleine — tu es désormais en liste d'attente pour place libérée (position N) ».ActivityLog auto_promoted_self_quotaavecresultingStatus = 'waitlist_capacity'.
Le mécanisme B ne dépend pas du timestamp FIFO de la file quota_exceeded pour décider si A est promu — c'est A qui libère son propre quota. Les autres athlètes en quota_exceeded derrière A restent en attente d'un déblocage coach.
Mécanisme C — Déblocage du quota par le coach
Action « Débloquer le quota » sur la séance. Le déblocage est un état de la séance, actif jusqu'à ce que le coach le referme ou que la séance commence :
- Précondition : séance future, non annulée, portant un tag de quota. Possible même file
quota_exceededvide (ouvrir la séance la veille, avant que quiconque n'attende). - Au clic, le système promeut autant d'athlètes de
quota_exceededque de places restantes, en FIFO timestamp, une fois la filecapacityservie (elle passe devant). - Tant que le quota est débloqué : l'inscription hors quota ne bloque plus (§4.10.3) et le mécanisme A pioche aussi dans
quota_exceeded. - Champ « motif » optionnel partagé pour le geste et le batch.
AuditLog: une entréeaction = 'quota_release'pour le geste (motif), puis N entrées individuellesaction = 'promote_quota_exceeded', une par athlète promu (actorId= coach,targetId= athlète promu,sessionId,motifpartagé). Les promotions automatiques qui suivent (mécanisme A) gardent leurActivityLogavec acteur système.- Notif push + email aux athlètes promus.
- Réévaluation de l'autre place (#103). Obtenir une place hors quota — promotion au clic, mécanisme A piochant dans
quota_exceeded, ou inscription directe sur séance débloquée — ne doit pas faire tenir deux places pour un quota d'une, au détriment de quelqu'un qui attend ailleurs. Si l'athlète dépasse alors son quota, sa dernière place obtenue sur une autre séance future du même tag dans la semaine est réévaluée comme s'il s'y inscrivait maintenant :- quota de cette séance débloqué → il la garde ;
- sinon → elle repasse en
waitlist quota_exceeded,registeredAtinchangé (c'était son premier choix, dans son quota : il passe devant ceux arrivés après lui déjà hors quota), et la place libérée part au mécanisme A. - Jamais pour une place obtenue par override (§4.10.5), ni pour une séance commencée. Un override ne déclenche pas non plus de réévaluation.
- L'ordre des jours n'importe plus : mardi obtenu hors quota rend vendredi, et vendredi débloqué à son tour y repromeut l'athlète normalement. Pas de boucle : la séance rétrogradée n'étant pas débloquée, A n'y pioche pas dans
quota_exceeded. - Communication : le dialog de quota (§4.10.3) nomme la séance qui serait rendue ; sur séance débloquée, l'inscription demande confirmation en la nommant ; la notification de promotion le dit en tête de son corps (« Tu es inscrit·e sur …, mais tu repasses en liste d'attente sur … »).
ActivityLog quota_requeued(acteur système,resultingStatus = waitlist_quota_exceeded).
- Refermer le quota : les athlètes déjà promus restent inscrits ; seules les inscriptions suivantes retrouvent la règle normale.
AuditLog action = 'quota_close'. - Piste hors V1 : déblocage automatique X heures avant le début.
Non-conflit A/B/C : A pioche dans
capacitysur place libérée, puis dansquota_exceededsi le quota de la séance est débloqué ; B traite le propre quota libéré par l'athlète sur sa propre filequota_exceeded; C est l'état posé par le coach, qui étend A et §4.10.3 jusqu'à la séance. La filecapacitypasse toujours devant la filequota_exceeded.
4.10.5 Override coach
Le coach peut forcer une inscription participating, en outrepassant quota et/ou capacité.
- Distinct du mécanisme C : l'override sert à inscrire un athlète qui n'est pas en file OU à dépasser strictement la capacité max ; pas la voie normale pour piocher dans
quota_exceeded. - Pas de limite logicielle : ni nombre max d'overrides par séance, ni plafond de débordement au-dessus de
capacity. La capacité max devient indicative. La discipline humaine et la traçabilité priment. UX : la fiche séance affiche « N inscrits (capacité M, surcapacité +K) » dès dépassement. - Cas d'usage :
- Inscrire un athlète qui n'est pas en file.
- Dépasser la capacité max stricte.
- Inscrire un athlète suspendu (
athleteAccessSuspended=true). - Inscrire un athlète qui dépasse son quota hebdo (via le dialog §4.9.7).
- Compte dans le compteur de quota de l'athlète (pas un bonus hors quota).
AuditLog action = 'override_quota', motif optionnel.- Badge + motif visibles à coachs + admins (transparence interne). Athlètes ne voient pas le badge.
4.10.6 Cycle de vie du quota
- Désistement avant le début (autorisé librement, cf. §4.9) : libère la place ET libère 1 unité de quota → déclenche le mécanisme B.
- Désistement après le début : bloqué côté UI.
- Annulation de la séance par le coach : libère le quota de tous les
participating. - Place obtenue hors quota : réévalue l'autre place de la semaine (§4.10.4 C) ; se désinscrire ensuite de la séance obtenue déclenche le mécanisme B vers celle rendue.
- Pas de cron de reset : le quota se recalcule naturellement sur la semaine courante.
4.10.7 UX et transparence
- Bandeau d'avertissement systématique avant validation d'une inscription qui partira en waitlist pour cause de quota.
- Compteurs visibles dans le profil athlète, regroupés par tag (et non par discipline) : « cette semaine : 1/1 piscine, 0/2 home-trainer-collectif, 2/3 endurance ». Les séances sans tag n'apparaissent dans aucun compteur.
- Vue coach : sur la liste des inscrits/waitlist, indication visuelle du statut (
quota_exceededvscapacity) et du nb de séances déjà faites cette semaine. - Vue coach (gestion de séance) : bouton « Débloquer le quota » sur toute séance future, non annulée et taguée. Au clic, modale de confirmation : ce que le déblocage change jusqu'à la séance, liste nominative des athlètes promus, champ « motif » optionnel partagé ; dès qu'au moins un athlète est promu, accusé de réception chiffré (« Je comprends que N athlètes seront prévenu·e·s ») avant d'armer le bouton. Une fois débloqué, le bouton devient « Refermer le quota » (simple confirmation).
- Chip « Quota débloqué » sur la fiche séance, visible de tous — athlètes compris, pour qu'ils sachent pouvoir s'inscrire.
4.10.8 Cas limites
- Semaine N→N+1 (dimanche soir / lundi matin) : compteurs distincts.
- Séance à cheval entre deux semaines : appartient à la semaine de sa date de début.
- Changement du tag de quota d'une séance (ou retrait du tag) : le déblocage (§4.10.4 C) est remis à zéro. Les athlètes déjà promus restent inscrits.
- Restauration d'une séance annulée : le déblocage est remis à zéro ; le coach le rouvre s'il le veut.
- Modification du
maxPerWeekd'un tag : inscriptions existantes pas remises en cause, seule la prochaine évaluation utilise le nouveau quota. - Ajout d'un tag a posteriori sur une séance avec inscrits : recalcul automatique ; les inscrits qui dépassent le nouveau quota sont remis en
waitlist quota_exceededavec notif. - Changement de discipline d'une séance déjà taguée : aucun effet de bord (tag universel).
- Même tag sur disciplines différentes : compteur additionne toutes disciplines confondues. Pas de garde-fou logiciel sur la cohérence du tagging — la mitigation repose sur des libellés clairs côté admin.
4.11 Encadrement : inscription coach et qualifications
Périmètre :
kind = 'training'uniquement. Pourcompetitionetclub_event,coaches[]est saisie librement par le créateur (« accompagnateurs » / « organisateurs »), sans mécanisme d'inscription, sans bloc qualifications agrégées sur la fiche, sans comptage dans les stats §4.16.
4.11.1 Créateur vs encadrants
Session.createdBy: FK User, audit, immuable après création.Session.coaches[]: liste M:N des coachs inscrits comme encadrants. Détermine : qui apparaît sur la fiche séance, quelles qualifs sont agrégées (§4.11.4), quelles séances comptent dans les stats d'activité (§4.16).- À la création d'une
training, le formulaire propose un multi-sélecteur de coachs encadrants parmi lesUserayant le rôlecoach:- Si le créateur a le rôle
coach, la case « M'inscrire comme coach sur cette séance » est cochée par défaut (décochable). Un admin pur ne se voit pas proposer cette auto-affectation. - Tout coach + tout admin créateur peut ajouter d'autres coachs au sélecteur.
- À l'enregistrement, chaque coach autre que le créateur auto-affecté reçoit une notif immédiate (push + email) et une entrée
ActivityLog coach_registered(actorId= créateur,userId= coach affecté).
- Si le créateur a le rôle
4.11.2 Inscription / désinscription comme coach
Qui peut être inscrit ? Tout User ayant le rôle coach (pas de cloisonnement par catégorie ni discipline — coach global).
Trois voies d'inscription équivalentes :
- Voie 1 — Affectation au formulaire de création / génération : à la création d'une
Sessionstandalone (cf. §4.11.1) ou d'unSessionTemplate(admin uniquement, cf. §4.8). - Voie 2 — Self-inscription post-création : bouton « S'inscrire comme coach » sur la fiche séance (visible aux coachs pas encore dans
coaches[]). - Voie 3 — Inscription d'un autre coach par un tiers post-création : tout coach + tout admin (y compris admin pur) peut inscrire n'importe quel autre coach via le bouton « Inscrire un coach » sur la fiche séance. Pas de workflow d'acceptation : le coach inscrit reçoit une notif immédiate « tu as été inscrit comme coach sur [X] par [Y] » et conserve le droit de se désinscrire librement.
Désinscription :
- Self-désinscription : bouton « Me retirer de l'encadrement » sur la fiche séance, tant que la séance n'a pas commencé.
- Désinscription d'un autre coach par un tiers : tout coach + tout admin peut retirer un autre coach (symétrique à l'inscription tierce). Notif au coach concerné.
- Dernier coach : la désinscription du dernier coach inscrit est autorisée, mais un dialog de confirmation explicite apparaît : « [Coach X] est le seul coach inscrit. La séance se retrouvera sans encadrement après son retrait. Continuer ? ». Responsabilité humaine prime.
Pas d'alerte programmée V1 sur séance sans coach (pas de cron / scheduler en V1). Détection par deux signaux in-context :
- Bandeau d'alerte douce sur la fiche séance (§4.11.4).
- Compteur « séances futures sans coach » dans le dashboard admin (§4.16).
Intervenant extérieur (training uniquement) : certaines séances sont encadrées par un prestataire non adhérent et sans compte — surveillant de baignade, MNS. Session.externalStaffLabel signale qu'il y en a un, sans nommer la personne : ni compte fictif (qui ferait pousser des notifications vers une adresse morte et polluerait pickers et statistiques), ni fiche sur un tiers non adhérent. Une séance qui le porte est considérée encadrée par les deux signaux ci-dessus. Champ non structurant : le renseigner ne notifie pas les inscrits (§4.7). Il se règle au formulaire de séance ou de modèle, et depuis la fiche (bloc Encadrement) par une fenêtre dédiée — distincte de « Inscrire un coach », qui agit au clic et notifie — ouverte à tout coach et tout admin tant que la séance n'a ni commencé ni été annulée.
Droits opérationnels inchangés : l'inscription comme coach ne confère aucun droit supplémentaire par rapport au rôle coach global. Tout coach du club (inscrit ou non sur la séance) peut éditer la séance, déclencher un override, déclencher le mécanisme C. La présence dans coaches[] sert uniquement à : afficher qui anime, agréger les qualifications, alimenter les stats d'activité.
Traçabilité : chaque action génère une entrée ActivityLog (coach_registered ou coach_unregistered, sans motif). actorId = utilisateur qui déclenche, userId = coach concerné — peuvent différer en cas d'inscription/désinscription par un tiers.
Notifs : à chaque action, push + email aux autres coachs déjà inscrits + à l'admin. Si l'action est déclenchée par un tiers, notif également au coach concerné.
4.11.3 Qualifications coach
Catalogue paramétrable (admin). Champs minimaux : label libre, code court optionnel (ex. BNSSA). Seed au déploiement : BF1, BF2, BF3, BF4, BF5, BNSSA, MNS, PSC1, PSE1, AFPS.
Attribution : entité UserQualification(userId, qualificationId, expiresAt?, attributedAt, attributedBy). expiresAt optionnel par attribution (certaines qualifs ont une validité réglementaire — BNSSA 5 ans, PSC1 annuel — d'autres non).
Saisie :
- Par le coach lui-même depuis son profil (page « Mes qualifications »).
- Par l'admin depuis la fiche utilisateur.
- Pas de workflow de validation : confiance.
Badges d'expiration (sur profil et fiche séance) :
expiresAtnul : pas de badge temporel.expiresAtfutur lointain : qualif normale, date en infobulle.expiresAt< 30 jours : badge « expire bientôt » (alerte douce).expiresAtpassé : badge « expirée » (alerte forte). Qualif reste affichée mais visuellement marquée.
Pas de blocage de l'inscription coach sur l'expiration. Information seulement.
Archivage : soft delete si référencée par au moins une UserQualification. Suppression dure possible si zéro référence.
4.11.4 Affichage sur la fiche séance (training)
Bloc « Encadrement » :
- Liste nominative des coachs inscrits (
coaches[]) — prénom + nom complet (visibilité publique à tout membre connecté, contrairement aux athlètes affichés en prénom + initiale, cf. §4.9.4). - Bloc « Qualifications disponibles » : chips agrégés uniques des qualifs des coachs inscrits (déduplication par
qualificationId). Au tap / hover d'un chip : détail des coachs qui portent cette qualif + badge d'expiration éventuel. - Carte « Intervenant extérieur » si
externalStaffLabelest renseigné : le libellé seul, sans avatar nominatif ni chips de qualifications — aucune donnée nominative n'est stockée sur ce prestataire (minimisation RGPD, cohérent avec les exclusions de §3.2). Affichée indépendamment des coachs inscrits : une séance peut légitimement avoir un coach du club et un surveillant externe. - Si
coaches[] = ∅etexternalStaffLabelvide : bandeau d'alerte douce « Pas de coach inscrit pour le moment ». S'efface dès qu'un coach s'inscrit ou qu'un intervenant extérieur est signalé — dans les deux cas la séance est encadrée. Pas de blocage d'inscription athlète.
Sur competition / club_event : liste nominative des coaches[] (« accompagnateurs » / « organisateurs ») — pas de bloc qualifs, pas de bandeau « pas de coach ».
4.11.5 Bascule athlète ↔ coach sur la même séance (4 cas symétriques)
Toute tentative d'inscription dans un rôle pour un utilisateur déjà inscrit dans l'autre rôle sur la même séance déclenche un dialog de confirmation explicite décrivant la bascule + ses conséquences directes + les cascades utilisateur visibles. Aucune bascule implicite, jamais.
- Cas 1 — Self, athlète → coach : un athlète inscrit clique « S'inscrire comme coach » sur la fiche séance. Dialog : « Tu es actuellement inscrit comme athlète sur cette séance. Pour t'inscrire comme coach, ton inscription athlète sera annulée — ta place et ton quota seront libérés. Conséquences possibles : un athlète en
waitlist capacitypourra être promu automatiquement (mécanisme A) ; si tu étais enwaitlist quota_exceededsur une autre séance du même tag cette semaine, ton inscription pourra y basculer enparticipatingautomatiquement (mécanisme B). Continuer ? ». À la validation :Registrationsoft-cancelled + ajout àcoaches[]. - Cas 2 — Self, coach → athlète : un coach inscrit clique « S'inscrire comme athlète ». Dialog : « Tu es actuellement inscrit comme coach sur cette séance. Pour t'inscrire comme athlète, ton inscription coach sera retirée. [Si dernier coach inscrit, ajout : "La séance se retrouvera sans encadrement après ce changement."] Continuer ? ». À la validation : retrait de
coaches[]+ inscription comme athlète selon §4.9 (statut résultant selon capacité + quota). - Cas 3 — Par tiers, coach → athlète : un tiers (coach C2 ou admin) inscrit C1 (déjà dans
coaches[]) comme athlète. Dialog au tiers symétrique au cas 2. Notif à C1 push + email. - Cas 4 — Par tiers, athlète → coach : un tiers inscrit C1 (déjà inscrit comme athlète) comme coach. Dialog au tiers symétrique au cas 1. Notif à C1 push + email.
Garde-fou « dernier coach » (cas 2 et 3) : warning supplémentaire dans le dialog si la bascule retire le dernier coach inscrit. Bascule autorisée — responsabilité humaine. Pas de symétrique « dernier athlète » (retirer un athlète libère simplement une place).
4.11.6 Retrait du rôle coach par un admin
- Maintien sur les séances en cours : les inscriptions
coaches[]existantes sur des séances futures sont conservées jusqu'au début de chaque séance (continuité opérationnelle). - Refus implicite des nouvelles : le user ne peut plus s'inscrire comme coach sur de nouvelles séances après le retrait du rôle, et ne peut plus en inscrire d'autres.
- Pour retirer le user des
coaches[]futurs, l'admin utilise la voie « Désinscrire un coach par un tiers ». AuditLog role_changed.
4.12 Contenu de séance et édition WYSIWYG
4.12.1 Champ texte enrichi (contentMarkdown, agenda de club_event, futurs champs équivalents)
- Mode d'édition : WYSIWYG à barre d'outils. Pas de syntaxe markdown brute exposée. Public cible : coachs et admins majoritairement bénévoles.
- Stockage interne en markdown (portabilité, export).
- Périmètre exposé (volontairement restreint pour cohérence visuelle et minimisation XSS) :
- Gras, italique, barré.
- Listes à puces et numérotées (avec imbrication).
- Liens hypertexte (URL externe).
- Titres h2 et h3 (pas de h1 — réservé au titre de la séance/événement).
- Citation (blockquote).
- Hors V1 : tableaux, images inline (la pièce jointe couvre le besoin média), couleurs, polices, blocs de code, vidéos embed, alignement, listes de tâches.
- Rendu côté lecteur : sanitisé, neutre, sans styles personnalisés au-delà du thème club. Liens externes :
target="_blank" rel="noopener noreferrer". - Accessibilité : raccourcis clavier standards (Ctrl/Cmd+B, +I, +K pour lien), focus visible, nav clavier dans la barre d'outils.
4.12.2 Pièce jointe
- 1 fichier, taille max 5 Mo. Formats : PDF, PNG, JPG/JPEG, WebP. Pas de GIF animé, pas de HEIC (conversion côté client si upload iPhone, comportement à arbitrer techniquement).
4.12.3 Visibilité
Visible à tous les membres connectés du club (athlètes, coachs, admins, parents) — qu'ils soient inscrits à la séance ou non. Pas d'accès public anonyme. La pièce jointe suit la même règle (cohérence : une seule règle pour toute la fiche). Voir n'est pas s'inscrire — le mécanisme d'inscription/quota reste inchangé.
4.12.4 Édition après début / après fin
Libre. Le coach peut compléter ou corriger le contenu en cours de séance (correctifs derniers) ET après la fin (notes post-séance, retours, photos). Notif via la même case à cocher §4.7 pour permettre des compléments silencieux. Distinct du blocage d'inscription/désinscription post-début (§4.9) qui reste strict.
4.12.5 Débriefs de compétition (Debrief)
Sur une séance kind = competition, chaque membre ayant participé peut publier son débrief : un retour personnel sur la compétition, en texte enrichi. Distinct du contentMarkdown (réservé training, rédigé par le coach) : le débrief est rédigé par l'athlète, signé, et il peut y en avoir plusieurs par compétition.
Auteur et cardinalité
- Auteur = un
Userayant uneRegistrationparticipatingsur la compétition. - 0..N débriefs par compétition, au plus 1 par (compétition, auteur). Pas d'entité « participation » dupliquée : unicité
(sessionId, authorId).
Champ texte : texte enrichi WYSIWYG, réutilisant exactement le périmètre exposé §4.12.1 (gras, italique, barré, listes, liens, h2/h3, citation), stockage markdown, sanitisation serveur faisant foi. Pas de pièce jointe en V1 (les photos vivent dans l'album externe, voir §4.12.6).
Cycle de vie et droits
- Création : par l'auteur, uniquement une fois la compétition commencée (
startAtdépassé) — c'est un débrief après la course. Le bouton « Rédiger mon débrief » n'apparaît qu'aux participants, aprèsstartAt. - Édition : par l'auteur (libre, à tout moment) et par l'admin.
- Archivage / réactivation : admin uniquement, en soft-delete (
archivedAt/archivedBy) — pas de suppression dure dans l'UI admin standard. Un débrief archivé disparaît de la liste publique mais reste consultable et restaurable par l'admin. Cohérent avec le pattern d'archivage des catalogues (§4.6) et desSessionTemplate(§4.8).
Visibilité : tous les membres connectés du club (athlètes, coachs, admins, parents), inscrits ou non à la compétition. Pas d'accès public anonyme. L'auteur est affiché avec la même convention que partout dans l'app (§4.9.4) : un athlète consultant voit prénom + initiale du nom (étendue à deux lettres en cas d'homonymie), un coach/admin consultant voit prénom + nom complet. Pas de traitement « signature » particulier.
Notification : à la publication d'un débrief, push + email à tout le club, sous deux types distincts de la matrice §4.15.3 (opt-out cellule par cellule, actifs par défaut) : les autres participants (participating) de la compétition reçoivent « nouveau débrief » ; tous les autres membres actifs — non inscrits, en liste d'attente, parents — reçoivent « débrief d'une autre compétition ». Chacun peut ainsi ne suivre que les compétitions auxquelles il participe. L'auteur n'est pas notifié ; un parent reçoit l'annonce club une seule fois, pas une par enfant. L'édition ultérieure ne renotifie pas (même esprit que les compléments silencieux §4.12.4).
Réaction « j'aime » : un seul type de réaction, qu'on pose ou qu'on retire.
- Qui : tout lecteur du débrief (membre connecté, mineur compris s'il a son compte), sauf son auteur. Réagir n'abonne à rien.
- Affichage sous le débrief : le nombre de « j'aime » et les noms selon la convention §4.9.4 (« Toi, Léa M. et 3 autres »).
- Notification à l'auteur seulement, regroupée : le premier « j'aime » ouvre une fenêtre de 30 min ; ceux qui suivent s'y ajoutent (« Léa, Tom et 3 autres ont aimé ton débrief ») au lieu d'envoyer une notification chacun. Un « j'aime » retiré avant l'envoi en est retiré ; s'il n'en reste aucun, rien ne part. Type dédié de la matrice §4.15.3, actif par défaut, routé au garant comme les autres notifications de l'auteur (§4.15.5).
- Archivage : un débrief archivé n'accepte plus de réaction ; les siennes sont conservées et masquées avec lui, et reviennent à sa réactivation.
- RGPD : les réactions d'un compte supprimé sont effacées (§4.3) — un geste de la personne, pas un contenu du club, à la différence du texte des débriefs.
Mise en avant : l'accueil porte un bloc « Derniers débriefs » listant les compétitions ayant reçu un débrief (non archivé) depuis moins de 15 jours — au-delà, il a déjà été lu. Une ligne par compétition : nom, date, nombre de débriefs de la période et prénoms des auteurs ; la compétition débriefée le plus récemment en tête ; la ligne ouvre directement l'onglet Débriefs de la fiche. Bloc masqué s'il est vide, identique pour tous les membres (y compris en vue parent). Sur le planning, la carte d'une séance porte le nombre de débriefs actifs.
RGPD : à l'anonymisation d'un compte (§4.3), authorId est anonymisé comme les autres références (anon:user:<hash>) ; le texte du débrief est conservé (valeur pour le club), aligné sur le traitement des inscriptions anonymisées. Recommandation aux rédacteurs : éviter de citer le nom de tiers ou des informations de santé (recommandation, pas de validation algorithmique — cf. §4.12.1).
4.12.6 Album photos externe (photosAlbumUrl)
Une competition ou un club_event peut porter un lien externe vers un album photos partagé (typiquement Google Photos), publié en amont par l'organisateur club et alimenté hors-app par les participants.
- 1 seule URL par séance, optionnelle, saisie/éditée par coach + admin (le créateur), au formulaire de création/édition de séance.
- Aucune intégration : pas d'embed, pas d'API, aucun appel serveur vers l'URL, aucune photo stockée par l'app. Simple lien ouvert
target="_blank" rel="noopener noreferrer"(cohérent §4.12.1). - Distinct d'
externalUrl(qui pointe l'organisateur / le formulaire d'inscription) :photosAlbumUrl= galerie partagée. - Visibilité : tous les membres connectés du club.
4.13 Parcours (OpenRunner Pro + GPX) et météo
Toutes les séances peuvent optionnellement porter des informations de parcours.
4.13.1 OpenRunner Pro — embed iframe officiel
Principe : la fiche séance affiche le parcours via le widget iframe officiel d'OpenRunner Pro — carte interactive (zoom, déplacement, profil altimétrique) visible dans la fiche sans clic supplémentaire. iframe HTML standard, pas de SDK JS à charger ni d'API REST côté serveur.
Compte OpenRunner Pro = prérequis assumé du club déployant. Un club self-hosté souscrit son propre compte OR Pro (à documenter dans le guide d'installation, comme le client Google OAuth et le fournisseur d'email transactionnel).
Pas de credential OpenRunner stockée côté app. Aucune API key, aucun OAuth, aucun champ de configuration admin V1 dédié à OR.
Deux champs côté Session, indépendants (l'un, l'autre, ou les deux ; chacun rendu selon qu'il est renseigné) :
routeOpenrunnerEmbedUrl?(optionnel) : URLsrcdu widget d'embed, formathttps://www.openrunner.com/embed.html?code=<token_opaque>. Lecodeest un token opaque chiffré généré par OR Pro, non dérivable depuis l'URL publique. Le coach extrait cette URL depuis OR Pro (parcours > Partager > Embed > copier l'URLsrc). Renseigné ⇒ carte iframe affichée dans la fiche.routeOpenrunnerPublicUrl?(optionnel) : URL publique du parcourshttps://www.openrunner.com/r/<id_numerique>. Renseigné ⇒ bouton permanent « Ouvrir sur OpenRunner » (nouvel onglet,target="_blank" rel="noopener noreferrer"), indépendamment de la présence ou du chargement de l'iframe.
Les deux champs ne se dérivent pas l'un de l'autre (le
codeopaque de l'embed n'est pas l'idnumérique public). La section « Parcours » s'affiche dès qu'au moins l'un des deux (ou un GPX) est renseigné.
Stockage : URL src uniquement, jamais le bloc iframe HTML brut. Stocker du HTML brut ouvrirait une porte XSS (un coach malveillant ou un compte compromis pourrait substituer n'importe quel HTML/JS) et ferait perdre le contrôle uniforme du rendu. L'app régénère l'iframe côté client à chaque affichage avec attributs figés (width="100%", height="650", loading="lazy", style="border:none", éventuellement sandbox selon ce qu'OR exige).
Validation serveur stricte (whitelist) :
- Sur
routeOpenrunnerEmbedUrl:https+ hostname exactwww.openrunner.com+ path exact/embed.html+ paramcodeprésent et non vide. Tout écart = refus avec message « Lien d'embed OpenRunner invalide — colle l'URLsrcissue de la fonctionnalité Embed d'OR Pro ». - Sur
routeOpenrunnerPublicUrl:https+ hostnamewww.openrunner.com(sans contrainte de path — OR utilise plusieurs schémas d'URL publique). - Validation symétrique côté client (feedback immédiat), serveur fait foi.
Conséquence RGPD : aucune donnée personnelle de l'application n'est transmise à OR ; l'iframe expose à OR l'IP et l'user-agent de l'utilisateur consultant (comportement standard d'embed tiers). À mentionner dans la politique de confidentialité (rédaction différée pré-prod).
Fallback si l'iframe échoue à charger (parcours retiré, compte Pro expiré, OR en maintenance, CSP) : message « Carte indisponible » à la place de la carte. Le bouton « Ouvrir sur OpenRunner » (si routeOpenrunnerPublicUrl renseignée) reste affiché en dessous et couvre l'accès au parcours. Pas de retry automatique.
4.13.2 GPX
- Upload direct, max 5 Mo (côté client ET serveur).
- Parsing client uniquement, jamais serveur (surface d'attaque réduite). Le client extrait distance totale, D+/D-, alt min/max, nb points, durée estimée, et envoie les métadonnées + fichier brut.
- Affichage du tracé sur fond OpenStreetMap, avec son profil altimétrique dessous.
- Survol du tracé (fiche séance et fiche parcours) : à la demande, jamais automatiquement, un point parcourt le tracé et la carte le suit. Lecture/pause, curseur pour se placer n'importe où, vitesse ×1/×2/×4 (un survol complet dure environ 1 min à ×1, quelle que soit la distance). La portion parcourue se distingue du reste, un curseur se déplace sur le profil altimétrique, et la distance parcourue/totale, l'altitude et la pente locale (lissée sur 100 m) du point courant sont affichées. Fonctionne sans déverrouiller la carte. Pas de survol sans GPX (carte OpenRunner seule).
- Téléchargement visible à tous les membres connectés du club (cohérence §4.12 — pas d'info de fiche séance restreinte aux inscrits).
- Le GPX déposé sur une séance alimente la bibliothèque de parcours (§4.20) : la séance le référence, elle ne le possède pas. Retirer le parcours d'une séance ne supprime donc jamais le fichier.
4.13.3 Coexistence OR + GPX
Les deux sources peuvent coexister sur une même séance (URL OR Pro et GPX). Elles sont alors présentées en deux onglets, et c'est le tracé GPX qui s'ouvre par défaut (arbitré le 2026-08-15) : c'est la source que le club maîtrise — hébergée chez lui, sans dépendance à un compte tiers ni à un jeton d'embed qui peut expirer, et accompagnée de ses métriques. La carte OpenRunner reste accessible en un clic, et n'est chargée qu'à l'ouverture de son onglet — un appel tiers de moins sur chaque consultation de fiche, cf. la conséquence RGPD du §4.13.1.
4.13.4 Lieux et géocodage
Bibliothèque Location (cf. §4.6) : nom, adresse, latitude, longitude, type, notes, is_archived. Coachs créent au fil de l'eau, admin gère/archive.
À la création de séance, le coach choisit soit un lieu de la bibliothèque, soit une adresse ponctuelle propre à cette séance — les deux sont exclusifs. L'adresse ponctuelle est géocodée via le même service ouvert et bénéficie donc, exactement comme un lieu favori, de l'aperçu cartographique et de la météo prévisionnelle (§4.13.5). Elle répond au cas de la compétition ou de l'événement qui se tient dans un endroit qui ne revient pas : le renseigner ne doit pas faire entrer une entrée à usage unique dans la bibliothèque de lieux favoris. Elle vit sur la séance et nulle part ailleurs — ni promotion vers la bibliothèque, ni réutilisation d'une séance à l'autre.
locationText est, lui, une précision libre (« RDV parking nord ») qui s'ajoute au lieu retenu au lieu de le remplacer, et qui n'est jamais géocodée. Les séances antérieures dont le locationText tient lieu d'adresse continuent de s'afficher à l'identique : aucune reprise de données.
Saisie assistée de l'adresse (gestion des Location) : un champ unique propose une autocomplétion — la recherche porte sur une adresse OU un nom de lieu et affiche des suggestions au fil de la frappe (fournies par un service de géocodage ouvert), présentées de façon lisible (nom du lieu en titre, adresse en dessous, type de lieu) plutôt qu'en chaîne brute concaténée. La sélection d'une suggestion auto-remplit nom (si vide), adresse, type (si déductible) et coordonnées (latitude/longitude). Les coordonnées restent éditables à la main si besoin. Évaluation en temps quasi-constant attendue (suggestions mises en cache, requêtes limitées pour respecter la politique d'usage du service).
Aperçu cartographique : dès qu'un lieu est géocodé — Location de la bibliothèque ou adresse ponctuelle de séance —, sa localisation est affichée sur une carte (marqueur) — dans le formulaire de lieu et dans le formulaire de séance (recentrée en direct à la sélection d'une suggestion) et en consultation de séance (bloc « Lieu » de la fiche, uniquement si le lieu est géocodé).
Échec / lieu non trouvé : si aucune suggestion ne correspond, saisie manuelle de lat/lng possible directement dans les champs coordonnées. Pas de fallback en cascade.
4.13.5 Météo prévisionnelle
- Source : service gratuit ou peu coûteux, hébergé UE, licence permissive, idéalement sans clé d'API. Cible : Open-Meteo (CC BY 4.0). Mention obligatoire de la source en pied de cartouche si Open-Meteo retenu.
- Paramètres affichés : température 2 m, probabilité de précipitations, précipitations (mm), vent (km/h + direction), code météo (pictogramme).
- Portée : la durée de la séance, pas son instant de départ. Une sortie longue peut partir sous un ciel dégagé et finir sous l'averse ; la prévision doit renseigner la question posée (« comment je m'habille ») sur tout le créneau. Elle annonce donc l'évolution de la température, les extrêmes de vent, le maximum de probabilité de pluie, le cumul des précipitations, et le pictogramme du phénomène le plus sévère rencontré — pas le plus tardif ni le plus précoce. La plage horaire couverte est affichée, pour que l'origine des valeurs soit lisible. Une séance tenant dans une seule heure affiche des valeurs ponctuelles, comme auparavant.
- Pas de déroulé heure par heure ni de courbe : la cartouche répond à « je m'habille comment », pas « montre-moi la journée ».
- Logique : affichage sur séances géocodées uniquement, et seulement si la séance est dans la fenêtre J-16. Si lieu non géocodé → cartouche masquée. Si > 16 j → cartouche absente, note discrète.
- Cache serveur 3h par couple
(lieu, créneau horaire)pour ne pas marteler la source. Pré-calcul périodique pour les séances de la fenêtre.
4.14 Flag « j'offre l'apéro »
Tradition club : au moins une fois par semaine, un adhérent paye l'apéro post-séance (anniversaire, podium, record, mutation pro…). Affordance simple sur la fiche séance, sans alourdir le modèle ni créer de pression sociale.
4.14.1 Périmètre
- 3
kindcouverts (training,competition,club_event). - Qui peut flagger ? Uniquement un
Userdont laRegistrationest active (status = 'participating'). Pas de flag depuis unewaitlist(promesse implicite « je serai là pour offrir »). Si l'inscription bascule dewaitlistàparticipating(mécanismes A/B/C), l'athlète refait le geste s'il le souhaite. - Pas de flag par procuration : un coach / admin ne peut pas flagger un athlète à sa place (le geste est personnel — engagement, pas attribution). Pas de pouvoir parent garant.
4.14.2 Contenu et cardinalité
- Plusieurs flags simultanés sur la même séance, sans plafond (tournées partagées).
- Un seul flag par utilisateur par séance (unicité
(sessionId, userId)). - Motif texte libre optionnel (
AperoFlag.motif, max 140 caractères) : précise la raison (« mon anniversaire », « podium dimanche »). Affiché en infobulle. Recommandation documentaire : pas d'infos confidentielles ni de noms tiers.
4.14.3 Fenêtre temporelle
- (Dé)flag libre jusqu'au début de la séance (
Session.startAt). - Bloqué après
startAt— règle uniforme avec la fenêtre d'inscription/désinscription §4.9.
4.14.4 Cycle de vie : retrait du flag
Trois voies, toutes tracées en ActivityLog apero_unflagged :
- Voie 1 — Self-déflag : le payeur retire son propre flag.
actorId = userId. - Voie 2 — Modération coach / admin : tout
Userayant le rôlecoachouadminpeut retirer le flag d'un athlète (bouton « Retirer ce flag » sur chaque entrée de la liste des payeurs, visible côté coach/admin uniquement). Cas d'usage : blague, doublon non intentionnel, malentendu. Pas de motif structuré requis. - Voie 3 — Cascade automatique sur perte d'inscription active : dès que la
Registrationperdparticipating, le flag est supprimé. Cas couverts :- Désinscription self ou par un coach.
- Annulation de la séance (cascade sur tous les flags).
- Bascule de saison (
bulk_athlete_deactivation). - Bascule athlète → coach sur la même séance.
actorId = system.
Réversibilité : à la restauration d'une séance annulée, les AperoFlag cascadés sont également restaurés (cohérent avec la restauration des inscriptions). ActivityLog apero_flagged avec actorId = system. Si entre-temps l'athlète a perdu son inscription active pour une autre raison, le flag n'est pas restauré.
4.14.5 Affichage
Visibilité publique à tous les membres connectés (cohérent avec §4.9.4). Pas d'accès public anonyme. Niveau :
- Athlètes consultant : prénom + initiale (extension auto homonymes).
- Coachs / admins consultant : prénom + nom complet.
- Motif libre : infobulle au survol / tap. Tronqué visuellement à ~60 caractères avec « … » + infobulle complète au-delà.
Trois affordances UI (sémantique figée, pictogramme et placement à arbitrer maquettes) :
- Planning compact (4 vues §4.7) : icône bien visible sur la pastille de séance dès qu'au moins un flag actif existe.
- Fiche séance détaillée : section dédiée « Apéro offert par » + bouton « J'offre l'apéro » / « Je ne l'offre plus » pour les inscrits actifs. Coachs / admins voient en plus un bouton « Retirer ce flag » par entrée.
- Home athlète (« mes prochaines séances ») : badge sur chaque carte où l'athlète est inscrit dès qu'au moins un payeur est flaggé.
4.14.6 Notifications
Aucune notification envoyée à un (dé)flag. Diffusion par consultation du planning. Évite de surcharger la matrice §4.15.
4.14.7 Traçabilité
ActivityLogactionsapero_flagged/apero_unflagged.- Pas de motif structuré dans le journal : le motif vit sur l'entité
AperoFlag. - Pas de double trace en
AuditLog: l'apéro n'est pas une action de gouvernance. - Anonymisation à la suppression de compte selon §4.18.
4.15 Notifications
4.15.1 Canaux
- Push : web push (PWA installée).
- Email : transactionnel hébergé UE.
Chaque canal peut être fermé à l'échelle du club (§4.17) : un club n'a pas forcément les moyens techniques du push, ni de fournisseur d'email. L'interrupteur s'applique en amont des préférences individuelles (§4.15.3) — un canal fermé ne part pour personne, quelle que soit la matrice, et les préférences déjà exprimées sont conservées telles quelles pour une éventuelle réactivation. Les deux canaux peuvent être fermés simultanément.
Exemption des messages porteurs d'un accès au compte. Ni l'interrupteur de canal ni la pause (§4.15.4) ne s'appliquent aux messages sans lesquels un adhérent ne peut ni entrer dans l'application, ni y revenir (ces messages ne figurent pas non plus dans la matrice §4.15.3) : les emails d'authentification (§4.1.1) et les invitations d'activation (§4.1.3, y compris celle du mineur autonomisé §4.2.1). Ces messages ne sont pas des notifications auxquelles on s'abonne — ce sont les portes d'entrée du compte. Un club en push seul qui ne pourrait plus inviter personne n'aurait aucun moyen de faire activer un compte, donc d'installer la PWA, donc de délivrer le moindre push : le canal voulu resterait inaccessible faute de pouvoir franchir la porte.
L'exemption porte sur le consentement, jamais sur la joignabilité : un destinataire sans adresse email, au compte inactif ou anonymisé, reste hors d'atteinte et l'invitation est refusée avec un motif explicite plutôt qu'ouverte à vide. Ce refus vaut pour les deux origines d'invitation — celle de l'adhérent créé par le bureau (§4.1.3) comme celle du mineur autonomisé (§4.2.1) : ouvrir un jeton d'activation sur un compte injoignable écrirait une trace d'envoi pour un message que personne ne recevra.
Push sur mes appareils (onglet Notifs du profil) : l'adhérent doit pouvoir savoir si ses push fonctionnent, sans passer par le bureau.
- Notification de test : quand le push est activé sur l'appareil, un bouton envoie une notification à cet appareil seul. Fréquence limitée (3 envois par 10 min et par compte). Le message dit si le service de notifications l'a acceptée, ne reconnaît plus l'appareil (à réactiver) ou est injoignable.
- Mes appareils : chaque appareil abonné (système et navigateur), la date de son dernier envoi réussi, un repère sur l'appareil courant et un signal si les derniers envois ont échoué. Un autre appareil se retire d'un geste (confirmation simple) ; l'appareil courant se coupe par l'interrupteur de l'onglet.
- Avertissement quand aucun appareil n'est abonné alors que l'adhérent veut des push (pas en pause, au moins un type activé en push) : ses push ne sont alors lisibles que sur la page Alertes (§4.15.7). Le message dit quoi faire selon l'appareil (activer, autoriser dans le navigateur, installer l'application sur iPhone). Rien n'est affiché quand le club a fermé le push.
- Pas de repli par email quand un push n'aboutit pas : l'adhérent règle ses canaux dans la matrice §4.15.3.
Mode d'envoi : par défaut, les notifications partent en envoi différé par lots (la latence de livraison reste bornée par la période de traitement). Une option envoi immédiat est proposée aux points de déclenchement (ex. modification de séance §4.7) et depuis l'écran de gestion des envois (§4.15.6), pour pousser sans attendre le lot suivant. L'évaluation des préférences (§4.15.3), de la pause (§4.15.4) et du routage parent/enfant (§4.15.5) est identique dans les deux modes.
4.15.2 Types
- Annulation de séance.
- Promotion depuis la liste d'attente (mécanismes A, B, C — un seul opt-out couvre les trois).
- Inscription forcée par un coach (override).
- Modification de séance (date, heure, lieu).
- Ajout/modification de contenu de séance (sous-types texte, parcours, météo).
- Création d'une compétition ou d'un événement club ciblant une catégorie de l'utilisateur.
- Ouverture des inscriptions à une compétition ciblant une catégorie de l'utilisateur : push + email 15 min avant l'heure d'ouverture, ou à 9 h heure club le jour même sans heure connue (cf. §4.7). Le message donne l'heure exacte.
- Nouveau débrief sur une compétition à laquelle tu participes : push + email aux autres participants à la publication d'un débrief (cf. §4.12.5).
- Débrief d'une autre compétition : push + email à tous les membres actifs non participants à la publication d'un débrief (cf. §4.12.5). Type distinct du précédent, pour pouvoir le couper sans perdre les débriefs de ses propres compétitions.
- Réactions à mon débrief : push + email à l'auteur quand des membres aiment son débrief, regroupés en une notification par fenêtre de 30 min (cf. §4.12.5).
- Inscription / désinscription d'un coach sur une
training: push + email aux autres coachs déjà inscrits + à l'admin. Si l'action est déclenchée par un tiers, également au coach concerné (distinct de l'actorId). Pas de notif aux athlètes inscrits (sauf si une modification de séance est par ailleurs déclenchée). - Affectation d'un coach au formulaire de création d'une
Session: notif immédiate push + email à chaque coach autre que le créateur auto-affecté. - Affectation via
defaultCoachIds[]à la génération en lot d'unSessionTemplate: notif récapitulative unique par coach (cf. §4.8 — évite le spam). - Accès athlète suspendu : pas d'email ni de push à la suspension de masse → bannière in-app persistante à la prochaine connexion (cf. §4.4).
- Accès athlète réactivé : email à l'utilisateur réactivé individuellement.
- Nouveau parent garant : push + email au garant entrant quand un admin rattache un mineur à un garant ou change son garant, et au pupille s'il a un compte propre (P2). Le garant sortant reçoit la rupture (§4.2.2), pas cette notification. Informative, hors matrice §4.15.3.
Pas de rappel temporel automatique avant événement en V1 (J-1, H-2, J-7 etc.), toutes kind confondues. La notification d'ouverture des inscriptions n'en est pas un : elle signale une échéance chez l'organisateur, une seule fois, sans rappel la veille.
Destination du lien (push, email et page Alertes, §4.15.7) : une notification qui concerne une séance ouvre sa fiche sur l'onglet qui porte l'information (#99) — Débriefs pour un nouveau débrief (de sa compétition ou d'une autre) ou une réaction, Encadrement pour l'inscription, la désinscription ou l'affectation d'un coach ; les autres ouvrent l'onglet Infos, où figurent statut et horaires. Sur ordinateur, sans onglets, la section correspondante est amenée à l'écran. Un onglet absent de la fiche ouvre Infos.
4.15.3 Préférences — matrice granulaire
- Chaque utilisateur a une matrice type × canal dans son profil.
- Défaut : tout activé. Opt-out cellule par cellule.
- Push iOS : limitation Safari 16.4+ et PWA installée acceptée. Email = fallback pour les iOS non-PWA.
- Pas de quiet hours côté app (l'OS de l'utilisateur gère iOS Focus, Android DND).
4.15.4 Pause globale des notifications
- Toggle « Pause toutes les notifs » dans le profil. Aucune notif (push + email) jusqu'à réactivation manuelle explicite.
- Pas de durée auto ni d'expiration. Si l'utilisateur oublie de la lever, c'est sa responsabilité (mention claire « tant que tu ne réactives pas, tu ne reçois rien »).
- Distinct de la matrice fine : matrice = filtres permanents par type/canal ; pause = interrupteur master temporaire.
- Pendant une pause, les événements applicatifs continuent ; seul le canal est suspendu.
- Hors pause : les messages porteurs d'un accès au compte (§4.15.1) — emails d'authentification et invitations d'activation. La pause exprime « pas d'alertes en ce moment », pas « ferme-moi la porte de mon compte ».
4.15.5 Notifs parent / enfant
Routage selon la phase (§4.2) :
- P1 : uniquement au parent garant.
- P2 : à l'enfant ET au parent garant (push + email sur les deux).
- P3 : à l'enfant uniquement.
La matrice §4.15.3 est propre à chaque utilisateur. Le parent gère ses opt-outs sur son compte, l'enfant (P2) gère les siens. Le parent ne peut pas modifier les préférences de l'enfant.
Identification du sujet. Une notification adressée au garant nomme l'enfant qu'elle concerne, et son lien ouvre l'application sur cet enfant comme sujet consulté (§4.2). Un parent est fréquemment adhérent lui-même et garant de plusieurs enfants : ses notifications et les leurs arrivent sur le même compte, dans la même boîte. Sans cette identification, deux notifications de deux enfants — ou celle du parent et celle de son enfant — sont indiscernables, et le lien mène à un écran qui parle de la mauvaise personne.
Contenu attendu. Une notification est autoporteuse : elle dit qui elle concerne (ci-dessus), quoi (son type) et, quand elle porte sur une séance, laquelle et quand. Elle doit rester compréhensible sans ouvrir l'application — c'est ce qui décide de la garder activée.
4.15.6 Gestion des envois sortants (bureau)
Écran bureau / admin donnant la main sur la file des notifications sortantes (NotificationOutbox, §5.1), pour superviser et rattraper les envois sans attendre le traitement automatique.
- Consultation filtrée : par statut (en attente / envoyée / en échec / annulée / sans destinataire), canal (push / email), type (§4.15.2) et destinataire ; détail d'un envoi (canal, contenu, nombre de tentatives, horodatages).
- Rattrapage : annulation d'un (ou de plusieurs) envoi(s) encore en attente — utile quand une notification a été générée par erreur, avant qu'elle ne parte.
- Envoi manuel immédiat : pousser tout de suite un envoi (ou tous les envois en attente) sans attendre le lot différé suivant (même effet que l'option « envoi prioritaire » §4.7).
- Rejeu des échecs : relancer les envois en échec, ou sans destinataire une fois l'adhérent équipé.
- Sans destinataire : statut distinct d'« envoyée » quand rien n'est parti — aucun appareil abonné au push, ou pas d'adresse email. Pas de nouvel essai. Un push sans destinataire reste lisible sur la page Alertes (§4.15.7), seul endroit où l'adhérent peut alors le voir. Pas de repli par email.
- Santé du push : l'écran donne le nombre d'appareils abonnés et le bilan des push des dernières 24 h (livrés, en échec, sans destinataire). Un bandeau d'alerte s'affiche quand au moins la moitié des essais échouent (5 essais au minimum) : signe d'une panne de configuration ou du service push, à repérer avant les adhérents. Les envois sans destinataire n'entrent pas dans ce taux : ce n'est pas une panne.
- Fiche adhérent : indique le nombre d'appareils abonnés au push, ou « aucun appareil abonné ».
- Appareils abandonnés : un appareil qui échoue 5 fois de suite sans aucune livraison réussie depuis 30 jours (ou depuis son abonnement) est désabonné, comme un appareil refusé par le service push.
- Accès : admin uniquement (acte de gouvernance, cohérent avec l'accès aux journaux §4.18). La consultation ne ré-émet jamais ; seules les actions explicites (annuler / pousser / rejouer) agissent sur la file.
4.15.7 Page Alertes (adhérent)
Chaque adhérent retrouve dans l'application les notifications push qu'il a reçues, avec un compteur de non-lues (cloche + navigation) remis à zéro à l'ouverture de la page.
- Visibilité : une alerte liée à une séance reste affichée jusqu'à 7 jours après la fin de la séance, quelle que soit sa date d'envoi — une compétition annoncée longtemps à l'avance ne disparaît pas avant d'avoir eu lieu. Séance annulée : même règle sur la fin prévue ; séance supprimée : 7 jours après le créneau connu. Une alerte sans séance (tutelle, compte…) reste affichée 60 jours après son envoi.
- Retrait manuel : l'adhérent retire une alerte d'un geste, sans confirmation, ou vide la liste après confirmation. Le retrait ne vaut que pour lui et ne supprime rien : l'historique des envois (§4.15.6) reste complet, et la notification d'origine (push, email) n'est pas touchée. Pas de restauration.
- Regroupement : les alertes d'un même envoi portant sur la même séance et le même type, mais sur des sujets différents (§4.15.5), forment une seule entrée qui nomme chacun (« Toi, Léa et Tom · … ») ; la retirer les retire toutes. Deux déclenchements distincts restent deux entrées.
- Les alertes retirées ou expirées ne comptent plus parmi les non-lues.
4.16 Dashboard statistiques bureau
4.16.1 Indicateurs V1
- Adhérents actifs : total, par section, par sous-catégorie.
- Taux de remplissage :
participating+waitlistcombinés et distingués (waitlist comme indicateur de demande non satisfaite), par séance / série / discipline / catégorie sur période. - Top séances : les plus / moins fréquentées.
- Évolution mensuelle : graphique d'inscriptions.
- Compétitions : nb de participants déclarés par course.
- Liste d'attente : nb en waitlist, taux de promotion.
- Overrides coach : nb d'inscriptions forcées par coach, par motif (agrégat — détail brut dans la page « Journaux » §4.18).
- Activité coachs (bloc dédié) : nb de séances
trainingencadrées par coach × discipline × période, évolution mensuelle, compteur live de séances futures sans coach. Lescompetitionetclub_eventne sont pas comptabilisés. - Filtres globaux : période, discipline, catégorie.
Bandeau d'alerte douce — comptes éligibles à suppression définitive : dès qu'au moins un User est éligible (deletionRequestedAt ≥ J-7), bandeau passif en tête de dashboard avec lien direct vers la page Adhérents filtrée. Disparaît dès que la liste est vide. Pas de notification active.
Pas de stats de présence / no-show V1 (absence de pointage).
Depuis le dashboard, lien « Consulter les journaux » mène à la page admin « Journaux » §4.18.
4.16.2 Export
- Format XLSX (pas CSV).
- Une feuille par tableau, formatage minimal (en-têtes en gras, dates formatées FR).
- L'export du journal d'audit / activité est distinct du dashboard et applique les filtres en cours de la page « Journaux ».
4.17 Paramètres club configurables (admin)
Page admin « Paramètres du club » :
- Identité : nom du club, logo (upload PNG/JPG/JPEG/WebP/SVG, max 1 Mo), couleurs primaires.
- Icônes PWA : trois PNG téléversés séparément —
192×192et512×512(manifest, tuile Android, notification push) et180×180(« Ajouter à l'écran d'accueil » iOS). Aucun n'est dérivé du logo : une icône d'application est carrée, masquée par le système (cercle sur Android pour les formatsmaskable, coins arrondis sur iOS) et lue à 48 px, contraintes qu'un logo de club ne respecte pas. Optionnel : sans téléversement, l'instance sert le jeu d'icônes livré avec l'application, de sorte qu'un déploiement neuf est installable en PWA sans aucune étape de personnalisation. Un bouton « revenir aux icônes par défaut » rétablit ce jeu. L'icône iOS est aplatie sur fond opaque à la réception (une transparence résiduelle est rendue en noir par iOS). Exigence non-fonctionnelle : le remplacement des icônes ne doit pas exiger de modifier un fichier du dépôt, afin qu'une instance reste à jour par simple mise à jour du code. - Localisation : fuseau horaire (défaut
Europe/Paris). - Catégories d'âge : catalogue éditable (cf. §4.5).
- Disciplines : catalogue éditable (cf. §4.6.1).
- Types d'épreuve : catalogue éditable (cf. §4.6.2).
- Tags de quota : création, renommage, définition du
maxPerWeek, archivage soft delete. Suppression dure si zéro référence.AuditLog quota_tag_modified. - Qualifications coach (cf. §4.11.3).
- Comptes : durée du lien d'invitation/activation (défaut 30 jours).
- Notifications : activation de chaque canal (push, email) pour tout le club — cf. §4.15.1.
- Moyens de connexion : activation du lien magique et de la connexion Google (cf. §4.1.1). La connexion par mot de passe n'est pas désactivable : elle garantit qu'une voie d'accès subsiste. Couper un moyen est refusé tant que des comptes actifs n'auraient plus aucun accès (invariant §4.1.2) — l'écran indique combien de comptes sont dans ce cas avant toute tentative.
- Bascule de saison : bouton « Désactiver tous les athlètes pour la nouvelle saison » (cf. §4.4) et bouton « Démarrer la nouvelle année sportive » (recalcul des catégories principales + reset des surclassements, cf. §4.5).
Toutes ces valeurs sont stockées en singleton côté serveur (sauf catalogues multi-entrées).
4.17.1 Page « Adhérents » (admin)
Distincte de la page Paramètres :
- Liste paginée : nom complet, email, catégorie principale, rôles, accès athlète (actif / suspendu), parent garant si applicable, date de création.
- Compteurs en en-tête : suspendus restants + comptes éligibles à suppression définitive (
deletionRequestedAt ≥ J-7). - Recherche : texte libre nom / prénom / email (autocomplete).
- Filtres :
- Catégorie d'âge (multi, actives + archivées).
- Accès athlète (actif / suspendu / tous).
- Rôles (multi parmi
athlete,coach,admin,parent_garant— pseudo-rôle dérivé du lien de tutelle). - Statut suppression (aucun / en cours de tampon / éligible / tous).
- Actions sur la fiche d'un user : éditer nom + prénom + email + date de naissance + rôles + catégories + qualifications, suspendre / réactiver l'accès athlète (cf. §4.4), supprimer le compte (avec bouton « Confirmer la suppression définitive » distinct qui apparaît / devient cliquable uniquement à partir de J+7 ; bouton « Annuler la demande de suppression » disponible pendant le tampon).
- Bouton « Ajouter un adhérent » : formulaire one-shot (cf. §4.1.3).
4.18 Traçabilité : AuditLog et ActivityLog
Deux journaux séparés, stockés dans deux entités distinctes (cf. §5). Séparation motivée par des profils de volume, valeur unitaire et schéma très différents — éviter de polluer la recherche d'actions humaines à motif par le flux d'événements d'inscription.
4.18.1 AuditLog — Gouvernance
Trace les actions humaines à enjeu (administration, gouvernance, sécurité, RGPD). Faible volume, valeur unitaire élevée, motif libre optionnel.
| Catégorie | Actions |
|---|---|
| Quota / inscriptions | override_quota, promote_quota_exceeded (mécanisme C, N entrées par batch), cancel_session |
| Tutelle / mineurs | guardianship_severed, child_account_activated |
| Cycle de vie compte | account_deletion_requested, account_deletion_cancelled, account_deleted (actorId = admin qui confirme, jamais system), account_activated, role_changed, auth_method_linked, auth_method_unlinked, password_reset, bulk_athlete_deactivation (1 entrée globale par clic admin) |
| Configuration club | quota_tag_modified, category_archived, discipline_modified, event_type_modified, allure_zone_modified, allure_levels_modified, season_rollover (1 entrée globale par démarrage de nouvelle année sportive, cf. §4.5) |
Enum extensible au cadrage technique si une action sensible non identifiée émerge.
Champs : id, actorId, actorRole (snapshot du rôle effectif : admin / coach / athlete / parent), action, targetType, targetId, sessionId?, motif? (libre, optionnel), timestamp.
4.18.2 ActivityLog — Opérationnel
Trace les événements d'inscription au fil de l'eau. Volume élevé, valeur unitaire faible mais ensemble précieux pour débug et cascades.
| Catégorie | Actions |
|---|---|
| Inscriptions | registration_created (avec resultingStatus : participating / waitlist_capacity / waitlist_quota_exceeded), inscription_by_coach |
| Désinscriptions | registration_cancelled |
| Promotions auto | auto_promoted_capacity (mécanisme A — resultingStatus = participating), auto_promoted_self_quota (mécanisme B — resultingStatus = participating ou waitlist_capacity), quota_requeued (place rendue après une place obtenue hors quota, §4.10.4 C — resultingStatus = waitlist_quota_exceeded) |
| Encadrement coachs | coach_registered, coach_unregistered |
| Apéro club | apero_flagged, apero_unflagged |
Pas de double journalisation : le mécanisme C promote_quota_exceeded reste uniquement dans AuditLog.
Champs : id, actorId (peut valoir system pour les promotions auto et cascades), action, userId (utilisateur concerné — peut différer de actorId si l'action est déclenchée par un tiers ou cascade), sessionId, registrationId?, resultingStatus?, timestamp. Pas de champ motif (sans objet).
4.18.3 Anonymisation à la suppression de compte
Dans les deux journaux, à la suppression d'un compte :
actorId,targetId(AuditLog),userId(ActivityLog) du compte supprimé sont remplacés par un marqueur stable (ex.anon:user:<hash>), permettant de continuer à corréler sans révéler l'identité.action,actorRole,sessionId,registrationId,resultingStatus,motif,timestampconservés.- Recommandation documentaire : ne pas saisir de noms propres dans le champ
motiflibre (pas de validation algorithmique en V1).
4.18.4 Rétention
Indéfinie en V1. Pas de purge automatique. Volume ActivityLog à monitorer ; à reconsidérer V2 (purge glissante) s'il devient problématique.
4.18.5 Accès en lecture
Admin uniquement : page dédiée « Journaux » dans le back-office admin.
- Sélecteur en-tête :
Audit/Activity/Tous. - Filtres : acteur (autocomplete), action (multi-select), type de cible, séance (autocomplete), période (défaut 30 j).
- Vue chronologique DESC paginée. Colonnes :
timestamp,acteur(nom + rôle snapshot),action,cible,séance liée,motiftronqué + tooltip (AuditLog uniquement). - Export XLSX : applique les filtres en cours. Une seule feuille avec colonne
source(audit | activity) pour le mode « Tous ».
Coachs : pas d'accès à la page « Journaux ». Badges in-context conservés sur les fiches séance (overrides quota + déblocages quota_exceeded — motif visible si renseigné).
Athlètes / parents : pas d'accès direct in-app. RGPD article 15 satisfait sur demande email à l'admin, qui exporte manuellement via les filtres. À reconsidérer V2.
4.19 Pages d'information (notes club)
Ajout de périmètre post-cadrage (demande bureau, juillet 2026). Non prévu au cadrage initial (§3.1). Assumé comme extension produit ; documenté ici pour rester source de vérité.
Pages courtes d'information à destination des membres : bons d'achat partenaires, codes promo magasin, informations générales du club. Objectif : centraliser dans l'app ce qui circulait sur WhatsApp/oralement.
- Contenu : titre + texte enrichi (même éditeur WYSIWYG que le contenu de séance, cf. §4.12.1 ; pas d'images ni de pièces jointes). Sanitisation serveur identique.
- Édition : admin uniquement (back-office). Création, édition, archivage soft (réactivable) puis suppression dure.
- Visibilité par niveau cumulatif : chaque page cible un niveau minimum —
all: tous les adhérents ;coach: coachs et bureau ;admin: bureau uniquement. Une page n'est jamais servie ni atteignable pour un rôle hors périmètre. La visibilité dépend du regardeur (rôles de l'utilisateur connecté), jamais du sujet parent/enfant consulté (cf. §4.2).
- Bannière d'accueil : une page peut être épinglée. Les pages épinglées visibles par le membre s'affichent, empilées, en haut de l'Accueil (titre + lien vers la page complète). Plusieurs bannières simultanées possibles.
- Consultation : page « Infos » accessible à tout membre connecté (entrée de menu), listant les pages qu'il a le droit de voir.
4.20 Bibliothèque de parcours
Ajout de périmètre post-cadrage (2026-08-01). Non prévu au cadrage initial (§3.1). Assumé comme extension produit ; documenté ici pour rester source de vérité.
Un parcours GPX cesse d'être un attribut jetable de séance (§4.13.2) pour devenir une entité réutilisable que les séances référencent. Motif : le club refait les mêmes boucles toute l'année ; un fichier par séance obligeait à re-déposer le même GPX indéfiniment, sans jamais pouvoir répondre à « quel était le parcours de la sortie longue de mars ? ».
- Création : coachs et admins, par deux chemins équivalents — formulaire dédié de la bibliothèque, ou upload direct depuis le formulaire de séance (qui verse alors le parcours à la bibliothèque). Les deux passent par la même validation.
- Doublons : un parcours re-déposé à l'identique est détecté (empreinte du fichier) et l'app propose de réutiliser l'existant plutôt que de créer un doublon. Le coach garde la main pour créer quand même.
- Consultation ouverte à tous les membres connectés, cohérente avec §4.13.2 : la liste, la fiche et le téléchargement sont accessibles à tout adhérent. Seules la création, la modification et l'archivage sont réservés aux coachs/admins.
- Métadonnées : nom, description libre, discipline, et les métriques dérivées du tracé (distance, D+/D−, altitudes). Les métriques restent calculées côté client (§4.13.2) — le serveur ne lit jamais le fichier.
- Qualification automatique du tracé, pour rendre la bibliothèque triable sans saisie manuelle :
- Secteur géographique (rose des vents à 8 directions) — où se trouve le parcours par rapport au point de départ habituel du club ;
- Forme — boucle arrondie ou parcours étiré (aller-retour, trajet) ;
- Relief — roulant / vallonné / exigeant, dérivé du dénivelé rapporté à la distance.
- Exploration : liste filtrable (recherche texte, secteur, discipline, distance, forme, relief, lieu de départ — plusieurs valeurs par filtre, unies entre elles) et carte d'ensemble superposant les tracés du jeu filtré, chaque tracé identifiable et menant à sa fiche.
- Filtre d'utilisation (2026-10-07, #130) : ce mois-ci, 3 derniers mois, saison en cours, période libre, ou jamais utilisé — une seule valeur à la fois, les périodes s'emboîtant. Un parcours est « utilisé » par une séance qui a eu lieu : commencée et non annulée. Une séance seulement planifiée ne compte pas encore.
- Tri de la liste : nom (défaut), distance, relief (D+ par kilomètre, pas le D+ total qui suit surtout la distance), date d'ajout, dernière utilisation, nombre d'utilisations. Les parcours sans la donnée triée vont en fin de liste quel que soit le sens. Avec un filtre d'utilisation actif, dernière utilisation et nombre d'utilisations se comptent sur la période (« les plus utilisés cette saison »). La carte n'a pas d'ordre de lecture : le tri ne la concerne pas.
- Sur mobile, les filtres se replient derrière un bouton indiquant le nombre de filtres actifs ; recherche et tri restent visibles.
- Fiche parcours : tracé sur fond OpenStreetMap, profil altimétrique, métriques, séances qui l'utilisent, téléchargement du GPX.
- Cycle de vie : archivage soft (le parcours sort de la liste, ses séances passées restent intactes et consultables) puis suppression dure. Un parcours utilisé par au moins une séance ne peut pas être supprimé — seulement archivé. L'archivage ne détruit pas le fichier : restaurer un parcours le rend pleinement fonctionnel.
- Retirer un parcours d'une séance ne fait que rompre la référence : le parcours et son fichier restent dans la bibliothèque. Le remplacement d'un parcours par un autre sur une séance est un changement structurant (§4.9) et suit le régime de notification des inscrits.
4.21 Export vers l'agenda personnel
Ajout de périmètre post-cadrage (2026-09-17, #39). Demande d'athlète : retrouver dans son agenda perso (Google en premier lieu) les séances auxquelles il est inscrit.
Deux gestes distincts, livrés séparément.
4.21.1 Ajout ponctuel d'une séance
- Sur la fiche d'une séance à venir et non annulée, tout membre connecté peut télécharger la séance au format agenda standard (iCalendar), importable dans Google Agenda, Apple Calendrier, Outlook.
- Contenu : titre, début et fin, lieu (nom et adresse), lien vers la fiche. Aucun nom de tiers, aucune liste d'inscrits, aucun contenu de séance : l'événement peut finir dans un agenda partagé.
- C'est une copie figée : un report ou une annulation ne la met pas à jour. L'écran le dit à côté du bouton.
4.21.2 Abonnement personnel
Chaque adhérent peut créer, depuis son profil, une adresse d'abonnement personnelle que son agenda relit périodiquement. Aucune adresse n'existe tant qu'il ne l'a pas demandée ; elle est révocable et régénérable à tout moment (confirmation simple). Accès athlète suspendu, compte supprimé ou anonymisé → flux fermé. Adresse inconnue → introuvable ; révoquée → explicitement disparue.
Contenu, réuni dans un seul flux, fenêtre de 60 jours passés à 365 jours à venir :
Source Rendu Réglage Mes inscriptions actives événement confirmé toujours inclus Mes places en liste d'attente provisoire, titre préfixé « ⏳ Liste d'attente — » case, cochée par défaut Les séances que j'encadre (coach) titre préfixé « Coach — » case affichée aux coachs, cochée par défaut Les séances de mes enfants (parent garant) titre préfixé du prénom de l'enfant case affichée aux garants, tous les enfants ou aucun, cochée par défaut Les réglages sont conservés côté serveur : changer d'avis ne demande pas de se réabonner.
Séance annulée : elle reste dans le flux (l'annulation est réversible), titre préfixé « Annulé — », statut annulé, créneau marqué libre. Une séance supprimée définitivement (§3.1) en sort.
Rappel réglable dans le profil : aucun, 1 h avant, 2 h avant, la veille au soir. Jamais de rappel sur une séance annulée. L'écran précise que seuls certains agendas honorent un rappel sur un calendrier abonné (Apple oui, Google non).
Limite assumée, écrite à l'écran : un agenda abonné se met à jour avec retard (jusqu'à 24 h chez Google). L'agenda n'est pas un canal d'alerte ; les notifications (§4.15) le restent.
Minimisation identique à §4.21.1.
Exigences non-fonctionnelles : le flux est borné dans le temps et se sert sans recalcul quand rien n'a changé ; l'adresse est un secret non devinable.
Hors périmètre : synchronisation bidirectionnelle, invitations par email, ajout automatique à l'inscription, un calendrier distinct par enfant, rappel envoyé par l'app.
4.22 Allures course
Ajout de périmètre post-cadrage. Les coachs prescrivent la course en zones exprimées en % de VMA ; l'application traduit ces zones en allures personnelles.
- Référentiel par discipline : l'admin rattache une discipline à un référentiel d'allures (V1 : course à pied, valeur de référence = VMA). Une discipline sans référentiel n'affiche aucune allure. Le modèle prévoit d'autres référentiels (vélo, natation) sans valeurs en V1.
- VMA du membre : une seule valeur courante par membre et par référentiel, écrasée à chaque mise à jour, sans historique, avec sa date et son origine (saisie libre, ou estimation depuis une distance de course). Saisie et visible par le membre seul : ni coach, ni admin. Bornes de vraisemblance 8–25 km/h, refus côté serveur. Effacée avec le compte (§4.3).
- Aucun chrono n'est enregistré : le temps saisi pour une estimation est une entrée de calcul.
- Zones d'allure (catalogue admin, §4.6) : code court unique (celui que les consignes citent), libellé, alias facultatifs (« SV2 », « SubT » : affichés à côté du code et reconnus comme lui ; aucun alias ne peut reprendre le code ou l'alias d'une autre zone), plage en % de VMA. Plages discontinues acceptées, chevauchement refusé. Seed générique : les coefficients d'un coach se saisissent sur l'instance du club, jamais dans le dépôt public.
- Estimation et projection : par défaut, modèle de Riegel (exposant 1,06, VMA tenue 6 min), sans niveau. L'admin peut compléter la table club (% de VMA tenable par niveau × distance — 5 km, 10 km, semi, marathon — en fourchettes, borne haute ouverte possible). Riegel y figure comme un niveau calculé (ses % dépendant de la VMA, ils sont montrés en exemple sur une plage de VMA) : non modifiable ni supprimable, mais ordonnable et désactivable comme les autres. Chaque niveau peut être retiré de ce qui est proposé aux membres sans être supprimé ; au moins un reste proposé. Le premier niveau proposé est le choix par défaut ; le niveau est choisi à chaque usage et jamais stocké. Estimation hors bornes : « temps improbable ».
- Onglet « Allures » du profil : allures par zone (fourchette /km et temps de passage de 100 m au marathon), curseur d'intensité sur la bande des zones, estimation de la VMA depuis une course, projection de temps de course.
- Consignes de séance (disciplines rattachées au référentiel) : chaque code de zone cité est complété, à l'affichage web, par la fourchette d'allure du lecteur ; une plage (« Z1-Z2 ») reçoit une seule fourchette couvrant les deux zones. Sans VMA renseignée : texte inchangé et invitation à la renseigner.
- Hors périmètre : tests de VMA guidés, outillage coach (saisie collective, groupes de niveau), vélo et natation.
5. Modèle conceptuel (entités et relations)
Vue logique des entités. Volontairement agnostique à la techno de stockage (relationnel, NoSQL document, autre) — la modélisation physique (schéma SQL, collections, dénormalisations, index, contraintes d'intégrité) sera produite au cadrage technique.
5.1 Entités principales
User:firstName,lastName,email?(nullable en P1),dob,isActive,athleteAccessSuspended(booléen — suspend la capacité d'inscription comme athlète, séparé deisActive),deletionRequestedAt?(timestamp — posé au déclenchement du flow §4.3, remis ànullà l'annulation),isMinor(dérivé dedob, jamais saisi ni figé : la minorité change d'elle-même au 18ᵉ anniversaire),roles[](athlete/coach/admincumulables). Lien optionnel0..1vers le parent garant. Pas de stockage du n° de licence FFTri (RGPD).Category:label,ageMin,ageMax(inclusives),sortOrder,archivedAt?. Pas de chevauchement entre catégories actives.UserCategory(M:N) : unUser↔ une ou plusieursCategory. Une catégorie est dite « principale » (dérivée auto, surclassable).Session:- Communs :
kind(training|competition|club_event),title,disciplineId?,startAt,durationMin,locationId?+locationText?,capacity?,categoryIds[],createdBy(immuable),coaches[](M:N),sourceTemplateId?(informatif uniquement),cancelledAt?/cancelledBy?,visibility. training:quotaTagId?,contentMarkdown,contentAttachment,externalStaffLabel?(libellé libre ≤ 120 car. d'un intervenant extérieur — non nominatif, cf. §4.11.2 ; forcé ànullsur les autreskind).competition:eventTypeId(FK Types d'épreuve),distance,externalUrl,photosAlbumUrl?,registrationOpensAt?(heure facultative).club_event:agenda(markdown),externalUrl?,photosAlbumUrl?.- Parcours :
routeOpenrunnerEmbedUrl?,routeOpenrunnerPublicUrl?,routeOpenrunnerId?(dérivé ducodeopaque, non exposé),route?(référence0..1vers unGpxRoute— cf. §4.20 ; remplace les anciensrouteGpxFile?/routeStats?portés par la séance).
- Communs :
Registration(rattachée à uneSession) :userId,status(participating|waitlist|cancelled),waitlistReason?(capacity|quota_exceeded),waitlistPosition?,registeredAt,promotedAt?,promotedBy?,overrideBy?,overrideReason?.SessionTemplate:id,label,kind,disciplineId?,dayOfWeek(1..7),startTimeOfDay,durationMin,locationId?/locationText?,externalStaffLabel?(recopié sur chaque séance générée,traininguniquement),capacity?,quotaTagId?,categoryIds[],defaultCoachIds[],generationStartDate,generationEndDate(obligatoires),createdBy(= admin),status(active/archived). Aucun lien retour comportemental vers lesSessiongénérées.QuotaTag:code,label,maxPerWeek,archivedAt?. Universel — aucune restriction de discipline.Discipline:id,label,referentiel?(référentiel d'allures, §4.22),archivedAt?. Référencée parSession.disciplineIdetSessionTemplate.disciplineId, sur lestraininguniquement (§4.7). Garde-fou : la dernière discipline active ne peut être ni archivée ni supprimée.EventType:id,label,archivedAt?. Référencé parSession.eventTypeId(uniquement pourkind = competition). Garde-fou : le dernier type actif ne peut être ni archivé ni supprimé (cf. §4.6.2 — note ouverte sur ce point).Qualification:id,label,code?,archivedAt?.UserQualification(M:N) :userId,qualificationId,expiresAt?,attributedAt,attributedBy. Unicité(userId, qualificationId)— les renouvellements mettent à jourexpiresAtplutôt que créer une nouvelle ligne.Location:name,address,latitude,longitude,kind,notes,createdBy,isArchived.GpxRoute(§4.20) :name,description?,disciplineId?,gpxFile(référence stockage objets),fingerprint(empreinte du fichier, pour la détection de doublon), métriques dérivées du tracé (distanceKm?,dPlusM?,dMinusM?,altMin?,altMax?), qualification automatique (sector?— rose des vents 8 directions,shape?— arrondi/étiré,grade?— roulant/vallonné/exigeant), données géographiques dérivées (emprise du tracé et tracé simplifié servant l'affichage cartographique),createdBy,archivedAt?. Référencé parSession.route(0..nséances par parcours). Toutes les métriques et données dérivées sont produites côté client (§4.13.2) : le serveur ne lit jamais le fichier, il borne et refuse les valeurs aberrantes.AllureZone(§4.22) :referentiel,code(unique par référentiel),label,pctMin,pctMax,archivedAt?.AllureLevel(§4.22, table club) :referentiel,label,sortOrder,targets(bornes de % par distance, borne haute optionnelle).ReferenceValue(§4.22) :userId,referentiel,value,source(saisie/estimation),sourceDistance?,measuredOn. Unicité(userId, referentiel)— valeur courante sans historique, visible du seul membre, effacée avec le compte.WeatherCacheEntry: indexée par(location, créneau horaire),forecast,fetchedAt.AuditLog:id,actorId(anonymisable),actorRole(snapshot),action(enum extensible),targetType,targetId(anonymisable),sessionId?,motif?,timestamp.ActivityLog:id,actorId(anonymisable, peut valoirsystem),action,userId(anonymisable — concerné par l'action, peut différer deactorId),sessionId,registrationId?,resultingStatus?,timestamp.AperoFlag:id,sessionId,userId(payeur),registrationId(index pour la cascade),motif?(max 140 caractères),flaggedAt,flaggedBy. Unicité(sessionId, userId). Cardinalité par séance :0..N. Hard delete au retrait (traçabilité dansActivityLog).Debrief:id,sessionId(FK,kind = competitionen V1),authorId(FK User, immuable, anonymisable),contentMarkdown(texte enrichi WYSIWYG §4.12.1),createdAt,updatedAt,archivedAt?/archivedBy?(soft-delete admin). Unicité(sessionId, authorId)— au plus 1 débrief par membre et par compétition. Cardinalité par séance :0..N. Nom d'entité volontairement générique (pas spécifique à la compétition) pour rester réutilisable, même si le périmètre V1 se limite àkind = competition(cf. §4.12.5).DebriefReaction:id,debriefId(FK),userId(FK User, effacée avec le compte),createdAt. Unicité(debriefId, userId)— un « j'aime » par membre et par débrief (cf. §4.12.5).InformationPage(note club, §4.19) :id,title,contentMarkdown?(texte enrichi WYSIWYG §4.12.1),visibility(all/coach/admin— niveau minimum cumulatif),pinned(épinglée en bannière d'accueil),createdBy(FK User, anonymisable),createdAt,updatedAt,archivedAt?/archivedBy?(soft-delete admin). Édition admin uniquement. Ajout de périmètre post-cadrage.ClubSettings(singleton) :name,logo,primaryColor,timezone,invitationLinkDays, etc.NotificationPreferences(rattachée à unUser) : matricetype × canal+ flag pause globale.
5.2 Notes importantes
- Le statut
participatingest uniforme sur les 3kind. La distinction engagement ferme vs intention déclarée est portée parSession.kind+ l'absence dequotaTagId+ capacité optionnelle pour competition/club_event — pas par la valeur destatus. Pas d'entitéCompetitionniClubEventséparée. - Pas d'entité
Attendanceen V1 (pas de pointage formel). - Pas de notion de tenant ni d'isolation multi-club.
- Pas de stockage de téléphone, ni de certificat médical (minimisation RGPD).
AuditLogetActivityLogsont deux entités séparées — pas de relation directe (jointure éventuelle viasessionIdou via le marqueur anonymisé).- Cycle de vie compte mineur : en P1, le
Userenfant existe en base sans credential et avecemailnul. La transition P1→P2 crée le credential et renseigne l'email. La transition P2→P3 rompt le lien de tutelle, mais leUserathlète et son historique restent intacts. - Règle d'accès clé : un parent doit pouvoir lire et écrire sur les
Registrationde toutUserenfant dont il est garant (sans quoi il ne peut pas agir en P1 ni P2). À formaliser dans le moteur de règles d'accès choisi au cadrage technique.
6. Contraintes non-fonctionnelles
| Domaine | Contrainte |
|---|---|
| Plateforme | PWA installable (manifest + service worker). Fonctionne offline en lecture du planning de la semaine en cours. |
| Hébergement | UE obligatoire pour le stockage de toutes les données utilisateurs et leurs sauvegardes (RGPD). Région privilégiée : Paris (sinon Francfort ou équivalent UE). |
| Distribution | AGPL-3.0 self-hosting un-instance-par-club. Pas de multi-tenant. |
| Sécurité | Contrôle d'accès appliqué côté backend (pas seulement client). Toute donnée a ses règles d'accès définies dès sa première migration en production. |
| RGPD | Données UE. Suppression de compte = anonymisation des inscriptions et des débriefs (auteur anonymisé, texte conservé — cf. §4.12.5) ; les réactions « j'aime » sont effacées. Pas de stockage santé (certif médical exclu) ni de téléphone (minimisation). Mentions légales / CGU accessibles depuis le footer (rédaction différée pré-prod). |
| Conservation | Pas de purge auto, conservation indéfinie. Monitoring du volume de stockage. |
| Performance | Chargement initial < 2s sur 4G. Planning quasi-instantané après mise en cache. Évaluation du quota en temps quasi-constant à l'inscription (cf. §4.10). |
| Sérialisation des inscriptions | Sur la dernière place, deux inscriptions concurrentes sérialisées atomiquement sur timestamp serveur. Premier = participating, second = waitlist capacity. Pas de double-acceptation, jamais. Implémentation libre. À garantir par tests E2E. |
| Fraîcheur des données | Listes d'inscrits, positions waitlist et compteurs dashboard à jour à chaque chargement de page (ou pull-to-refresh). Le temps réel push est souhaitable mais non requis V1 — activé si la stack le permet sans surcoût, sinon refresh manuel suffit. Cette exigence ne doit pas contraindre le choix de stack. |
| Accessibilité | Niveau AA recommandé (lisibilité mobile, contraste). |
| Langue | Français uniquement V1. |
| Tests | Couverture > 80 % (unitaires + E2E sur parcours critiques : inscription, désinscription, promotion, override). |
| CI/CD | CI obligatoire sur chaque PR (lint, type-check, tests unit + E2E, seuil de couverture > 80 %). Environnement de preview déployable par PR. Déploiement prod manuel par le mainteneur. Garde-fou sur la branche principale. |
| Monitoring | Outillage de monitoring d'erreurs + logs serveur centralisés. |
| Backups | RPO 7j minimum (point-in-time recovery ou équivalent) + export complet périodique à froid (rétention 1 an). |
| Migrations | Toutes les migrations de schéma, règles d'accès et données de seed versionnées dans le repo et reproductibles sur un environnement neuf. |
Choix techniques non figés : frontend, backend, persistance, auth (provider exact), push, email transactionnel, hébergement, monitoring, CI/CD, frameworks de test, bibliothèques de cartes / parsing GPX, génération XLSX, etc. Tous à arbitrer au cadrage technique dédié, après validation des maquettes (M0). Les services tiers nommés dans ce PRD (OpenRunner Pro, Open-Meteo) sont des choix produit / légaux, pas des choix de stack applicative.