# Roadmap FletchScore
**État actuel : v0.1, v0.2 et v0.3 complètes -- 502 tests,
tous verts, confirmés par la CI sans aucun `skipped`** (y compris les
tests fpdf2/qrcode, jamais exécutables dans l'environnement de dev
utilisé ici -- `cryptography`, en revanche, s'y est révélée
disponible, voir la v0.3 ci-dessous).
> **Note de renumérotation** (demande de l'utilisateur) : la v0.2
> d'origine (vue compétiteur lecture seule) était trop petite pour
> justifier une version à part -- fusionnée avec l'ancienne v0.3
> (tokens/sécurité). Ce qui suivait est décalé d'un cran : l'ancienne
> v0.4 (proposition de score) devient v0.3, l'ancienne v0.5 (finition)
> devient v0.4. Renumérotation de documentation uniquement -- aucun
> changement de code, aucun tag git existant retouché.
- **v0.1** : `models/`, `storage/`, `referentiels/`, `io/import_csv.py`
(import + export CSV clubs/compétiteurs), `scoring/`, `gui/`
(8 écrans après réorganisation, voir plus bas), `io/export/`
(CSV/Excel/PDF, classement par épreuve et
global). Modification de compétitions/épreuves/clubs/compétiteurs
existants. 6 barèmes préconfigurés (Flint Indoor, IFAA Indoor, Field,
Hunter, International, Expert Field). Modèles d'épreuve réutilisables.
Saisie au score final (pas volée par volée). Guide utilisateur complet
et cahier des charges recalé sur l'état réel.
- **v0.2** : `api/competiteur.py`, vue compétiteur en lecture seule
(classement live), identité visuelle FletchTime, bilingue FR/EN.
Fondation Token/DemandeRattachement, QR code, GUI organisateur
("Demandes d'accès" -- valider/rejeter/révoquer/envoyer un message),
endpoint web de rattachement, page "Mes messages" compétiteur (cookie
de session signé HMAC), et authentification organisateur (mot de
passe optionnel, PBKDF2). HTTPS décalé en v0.3 (définitif, décision de
l'utilisateur), voir "Points
tranchés" du cahier des charges.
- **v0.3** : proposition de score compétiteur, du formulaire
web (identifié + inscrit, sans champ falsifiable) jusqu'à la
validation organisateur (`gui/ecran_propositions.py`) -- le score
proposé devient LE score officiel dès validation, réutilisant
`StatutScore.PROPOSE` déjà prévu dans le modèle depuis la v0.1.
Garde-fou contre les demandes de rattachement redondantes. Accueil
personnalisé (bienvenue, statut par épreuve). Limitation de débit
(`fletchscore/limiteur_debit.py`). HTTPS local, certificat auto-signé
généré automatiquement (`fletchscore/certificat_https.py`).
Détail incrément par incrément ci-dessous.
Découpage par jalons livrables, dans l'ordre des dépendances réelles :
impossible de tester le scoring sans modèle de données, impossible de
tester la vue compétiteur sans que le poste organisateur marche déjà
seul. Chaque version doit être testable en conditions réelles avant de
passer à la suivante -- même philosophie que FletchTime (v0.2.0 validée
en compétition live avant d'aller plus loin).
## v0.1 -- Poste organisateur, sans réseau
- [x] `models/` + `storage/` -- entités (Compétiteur, Club, Style,
Competition, Épreuve, Barème, Inscription, Score, Token,
DemandeRattachement), schéma SQLite. 43 tests, tous verts.
- [x] `referentiels/` + `io/import_csv.py` -- chargement `styles.csv`
(pré-rempli IFAA), import `clubs.csv`/`competiteurs.csv` avec
rapport d'erreurs (jamais de création automatique silencieuse).
64 tests, tous verts.
- [x] `scoring/` -- calcul de score par volée, cas particuliers (flèches
en trop/manquantes, mauvaise cible), classement par catégorie,
départage au X. Isolé et testé unitairement en premier, sans GUI ni
DB réelle. 80 tests, tous verts.
- [x] `gui/` -- créer une compétition/épreuve avec un barème, inscrire des
compétiteurs, saisir le score final, classement live
(⚠️ décrit à l'origine comme "volée par volée" -- révisé depuis,
voir "Extension -- Saisie du score final" plus bas)
- [x] `services.py` -- couche de cas d'usage appelée par la GUI
(créer compétition/épreuve, inscrire, saisir un score,
classement live), avec validations métier et messages destinés à
l'organisateur
- [x] `gui/config.py` -- préférences d'affichage (thème) persistées
dans `config/gui.toml`, tolérantes à un fichier absent ou corrompu
- [x] `storage.db.ouvrir_base()` + branchement de `__main__` sur la GUI
(`fletchscore --db chemin.db`)
- [x] Coquille de la fenêtre : barre latérale, navigation, sélecteur de
thème
- [x] Robustesse : arrêt propre sur Ctrl+C/`kill` (SIGINT/SIGTERM),
message clair si aucun affichage n'est disponible plutôt qu'une
trace Tcl brute (`gui/robustesse.py`, testable sans tkinter)
- [x] Écran « Compétitions » (créer/lister compétitions et épreuves)
- [x] Écran « Compétiteurs » (import CSV, liste)
- [x] Export CSV clubs/compétiteurs (symétrique à l'import, round-trip
garanti) -- manque signalé par l'utilisateur après un premier
test réel
- [x] Saisie manuelle de club/compétiteur (sans passer par un CSV) --
`services.creer_club()` / `services.creer_competiteur()`, mêmes
règles que l'import (pas de création automatique de référence
manquante) -- ajouté après le premier essai réel (voir note
ci-dessous), absent du cahier des charges initial ; **pas encore
testé en réel**, contrairement au reste de `gui/`
- [x] Modification de club/compétiteur existant -- `services.
modifier_club()`/`modifier_competiteur()`, identifiant
(`code_club`/`id_federal`) non modifiable (clé référencée
ailleurs). Boutons "Modifier" sur la liste des compétiteurs et
sur un sélecteur dédié dans le formulaire club. ⚠️ **rendu non
vérifié** -- manque signalé par l'utilisateur après un test réel
- [x] Écran « Saisie des scores »
- [x] Écran « Classement »
- [x] `io/export/` -- Excel, PDF, CSV + podiums par catégorie
- [x] `scoring.podium_par_categorie()` -- extrait le top N (défaut 3)
d'un classement déjà calculé, par rang (pas par position --
une égalité au rang 1 met deux personnes sur le podium)
- [x] `io/export/csv.py` -- export brut de secours, classement complet
ou podium seul (même fonction, le filtrage se fait en amont)
- [x] `io/export/excel.py` -- openpyxl, une feuille groupée par
catégorie. Premier export **réellement exécuté et vérifié**
(openpyxl est installé ici, contrairement à fpdf2/customtkinter)
- [x] `io/export/pdf.py` -- fpdf2 (choisi : pur Python, plus sûr sur
Pydroid qu'une lib avec composants C ; besoin simple, un tableau,
pas une mise en page élaborée). Jamais exécutable dans
l'environnement de dev utilisé ici (pas de réseau pour installer
fpdf2) -- **confirmé exécuté et vert par la CI** après correction
du job `test.yml` (voir plus bas), les 3 tests tournent
réellement, plus aucun `skipped`
- [x] Branchement dans `gui/ecran_classement.py` -- boutons "Exporter
CSV/Excel/PDF" + case "Podium seulement". Oublié dans le
premier jet (les fonctions existaient mais n'étaient appelées
nulle part dans la GUI) -- signalé par l'utilisateur. Chaîne
classement → podium → export CSV/Excel vérifiée réellement de
bout en bout hors GUI (PDF non exécutable ici comme toujours)
**Jalon utilisable seul** : ce point donne un FletchScore fonctionnel en
club, sans la partie web/compétiteur. Bon moment pour un premier vrai
test en conditions réelles avant d'aller plus loin.
**Premier essai réel effectué** (juillet 2026) sur la coquille GUI et
les écrans Compétitions/Compétiteurs/Saisie/Classement -- retour
positif. Pas de confirmation détaillée écran par écran ni d'ergonomie
poussée (ex. champs de date en texte libre) ; les formulaires d'ajout
manuel de club/compétiteur, ajoutés juste après ce test, n'ont pas
encore été essayés.
## v0.2 -- Vue compétiteur (lecture seule) + Token et sécurité
- [x] `api/competiteur.py` (lecture uniquement) -- serveur HTTP stdlib
(`http.server`), zéro dépendance ajoutée. Chaque requête ouvre sa
propre connexion SQLite en lecture seule (`file:...?mode=ro`) --
jamais celle de la GUI, qui appartient à un autre thread. Pas de
template engine : HTML généré par de simples f-strings, échappé
via `html.escape`. 13 tests, dont 3 d'intégration avec un **vrai
serveur démarré sur un port réel et de vraies requêtes HTTP**
(`urllib.request`) -- pas seulement les fonctions de génération de
page testées isolément.
- [x] Page web : liste des compétitions/épreuves en cours -> clic ->
classement (par épreuve, ou global cumulé pour toute la
compétition). Rechargement automatique par balise
`` (15 s) -- pas de JS.
- [x] GUI (`gui/ecran_vue_competiteur.py`) : bouton démarrer/arrêter
explicite (pas de démarrage automatique). L'état du serveur
(thread + instance) vit sur `FenetrePrincipale`, pas sur l'écran
lui-même -- l'écran est recréé à chaque navigation, le serveur
doit lui survivre. ⚠️ **rendu non vérifié** -- cycle
démarrer/requête/arrêter vérifié réellement hors GUI.
- [x] Identité visuelle FletchTime + bilingue FR/EN -- demande de
l'utilisateur, `theme.css` (fourni, système de conception partagé
FletchTime/FletchScore) copié tel quel dans
`src/fletchscore/web/`, jamais dupliqué dans le code Python.
`classement.css` ajouté à côté pour les tableaux (absents du
fichier source, extrait d'une page de config sans tableau).
Préférence langue/thème mémorisée par **cookie**, pas par
JavaScript -- cohérent avec le choix "pas de JS" déjà fait, et
survit naturellement au rechargement automatique périodique (un
état JS en mémoire ne survivrait pas à un rechargement complet).
Bascule via de simples liens `` vers un endpoint `/preference`
qui pose les cookies et redirige (302) -- aucun JavaScript sur
toute la page. 10 nouveaux tests (23 au total sur ce module),
**vérifiés réellement** : fichiers statiques servis (contenu
relu), cookie de préférence posé et respecté sur la page
suivante, aperçu HTML généré et relu ligne par ligne pour
confirmer un rendu cohérent (état "actif" des boutons, textes
dans la bonne langue, chemins de retour corrects).
Zéro écriture, donc zéro risque de sécurité nouveau -- rapide à sortir et
à faire tester par de vrais archers en salle.
### Token et sécurité (fusionné dans la v0.2 -- initialement prévu v0.3)
Ordre retenu (proposé, confirmé par l'utilisateur) : 1) Token/QR +
rattachement (fondation) 2) authentification organisateur 3) HTTPS
local. HTTPS repoussé à la v0.3 (voir "Points tranchés" dans le cahier
des charges) -- v0.2 n'a encore qu'une écriture à faible enjeu (une
demande de rattachement, pas un score), pas de raison de durcir avant
la vraie donnée sensible.
- [x] `Token` / `DemandeRattachement` -- backend complet.
`fletchscore/securite.py` (nouveau) : clé secrète serveur générée
au premier lancement, stockée dans `config/cle_secrete.txt`
(gitignoré, hors de la base SQLite -- récupérer le `.db` seul ne
suffit pas à reconstituer un token). `services.generer_token()`
(code court 6 caractères sans 0/O/1/I, secret aléatoire, HMAC-
SHA256 -- jamais le secret stocké en clair),
`verifier_token()` (`hmac.compare_digest`, jamais `==`),
`demander_rattachement()`/`lister_demandes_en_attente()`/
`valider_rattachement()`/`rejeter_rattachement()`. Le token n'est
généré qu'à la validation, jamais à la demande. 20 tests,
**vérifiés aussi en conditions réelles** (flux complet
demande → validation → vérification avec un vrai secret, un
mauvais secret bien rejeté).
- [x] Génération de QR code -- `qrcode>=7.4` ajoutée à `pyproject.toml`,
`gui/qr_code.py` avec le même mécanisme `skipUnless` que `fpdf2`
(bibliothèque non installable ici, pas de réseau). Le code court
reste affiché en toutes circonstances, avec ou sans QR (voir
cahier des charges, "QR code + code court en secours").
- [x] GUI organisateur : `gui/ecran_rattachement.py`, nouvel écran
"Demandes d'accès" -- liste des demandes en attente par
compétition, boutons Valider/Rejeter. Une fenêtre éphémère
(`CTkToplevel`) affiche le code + QR généré juste après
validation -- jamais conservée à l'écran en permanence, puisque
le secret ne sera plus jamais récupérable une fois cette fenêtre
fermée (voir `services.generer_token`). ⚠️ **rendu non vérifié**
-- comme toute la GUI, pas d'affichage disponible ici.
- [x] Endpoint web compétiteur pour soumettre une demande de
rattachement -- `GET /rattachement/` (recherche
par nom parmi tous les inscrits de la compétition, insensible à
la casse) puis `POST` (vrai formulaire HTML, sans JavaScript) qui
appelle `services.demander_rattachement()`. Pas de rafraîchissement
automatique sur cette page (contrairement au classement) : un
compétiteur en train de chercher ou remplir un formulaire ne doit
pas se faire couper. 12 tests, dont 4 d'intégration avec un
**vrai POST HTTP qui crée réellement une demande en base**,
vérifiée après coup par une connexion séparée -- pas seulement
que la page de confirmation s'affiche. **Bug de texte repéré en
relisant une page générée réellement** (pas en test unitaire) :
le lien de retour disait "Toutes les compétitions" en pointant en
fait vers une compétition précise -- corrigé avec un texte dédié.
- [x] Authentification organisateur -- mot de passe hashé (PBKDF2-
SHA256, stdlib, pas de dépendance compilée à faire fonctionner
sur Pydroid) dans `config/auth.toml`, déjà réservé dans
`.gitignore` depuis le début. **Optionnelle** : sans mot de passe
défini, FletchScore s'ouvre directement (comportement historique
inchangé). `fletchscore/auth.py` (nouveau module) :
`definir_mot_de_passe()`/`verifier_mot_de_passe()`/
`supprimer_mot_de_passe()`, sel aléatoire à chaque définition.
Écran GUI "Sécurité" (définir, changer, supprimer -- la
suppression et le changement redemandent le mot de passe actuel).
Fenêtre de connexion bloquante au lancement si un mot de passe
est configuré, avant que le reste de l'interface ne se construise.
11 tests, et le cycle complet (définir → vérifier → changer →
supprimer) **vérifié réellement**, pas seulement en tests
unitaires isolés.
- [x] Port du serveur web fixe et paramétrable -- demande de
l'utilisateur. `ConfigGui.http_port` (persisté dans
`config/gui.toml`, comme le thème), `--http-port` en CLI
(préremplit le port proposé, ne démarre jamais le serveur tout
seul -- reste une action explicite de l'organisateur, décision
déjà prise en v0.2). Champ de saisie sur l'écran "Vue
compétiteur", désactivé pendant que le serveur tourne (le
changer n'aurait aucun effet avant un arrêt/redémarrage). Un port
déjà occupé lève une `OSError` affichée proprement -- **vérifié
réellement** en faisant collisionner deux serveurs sur le même
port.
- [x] Écrans Accueil et Aide mis à jour -- demande de l'utilisateur.
L'aide **dans l'appli** (`gui/ecran_aide.py`) n'avait jamais
suivi l'ajout de la vue compétiteur et des demandes d'accès
(seule la doc Sphinx en ligne les décrivait) ; la description de
"Saisie des scores" y mentionnait encore des "volées", périmé
depuis la révision au score final. Les raccourcis de l'Accueil
(`_RACCOURCIS`) ont le même défaut corrigé.
- [x] Vérification d'identité côté organisateur, clarifiée --
`gui/ecran_rattachement.py` affichait seulement nom/prénom/id
fédéral, pas assez pour recouper contre une pièce d'identité. La
liste des demandes affiche maintenant aussi la date de naissance
et le club, et une note en tête d'écran rappelle explicitement que
la vérification reste un acte humain -- FletchScore n'a aucun
moyen de la faire à la place de l'organisateur, seulement
d'afficher ce qu'il connaît déjà pour aider à recouper.
- [x] Demande de rattachement déplacée sur la page d'accueil du
compétiteur -- demande de l'utilisateur. Un lien apparaît
maintenant directement sous chaque compétition listée en accueil
(plus besoin de d'abord ouvrir le classement global pour le
trouver) ; conservé aussi sur la page compétition, les deux
cohabitent sans conflit.
- [x] Page d'accueil du compétiteur plus accueillante -- message de
bienvenue + phrase d'intro (bilingue), avant la liste des
compétitions. Demande de l'utilisateur.
- [x] Saisie manuelle du code de confirmation -- une fois un
rattachement validé par l'organisateur, le compétiteur peut taper
son code à 6 caractères directement sur la page d'accueil
(`POST /code`) plutôt que de devoir scanner le QR. Nouvelle
fonction `services.verifier_code_court()`, **volontairement plus
faible que `verifier_token()`** (ne demande pas le secret complet)
-- acceptable maintenant (aucune donnée sensible en jeu), à
revoir avant la v0.3 si ce chemin sert un jour à transmettre un
score. 8 tests, dont un vrai `POST /code` avec un vrai token
généré, vérifié de bout en bout.
- [x] Révocation d'accès depuis la GUI -- demande de l'utilisateur.
`services.revoquer_acces()` (enveloppe `db.revoquer_token`, déjà
existante mais jamais appelée depuis aucune couche supérieure) et
`services.lister_tokens_actifs()` (nouveau,
`db.list_tokens_by_competition()` ajoutée). L'écran "Demandes
d'accès" a maintenant deux onglets (`CTkTabview`) : "Demandes en
attente" (inchangé) et "Accès actifs" (nouveau, bouton Révoquer
par ligne). 11 tests.
- [x] **Envoi de message à un compétiteur (ou à tous)** -- demandé par
l'utilisateur, cadré puis fait (bandeau sur l'accueil ET page
dédiée "Mes messages" ; historique persistant ; pas de suivi
lu/non lu, confirmé par l'utilisateur). Vraie nouvelle
fonctionnalité, contrairement au reste de cette session :
`models/message.py` (nouvelle entité, table `messages`),
`services.envoyer_message()`/`lister_messages_pour()`/
`lister_messages_envoyes()`.
**Sujet technique nouveau soulevé par cette fonctionnalité** :
pour qu'un message *ciblé* arrive à la bonne personne, le
navigateur du compétiteur doit "se souvenir" de qui il est après
confirmation d'un code -- un cookie en clair aurait permis à
n'importe qui de lire les messages de n'importe qui en modifiant
son cookie à la main. `services.signer_identite_competiteur()`/
`verifier_identite_signee()` : cookie de session **signé HMAC**
(même principe que les tokens), portant id fédéral + compétition
ensemble (un "mes messages" doit savoir pour quelle compétition).
Posé après un `POST /code` réussi, `HttpOnly`, 7 jours (le temps
d'un week-end de compétition). GUI : 3ᵉ onglet "Envoyer un
message" dans l'écran "Demandes d'accès" (destinataire = un
compétiteur actif ou "Tous", historique des envois). 20 tests,
dont un **test d'intégration décisif** : vrai `POST /code` → vrai
`Set-Cookie` reçu → ce cookie renvoyé sur un vrai `GET
/mes-messages` → les bons messages apparaissent -- pas seulement
les fonctions testées isolément.
Prérequis technique avant d'ouvrir la moindre écriture externe -- pas de
fonctionnalité visible en soi, mais indispensable avant la v0.3.
## v0.3 -- Proposition de score compétiteur
- [x] HTTPS local -- décalé ici définitivement (décision de
l'utilisateur), puis fait. Cadré avant de coder : `cryptography`
choisie plutôt qu'appeler `openssl` en CLI (présence incertaine
sur Pydroid) ou demander un certificat fourni par l'utilisateur
(plus de friction) -- confirmé par l'utilisateur malgré le risque
de compatibilité Pydroid non vérifiable ici. **Bonne surprise** :
contrairement à `fpdf2`/`qrcode`, `cryptography` s'est révélée
réellement disponible dans cet environnement de dev -- tous les
tests HTTPS tournent donc ici pour de vrai, pas seulement chez
l'utilisateur/en CI. `fletchscore/certificat_https.py` (nouveau) :
certificat auto-signé (RSA 2048, SHA-256, 10 ans de validité --
usage local, pas de raison de le faire tourner), généré une seule
fois puis réutilisé. `creer_serveur(..., https=True)` enveloppe
le socket déjà lié dans TLS. Case à cocher sur l'écran "Vue
compétiteur" (désactivée si `cryptography` absent, avec message
explicite), persistée comme le port. **Bug attrapé avant
livraison** : même piège que `_hash_token` déjà rencontré (un
argument par défaut figé à la définition de fonction ignore un
`mock.patch` sur l'attribut du module) -- corrigé en passant les
chemins explicitement. **Fuite de fichier intermittente non
totalement expliquée** pendant le développement (~1 fois sur une
dizaine de lancements complets de la suite) -- filet de sécurité
ajouté en fin de test plutôt que laissée sans réponse claire,
confirmé propre sur 5 relances après coup. 14 nouveaux tests, et
**vérifié réellement** : une vraie poignée de main TLS établie,
une vraie réponse HTTPS reçue, une connexion HTTP simple qui
échoue bien contre le serveur HTTPS.
- [x] Limitation de débit -- `fletchscore/limiteur_debit.py` (nouveau
module, fenêtre glissante en mémoire, aucune dépendance). Plus
stricte sur `POST /code` (10 tentatives / 5 min par IP) que sur
les autres écritures (`POST /rattachement`, `POST
/proposer-score`, 30 / 5 min) -- `/code` devine un secret (le
code court, ~30 bits), pas juste une action à limiter par
confort. Réponse HTTP 429 standard, avec en-tête `Retry-After`.
5 tests avec une horloge factice (fenêtre glissante testée
instantanément, sans vraies pauses de plusieurs minutes) + **1
test d'intégration décisif** : 10 vraies requêtes `POST /code`
passent, la 11e reçoit un vrai 429 -- pas seulement que
`LimiteurDebit` fonctionne en isolation, mais que le serveur
l'applique réellement.
- [x] `api/competiteur.py` (écriture -- proposition de score) --
cadré avec l'utilisateur avant de coder (3 questions : format
identique à la saisie organisateur -- total + X ; nécessite le
code d'accès confirmé au préalable, même session que "Mes
messages" ; la validation appelle directement
`services.saisir_score_final()`, le score proposé devient LE
score officiel). `services.proposer_score()` -- refuse
d'écraser un score déjà **validé** (seule l'organisateur peut le
corriger, écran Saisie), mais permet de reproposer librement tant
que rien n'est validé. Réutilise `StatutScore.PROPOSE`, déjà
prévu dans le modèle depuis la simplification du score en v0.1,
jamais branché jusqu'ici. Formulaire affiché sur la page de
l'épreuve, uniquement si le compétiteur est identifié (cookie de
session), inscrit à cette épreuve précise, et n'a pas déjà de
score officiel. **L'id fédéral vient exclusivement du cookie
signé, jamais d'un champ de formulaire** -- personne ne peut
proposer un score pour quelqu'un d'autre en modifiant du HTML.
- [x] File de validation côté organisateur --
`gui/ecran_propositions.py` (nouvel écran "Propositions de
score"), sélecteur d'épreuve, liste des propositions en attente,
Valider/Rejeter. `api/organisateur.py` reste vide -- toute la
validation se fait depuis la GUI organisateur existante, pas
besoin d'une API dédiée pour ça.
- [x] Flux complet : proposition -> validation -> score officiel --
`services.valider_score_propose()`/`rejeter_score_propose()`,
27 nouveaux tests, **vérifié réellement à deux niveaux** : (1)
bout en bout services (proposer -> lister -> valider -> le
classement passe de 0 à 270 points) et (2) bout en bout HTTP
(vrai `POST /code` -> vrai cookie -> vrai `POST /proposer-score`
-> vrai `Score` en base avec statut `propose`) -- pas seulement
des fonctions testées isolément.
- [x] Garde-fou contre les demandes/liens redondants -- signalé par
l'utilisateur ("si le code a déjà été donné, il ne devrait pas y
avoir de demande d'accès possible"). Sans ça, valider une
deuxième demande aurait émis un second token sans jamais révoquer
le premier : deux codes valides simultanés pour un seul
compétiteur. `services.demander_rattachement()` refuse désormais
si un accès valide existe déjà, ou si une demande est déjà en
attente -- mais reste possible après une révocation (un accès
révoqué ne doit pas bloquer indéfiniment). Le lien "Demander un
accès" et le formulaire de recherche disparaissent de l'accueil,
de la page compétition et de la page de rattachement elle-même
dès que le compétiteur est identifié pour cette compétition
précise (remplacés par un simple "Accès déjà confirmé") --
double protection : le backend refuse même si l'UI était
contournée, l'UI n'affiche même plus l'option pour éviter un clic
inutile. 11 nouveaux tests. **Vérifié réellement** : demande →
validation → nouvelle tentative de demande, refusée avec le bon
message.
- [x] Trois améliorations de l'accueil compétiteur -- signalées par
l'utilisateur après un test réel. (1) Le formulaire "J'ai un
code" disparaît désormais si une session est déjà identifiée
(même logique que le lien de rattachement masqué juste avant --
cohérence). (2) Message de bienvenue personnalisé ("Bonjour
Prénom Nom !") une fois identifié. (3) Statut par épreuve affiché
à côté de chaque lien quand identifié pour la bonne compétition :
"pas inscrit·e" / "inscrit·e" / "score en attente de validation"
/ "score validé : N pts" (`_statut_epreuve_pour()`, nouveau).
13 nouveaux tests, **vérifié réellement** sur une page complète
générée avec un compétiteur inscrit à une épreuve (score en
attente) et non inscrit à une autre -- les deux statuts corrects
côte à côte.
Jalon le plus sensible (premières écritures externes en compétition
réelle) -- à tester d'abord en interne/amical avant un vrai concours
homologué. **v0.3 complète.**
## Extension -- Import/export de compétitions
Question posée par l'utilisateur, absente du cahier des charges
initial : "importer/exporter des événements/compétitions" recouvre en
fait trois besoins distincts, tous jugés pertinents (pas un choix
exclusif) :
1. Sauvegarder/restaurer une compétition entière (archiver, transférer
d'une machine à une autre)
2. Réutiliser une épreuve type (nom, barème) d'une compétition à l'autre
sans tout retaper -- **priorité retenue en premier**
3. Exporter un paquet complet pour la fédération (définition + scores +
classement en un seul fichier)
Avancement :
- [x] **Besoin 2 -- modèles d'épreuve, backend** : `EpreuveTemplate`
(nom + barème, indépendant de toute compétition -- la date reste
propre à chaque épreuve, jamais dans le modèle).
`services.creer_template_epreuve()`,
`creer_template_depuis_epreuve()` (enregistrer une épreuve
existante comme modèle, nom personnalisable),
`creer_epreuve_depuis_template()` (ne duplique pas les
validations de `creer_epreuve()` -- une date hors des bornes de
la compétition reste refusée même via un modèle). 9 tests.
- [x] **Besoin 2 -- modèles d'épreuve, GUI** : sélecteur de modèle dans
le formulaire de création d'épreuve (préremplit nom + barème,
"(aucun modèle -- saisie libre)" par défaut) ; bouton "Enregistrer
comme modèle" sur chaque épreuve listée. ⚠️ **rendu non vérifié**
-- comme toute la GUI, pas d'affichage disponible ici
- [x] **Modèles de compétition (issue #25, un cran au-dessus des modèles
d'épreuve)** : `CompetitionTemplate`/`CompetitionTemplateEpreuve`
(bundle de plusieurs `(nom, bareme_id)`, toujours sans date, même
principe qu'`EpreuveTemplate`). `services.creer_template_competition()`,
`creer_template_depuis_competition()`, `creer_competition_depuis_template()`
(crée la compétition puis génère toutes ses épreuves en une fois --
délègue à `creer_competition()`/`creer_epreuve()`, ne duplique pas
leurs validations ; chaque épreuve générée prend `date_debut` comme
date par défaut, ajustable ensuite via `modifier_epreuve()`). GUI :
sélecteur de modèle dans le formulaire de création de compétition
(n'préremplit rien, juste mémorisé jusqu'à la soumission -- un
modèle de compétition ne porte ni nom ni dates), bouton "Enregistrer
comme modèle" sur chaque compétition listée. 23 tests (10 stockage +
13 services). **Rendu vérifié réellement** (Xvfb + capture d'écran,
scénario complet : enregistrer un modèle depuis une compétition à
deux épreuves, l'appliquer à une nouvelle compétition, confirmer les
épreuves générées avec les bons noms/barèmes/dates).
- [x] **Besoin 1 -- sauvegarde/restauration d'une compétition complète
(issue #7, 2026-08-14)**. `io/sauvegarde_competition.py` : format
JSON auto-descriptif (pas de dépendance externe), autoportant --
en plus de la compétition/épreuves/inscriptions/scores, embarque
aussi les clubs/compétiteurs/barèmes référencés, sans quoi
réimporter sur une machine qui ne les connaît pas déjà échouerait
sur des clés étrangères manquantes. `exporter_competition()`,
`importer_competition()` -- refuse si l'id de compétition existe
déjà (pas de fusion, un import réussi ou pas du tout), réutilise
(sans dupliquer) les clubs/compétiteurs/barèmes déjà présents côté
cible. `db.importer_donnees_competition()` écrit tout en une seule
transaction (même pattern que `db.anonymiser_competiteur()`,
issue #37) -- une compétition à moitié restaurée serait pire
qu'un échec net. Volontairement hors périmètre : tokens, demandes
de rattachement, procurations, messages -- état d'accès propre à
la machine d'origine, pas des données "de compétition". GUI :
bouton **📦** sur chaque compétition listée (export), bouton
**📥 Restaurer** dans l'en-tête de la colonne. 8 tests (export,
restauration sur base neuve, réutilisation de référentiels
partagés, conflit d'id, version de format invalide, classement
recalculable après restauration). Vérifié réellement (Xvfb) :
sauvegarde et restauration déclenchées via les vrais boutons GUI
(popup de chemin simulé), sur une vraie seconde base "cible"
construite à la volée pour simuler une autre machine.
- [ ] Besoin 3 -- export fédération tout-en-un
## Extension -- Modification de compétitions/épreuves existantes
Manque signalé par l'utilisateur en cours de test réel : seules la
création et la liste existaient, aucun moyen de corriger une erreur de
saisie sans passer par un script Python à la main.
- [x] `services.modifier_competition()` -- mêmes règles que
`creer_competition()`, plus une vérification propre à la
modification : rétrécir les dates ne doit pas laisser une épreuve
existante hors des nouvelles bornes. Le statut n'est pas
modifiable ici (clôturer une compétition est une action distincte).
- [x] `services.modifier_epreuve()` -- mêmes règles que
`creer_epreuve()`, plus une protection : le barème ne peut plus
être changé une fois qu'une volée a été saisie pour cette épreuve
(`storage.epreuve_a_des_scores()`) -- les numéros de série/volée
déjà enregistrés ne correspondraient plus forcément au nouveau
barème. 16 tests au total pour les deux fonctions.
- [x] GUI (`ecran_competitions.py`) : bouton "Modifier" sur chaque
compétition/épreuve listée, formulaire préreempli, bouton
"Annuler" pour sortir du mode édition sans enregistrer. ⚠️
**rendu non vérifié** -- comme toute la GUI, pas d'affichage
disponible ici.
## Extension -- Accueil et aide dans la GUI
Demande de l'utilisateur en cours de test réel : la fenêtre s'ouvrait
directement sur l'écran Compétitions, sans vue d'ensemble ni aide
accessible depuis l'appli.
- [x] Écran « Accueil » (nouvel écran par défaut à l'ouverture) :
message de bienvenue, résumé chiffré (nb compétitions,
compétiteurs, épreuves), dernière épreuve en date, raccourcis vers
les 4 sections. `services.resumer_accueil()` -- "dernière
activité" interprétée comme l'épreuve la plus récente par date,
pas un horodatage d'action réelle (rien dans le modèle ne trace
"quand" une action a eu lieu). 5 tests.
- [x] Écran « Aide » : résumé du mode d'emploi de chaque section
directement dans la GUI, plus un bouton qui ouvre la documentation
complète dans le navigateur par défaut (`webbrowser.open`, stdlib).
Contenu statique, rien à tester au-delà de la syntaxe.
- Les deux : ⚠️ **rendu non vérifié** -- comme toute la GUI, pas
d'affichage disponible ici.
## Extension -- Saisie du score final, pas volée par volée
Révision d'un choix initial, proposée par l'utilisateur : la saisie
flèche par flèche/volée par volée s'est avérée trop lourde face à
l'usage réel -- les scores sont déjà totalisés à la main sur la feuille
de match pendant le tir, le rôle de FletchScore est d'enregistrer ce
résultat et de classer, pas de rejouer le calcul flèche par flèche.
- [x] `models/score.py` -- simplifié à `total` + `nombre_x` (une ligne
par Inscription, contrainte UNIQUE en base -- plus de
`numero_serie`/`numero_volee`/`valeurs`).
- [x] `scoring/volee.py` -- **supprimé** (`normaliser_volee` et la
validation flèche par flèche n'ont plus de raison d'être).
- [x] `services.saisir_score_final()` remplace `saisir_volee()` -- borne
le total à `bareme.score_max`, le nombre de X à
`bareme.total_flèches`, refuse un X non nul si le barème n'en
utilise pas.
- [x] `gui/ecran_saisie.py` -- réécrit : deux champs (score total,
nombre de X) au lieu du formulaire volée par volée avec sélecteurs
série/volée et champs par flèche.
- [x] Tous les tests concernés mis à jour (`test_storage.py`,
`test_models.py`, `test_scoring_classement.py`, `test_services.py`,
les 3 fichiers `test_export_*.py`, `scripts/demo_v0_1.py`).
**Bénéfice inattendu** : ça lève le blocage sur l'Animal Round et les
rounds 3-D (voir cahier des charges, section rounds) -- leur système de
score complexe (kill/wound, arrêt au premier impact) ne pose plus
problème puisque FletchScore n'a plus besoin de le modéliser en détail,
juste de connaître le `score_max` possible pour borner la saisie.
⚠️ **GUI non vérifiée** -- comme toujours, à confirmer par un vrai
lancement.
## Extension -- Classement global sur plusieurs épreuves
Demande de l'utilisateur : exporter la totalité d'un concours (toutes
ses épreuves), avec un classement global -- une colonne par épreuve,
une colonne total.
- [x] `scoring.classement_global()` -- cumule les totaux d'un
compétiteur sur toutes les épreuves d'une compétition, trie sur le
total global uniquement (pas de départage au X inventé entre
épreuves à barèmes potentiellement différents). 8 tests.
- [x] `services.classement_global_competition()` -- rassemble les
compétiteurs inscrits à au moins une épreuve, complète à 0 les
épreuves où ils ne sont pas inscrits/n'ont pas de score validé.
5 tests.
- [x] `io/export/csv.py` et `io/export/excel.py` --
`exporter_classement_global_csv()`/`_excel()`, colonnes par
épreuve identifiées par nom + date (évite une collision si deux
épreuves portent le même nom). 9 tests au total. **Vérifié
réellement de bout en bout** (pas seulement les tests unitaires) :
compétition à 2 épreuves, un compétiteur inscrit aux deux, un
inscrit à une seule -- export CSV et Excel produits et relus.
- [x] `io/export/pdf.py` -- `exporter_classement_global_pdf()`, page en
**paysage** (pas portrait comme l'export par épreuve) : le nombre
de colonnes dépend du nombre d'épreuves, ça laisse plus de place.
Largeur de colonne par épreuve avec un plancher à 20mm -- au-delà
d'une poignée d'épreuves, ça se resserre plutôt que de planter
(testé explicitement avec 10 épreuves). 4 tests, en attente comme
le reste des tests PDF (`fpdf2` non installable ici)
- [x] Bouton GUI pour le classement global (CSV, Excel et PDF) --
nouvelle section dans `ecran_classement.py` (sélecteur de
compétition dérivé de `lister_epreuves_toutes()`, dédupliqué --
vérifié réellement) avec ses propres boutons et son propre label
d'erreur, distincts de la section export par épreuve. ⚠️ **rendu
non vérifié** -- signalé par l'utilisateur ("sur quel bouton
appuyer ?") qui a eu raison de demander, il n'existait pas encore
## v0.4 -- Finition
- [ ] Format d'export fédération figé (dépend du modèle Excel imposé ou
non -- point ouvert, voir cahier des charges)
- [x] Doc "premier club" façon onboarding FletchTime -- avancée avant
l'heure (demande de l'utilisateur, "avant que ça ne dérive")
plutôt que d'attendre v0.4. `docs/guide-utilisateur/` : 4 pages
(installation, premiers pas, écrans, dépannage), toctree déjà
scaffoldé mais 4 fichiers manquaient encore (le build Sphinx
aurait échoué avec des liens vers des pages inexistantes). Contenu
du dépannage tiré des vrais problèmes rencontrés pendant le
développement, pas de cas inventés. Deux liens vers un
"guide développeur" (`.../dev-guide/index.html`) trouvés cassés en
vérifiant -- cette page n'a jamais existé, référencée à tort dans
`CONTRIBUTING.md` depuis longtemps ; corrigés dans mes nouveaux
fichiers (pointent vers `CONTRIBUTING.md` sur GitHub à la place),
**mais les 4 occurrences dans `CONTRIBUTING.md` lui-même restent à
corriger** -- pas fait cette fois, hors du périmètre demandé.
**Non vérifié par une vraie construction Sphinx** (toujours pas
installable ici, pas de réseau) -- seulement des vérifications
statiques (soulignements de titres, références `:doc:` toutes
résolues).
- [x] Cahier des charges revérifié et recalé sur l'état réel --
`perimetre.rst` mentionnait encore "volée par volée" (périmé
depuis la révision du score final) ; `architecture.rst`
entièrement réécrite (décrivait encore `api/` comme fonctionnel
et ne mentionnait ni `services.py`, ni les 6 écrans `gui/`, ni
`io/export/` -- dérive accumulée au fil de tous les incréments) ;
`modele-donnees.rst` complétée avec `EpreuveTemplate` (absente) ;
`securite.rst` clarifiée en tête comme un plan v0.2/v0.3, pas
l'état actuel.
- [ ] Durcissement suite aux retours terrain
## Réorganisation GUI (chantier esthétique)
Demande de l'utilisateur, une fois la v0.3 quasiment close : 10 écrans
étaient devenus trop nombreux et se chevauchaient par endroits.
Cadré avant de coder (plusieurs allers-retours sur le regroupement
exact) plutôt que de deviner -- voir la conversation pour le
raisonnement complet.
- [x] `gui/ecran_saisie.py` fusionné avec l'ancien
`gui/ecran_propositions.py` -- deux onglets ("Saisie manuelle",
"Propositions en attente") : un score entre dans le système par
l'un ou par l'autre, décision de l'utilisateur ("les propositions
de score devraient être dans la saisie").
- [x] Nouveau `gui/ecran_connexions.py` -- fusion des anciens
`gui/ecran_vue_competiteur.py` (contrôles serveur) et
`gui/ecran_rattachement.py` (demandes/accès/messages) en un seul
écran "Connexions compétiteurs" : contrôles serveur en haut, puis
trois onglets (Demandes en attente, Accès actifs, Messages).
Décision explicite de l'utilisateur de fusionner le contrôle du
serveur avec la messagerie plutôt que de les séparer ("le serveur
et les messages vont ensemble").
- [x] `gui/ecran_securite.py` renommé `gui/ecran_mot_de_passe.py`
(classe `EcranMotDePasse`) -- "Sécurité" prêtait à confusion une
fois "Connexions compétiteurs" en place (on pourrait croire que
ça parle de la sécurité de la vue web, pas du mot de passe de
l'appli).
- [x] `ecran_accueil.py` (raccourcis) et `ecran_aide.py` (mode
d'emploi intégré) mis à jour en conséquence. **Bug repéré et
corrigé en relisant** : une fausse syntaxe markdown (`**gras**`)
s'était glissée dans le nouveau texte d'aide -- `CTkLabel`
n'interprète aucun texte enrichi, ça se serait affiché en toutes
lettres avec les astérisques.
- [x] Guide utilisateur (`docs/guide-utilisateur/ecrans.rst`) réécrit
pour les 8 écrans finaux.
Résultat : 10 écrans -> 8, chacun avec une frontière claire (qui a
accès / qu'est-ce qu'on leur dit / quels scores valider ne se marchent
plus dessus). ⚠️ **Rendu GUI non vérifié**, comme toujours -- à
confirmer par un vrai lancement.
## Compléments post-v0.3
- [x] **Bug trouvé en vérifiant la doc** : `Score.propose_par_id_federal`
existait depuis le chantier procuration précédent mais n'était
affiché nulle part dans l'écran organisateur -- tout l'intérêt de
ce champ était pourtant de laisser l'organisateur juger la
fiabilité d'une proposition avant de la valider. `gui/
ecran_saisie.py` affiche maintenant "proposé par [nom]" quand ce
n'est pas la personne cible elle-même qui a soumis.
- [x] **Procuration -- proposer un score au nom d'un autre compétiteur**.
Demandé par l'utilisateur : sur un pas de tir, une seule personne
note souvent les scores de tout le groupe. Cadré avant de coder
(2 questions) : ouvert à n'importe qui inscrit à la compétition
(pas restreint à la même épreuve), mais **toujours soumis à
validation par l'organisateur** avant de produire le moindre
effet (même principe que `DemandeRattachement`) ; et le
proposant réel doit être tracé et affiché, pas seulement pour
qui, pour que l'organisateur puisse juger la fiabilité en
validant.
`models/procuration.py` (nouveau) : entité `Procuration`, enum
`StatutProcuration` (EN_ATTENTE/VALIDEE/REJETEE/REVOQUEE).
`Score.propose_par_id_federal` (nouveau champ) : qui a réellement
soumis, distinct de pour qui (l'inscription). `services.py` :
`demander_procuration()`/`valider_procuration()`/
`rejeter_procuration()`/`revoquer_procuration()`, et
`proposer_score()` réécrite pour accepter un `id_federal_cible`
optionnel (soi-même par défaut). Table `procurations`
**volontairement sans contrainte UNIQUE stricte** en base -- une
contrainte aurait bloqué une nouvelle demande après un rejet ; la
logique de doublon vit dans `services.py`, comme pour
`DemandeRattachement`. ⚠️ **Changement de schéma sur `scores`**
(nouvelle colonne `propose_par_id_federal`) -- comme d'habitude
sur ce projet, pas de migration automatique : supprimer
`fletchscore.db` et relancer si la base existante date d'avant ce
changement.
Côté web : `page_procuration()` (recherche + demande, exclut le
demandeur de la liste), formulaire de proposition de score
(`_section_proposer_score()`) réécrit pour lister tous les
candidats possibles (soi-même + chaque mandant inscrit à cette
épreuve précise) -- un `