fletchtime 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fletchtime-0.2.0/src/fletchtime.egg-info → fletchtime-0.2.1}/PKG-INFO +1 -1
- {fletchtime-0.2.0 → fletchtime-0.2.1}/docs/architecture.md +55 -6
- {fletchtime-0.2.0 → fletchtime-0.2.1}/docs/roadmap.md +37 -27
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/__main__.py +4 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/_version.py +3 -3
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/engine/engine.py +22 -1
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/gui.py +52 -2
- fletchtime-0.2.1/src/fletchtime/logging_setup.py +56 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/server/match_server.py +37 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/server/ws_server.py +8 -2
- {fletchtime-0.2.0 → fletchtime-0.2.1/src/fletchtime.egg-info}/PKG-INFO +1 -1
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime.egg-info/SOURCES.txt +1 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime.egg-info/scm_file_list.json +1 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime.egg-info/scm_version.json +2 -2
- {fletchtime-0.2.0 → fletchtime-0.2.1}/tests/test_engine.py +15 -2
- {fletchtime-0.2.0 → fletchtime-0.2.1}/tests/test_match_server.py +116 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/.github/workflows/build.yml +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/.github/workflows/docs.yml +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/.github/workflows/test.yml +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/.gitignore +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/CONTRIBUTING.md +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/LICENSE +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/README.md +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/config/app.toml +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/config/flint.toml +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/config/indoor.toml +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/demo.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/docs/_static/logo.svg +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/docs/api-reference.md +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/docs/conf.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/docs/dev-guide/index.md +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/docs/index.md +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/docs/requirements.txt +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/docs/specifications.md +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/docs/user-guide/index.md +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/fletchtime.spec +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/pyproject.toml +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/run_server.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/run_tests.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/scripts/generate_classic_sounds.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/setup.cfg +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/__init__.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/engine/__init__.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/engine/models.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/engine/modes/__init__.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/engine/modes/base.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/engine/modes/flint.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/engine/modes/indoor.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/engine/sequence.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/engine/turn_modes.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/runtime.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/server/__init__.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/server/config_store.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/server/http_static.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/__init__.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/banners/README.md +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/club/README.md +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/gui/theme.json +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/sounds/packs/README.md +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/sounds/packs/classic/countdown_tick.wav +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/sounds/packs/classic/emergency_end.wav +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/sounds/packs/classic/emergency_start.wav +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/sounds/packs/classic/end_of_match.wav +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/sounds/packs/classic/end_of_volee.wav +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/sounds/packs/classic/pause_end.wav +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/sounds/packs/classic/pause_start.wav +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/sounds/packs/classic/prep_start.wav +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/sounds/packs/classic/shoot_start.wav +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/sounds/packs/classic/warning_orange.wav +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/targets/flint_20cm_4spot.jpg +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/targets/flint_35cm_1spot.jpg +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/targets/indoor_compound.jpg +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/targets/indoor_recurve.jpg +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/config.html +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/control.html +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/display.html +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/i18n.js +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/index.html +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/logo.svg +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/manual.html +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/screenshots/config.png +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/screenshots/control.png +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/screenshots/display.png +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/screenshots/index.png +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/theme.js +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime.egg-info/dependency_links.txt +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime.egg-info/entry_points.txt +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime.egg-info/requires.txt +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime.egg-info/top_level.txt +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/tests/test_config_store.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/tests/test_flint_mode.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/tests/test_indoor_mode.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/tests/test_runtime.py +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/web/assets/banners/README.md +0 -0
- {fletchtime-0.2.0 → fletchtime-0.2.1}/web/assets/sounds/packs/README.md +0 -0
|
@@ -343,6 +343,41 @@ confirmer le rendu et l'ergonomie tactile -- voir aussi le piège
|
|
|
343
343
|
PyInstaller/`customtkinter` documenté dans {doc}`dev-guide/index`.
|
|
344
344
|
```
|
|
345
345
|
|
|
346
|
+
## Journal applicatif persistant
|
|
347
|
+
|
|
348
|
+
En plus du journal affiché dans la fenêtre graphique (en mémoire,
|
|
349
|
+
perdu à la fermeture -- voir `fletchtime.gui._QueueWriter`),
|
|
350
|
+
`fletchtime.logging_setup.configure_logging` écrit un fichier de journal
|
|
351
|
+
persistant (`<données du club>/logs/fletchtime.log`, avec rotation --
|
|
352
|
+
1 Mo par fichier, 5 fichiers conservés) : utile pour comprendre après
|
|
353
|
+
coup ce qui s'est passé pendant un concours, sans dépendre de la mémoire
|
|
354
|
+
volatile de la fenêtre.
|
|
355
|
+
|
|
356
|
+
`MatchServer` y journalise :
|
|
357
|
+
- chaque commande reçue (`next`, `stop`, `start_indoor`...), avec la lane
|
|
358
|
+
d'origine si connue ;
|
|
359
|
+
- chaque connexion/déconnexion, avec le nombre de clients restants ;
|
|
360
|
+
- les pertes de connexion réseau détectées (WiFi, mise en veille...),
|
|
361
|
+
distinguées d'une fermeture normale ;
|
|
362
|
+
- les messages malformés reçus (JSON invalide) ;
|
|
363
|
+
- les transitions d'état significatives (`prep_start`, `shoot_start`,
|
|
364
|
+
`warning_orange`, `end_of_volee`, `pause_start`/`pause_end`,
|
|
365
|
+
`emergency_start`/`emergency_end`, `end_of_match`), avec le contexte
|
|
366
|
+
(unité, volée, tour, phase) -- qu'elles soient déclenchées naturellement
|
|
367
|
+
par le décompte ou manuellement (`next`, `goto`...), puisque les deux
|
|
368
|
+
passent par le même mécanisme de collecte d'événements. `countdown_tick`
|
|
369
|
+
est volontairement exclu (se déclenche une fois par seconde dans les
|
|
370
|
+
dernières secondes de chaque volée -- noierait le journal sans valeur
|
|
371
|
+
diagnostique ajoutée).
|
|
372
|
+
|
|
373
|
+
```{important}
|
|
374
|
+
Le mot de passe (action `authenticate`) n'est **jamais** journalisé -- le
|
|
375
|
+
code ne journalise jamais `data` tel quel, seulement le nom de l'action
|
|
376
|
+
et la lane, précisément pour éviter qu'un secret ne traîne en clair dans
|
|
377
|
+
un fichier qui persiste sur disque. Vérifié par un test dédié
|
|
378
|
+
(`test_password_never_appears_in_logs`).
|
|
379
|
+
```
|
|
380
|
+
|
|
346
381
|
## Récupération après un plantage ou redémarrage du serveur
|
|
347
382
|
|
|
348
383
|
Avant ce mécanisme, un plantage ou un redémarrage du serveur en plein
|
|
@@ -398,15 +433,29 @@ comparable à travers un redémarrage du processus.
|
|
|
398
433
|
`wallclock_deadline` vaut `None` pendant une pause/urgence (le décompte
|
|
399
434
|
est gelé, aucune échéance à calculer) : `time_left` reste alors la
|
|
400
435
|
valeur figée telle quelle, sans recalcul.
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
```{important}
|
|
439
|
+
**Si l'échéance est déjà dépassée à la restauration** (le temps
|
|
440
|
+
réellement écoulé pendant l'indisponibilité dépasse ce qui restait sur
|
|
441
|
+
l'étape en cours), le match est marqué **terminé** plutôt que de figer
|
|
442
|
+
`time_left` à 0. Sans ça, le tick normal suivant (`MatchEngine.tick()`,
|
|
443
|
+
voir plus haut) aurait déclenché son rattrapage automatique -- pensé
|
|
444
|
+
pour un simple ralentissement passager du serveur, pas une coupure de
|
|
445
|
+
plusieurs minutes -- et avancé à l'étape suivante avec une durée
|
|
446
|
+
fraîche : le chrono semblait alors "redémarrer" du point de vue des
|
|
447
|
+
archers, alors qu'en réalité bien plus de temps s'était écoulé que ce
|
|
448
|
+
que l'étape en cours pouvait raisonnablement absorber. Une coupure trop
|
|
449
|
+
longue pour être absorbée dans l'étape en cours demande donc une
|
|
450
|
+
intervention explicite du responsable du chronométrage (`restart`/
|
|
451
|
+
`goto`) plutôt qu'une reprise automatique qui devinerait une position
|
|
452
|
+
dans une coupure d'une durée arbitraire.
|
|
453
|
+
```
|
|
401
454
|
|
|
402
455
|
La sauvegarde à chaque tick (plutôt que périodiquement, ex. toutes les
|
|
403
456
|
5s) garde malgré tout sa valeur avec ce mécanisme : elle limite la
|
|
404
|
-
fenêtre pendant laquelle un
|
|
405
|
-
|
|
406
|
-
plantage -- l'échéance recalculée donne un temps exact pour l'étape
|
|
407
|
-
*enregistrée*, mais ne devine jamais qu'une étape différente aurait dû
|
|
408
|
-
être active entre-temps.
|
|
409
|
-
```
|
|
457
|
+
fenêtre pendant laquelle un changement d'étape aurait pu se produire
|
|
458
|
+
juste avant un plantage sans jamais être capturé dans l'instantané.
|
|
410
459
|
|
|
411
460
|
## Fonctionnement dégradé en cas de coupure réseau
|
|
412
461
|
|
|
@@ -73,27 +73,41 @@ dépendent d'extensions compilées (Rust/C) peu fiables sur Pydroid 3, voir
|
|
|
73
73
|
|
|
74
74
|
## Backlog — à discuter / non encore programmé dans une étape précise
|
|
75
75
|
|
|
76
|
+
- ~~**Journal applicatif persistant**~~ -- fait : `MatchServer` journalise
|
|
77
|
+
désormais chaque commande reçue, (dé)connexion, perte de connexion
|
|
78
|
+
réseau et message malformé dans un fichier avec rotation
|
|
79
|
+
(`logs/fletchtime.log`), en plus du journal déjà affiché dans la
|
|
80
|
+
fenêtre (en mémoire, perdu à la fermeture). Voir {doc}`architecture`,
|
|
81
|
+
section dédiée -- vérifié que le mot de passe ne s'y retrouve jamais.
|
|
76
82
|
- **Fenêtre graphique -- améliorations restant à faire** :
|
|
77
83
|
- Paramétrage du serveur (ports, etc.) et de l'adresse IP exposée sur le
|
|
78
84
|
réseau local, directement depuis la fenêtre plutôt que fichiers de
|
|
79
85
|
config seuls.
|
|
80
|
-
- Affichage des adresses exposées
|
|
81
|
-
fenêtre elle-même,
|
|
82
|
-
|
|
86
|
+
- ~~Affichage des adresses exposées~~ -- fait : champ dédié (adresse
|
|
87
|
+
d'accueil) dans la fenêtre elle-même, en plus des boutons de
|
|
88
|
+
raccourci -- utile pour retaper l'adresse à la main sur un appareil
|
|
89
|
+
sans bouton de raccourci. Non vérifié visuellement si le texte reste
|
|
90
|
+
bien sélectionnable/copiable une fois le champ désactivé (`CTkEntry`
|
|
91
|
+
n'a pas de vrai état "readonly" contrairement à `ttk.Entry`).
|
|
83
92
|
- Choix du thème clair/sombre dans la fenêtre -- actuellement forcé en
|
|
84
93
|
sombre (`ctk.set_appearance_mode("dark")`), cohérent avec
|
|
85
94
|
`display.html` mais pas ajustable par l'utilisateur comme le sont les
|
|
86
95
|
autres pages web.
|
|
87
|
-
- Couleur du bouton Quitter à revoir
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
- ~~Couleur du bouton Quitter à revoir~~ -- fait : couleur pleine
|
|
97
|
+
(`#3a4354`) au lieu de transparent+contour, visible sans être
|
|
98
|
+
alarmante (distincte du rouge d'Arrêter et du doré de Démarrer).
|
|
99
|
+
- ~~**Récupération après plantage/redémarrage du serveur**~~ -- fait et
|
|
100
|
+
**confirmé en conditions réelles avec la v0.2.0** (synchronisation du
|
|
101
|
+
temps testée, plus de retour en arrière du chrono ; reconnexion
|
|
102
|
+
vérifiée) : `MatchServer` persiste un instantané JSON de l'état du
|
|
103
|
+
match (`config/match_state.json`, jamais versionné) à chaque commande
|
|
104
|
+
qui change l'état et périodiquement pendant le décompte, et tente une
|
|
92
105
|
restauration silencieuse à la construction -- avant ça, un plantage en
|
|
93
106
|
plein match perdait toute la progression (série, volée, temps écoulé).
|
|
94
107
|
Voir {doc}`architecture`, section dédiée, pour le détail complet
|
|
95
|
-
(
|
|
96
|
-
|
|
108
|
+
(échéance en horloge murale pour un temps exact quelle que soit la
|
|
109
|
+
durée de l'indisponibilité, écriture atomique, repli silencieux sur un
|
|
110
|
+
démarrage normal si la reprise échoue).
|
|
97
111
|
- ~~**Revamping du logo**~~ -- fait : nouveau logo cadran/flèche noir-or-blanc,
|
|
98
112
|
wordmark "FletchTime" bicolore Fletch/Time, cohérent avec le thème clair/sombre.
|
|
99
113
|
- ~~**Sécurisation cyber**~~ -- en partie fait : mot de passe optionnel
|
|
@@ -118,20 +132,16 @@ dépendent d'extensions compilées (Rust/C) peu fiables sur Pydroid 3, voir
|
|
|
118
132
|
confirmée) -- voir le détail de l'investigation dans
|
|
119
133
|
{doc}`dev-guide/index`. Pas de besoin utilisateur macOS avéré à ce jour ;
|
|
120
134
|
à reconsidérer si ça change.
|
|
121
|
-
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
d'
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
précédente (ex. v0.1.2) si le code a changé depuis. À terme : une doc
|
|
135
|
-
par tag de version (ex. `/v0.1.2/`, `/v0.1.3/`) en plus d'une version
|
|
136
|
-
`/latest/`, avec un sélecteur de version dans le thème Furo (voir
|
|
137
|
-
`sphinx-multiversion` ou équivalent).
|
|
135
|
+
- ~~**Web app côté client (PWA)**~~ -- abandonné délibérément : pas jugé
|
|
136
|
+
utile pour l'usage réel du club. (Aurait transformé
|
|
137
|
+
`control.html`/`display.html` en Progressive Web App -- installation
|
|
138
|
+
sur écran d'accueil, mise en cache des fichiers statiques.)
|
|
139
|
+
- ~~**Documentation Sphinx versionnée**~~ -- abandonné délibérément : les
|
|
140
|
+
archives `.tar.gz` jointes à chaque Release GitHub suffisent pour
|
|
141
|
+
consulter la doc d'une version précédente, pas besoin d'un système de
|
|
142
|
+
versionnage complet côté GitHub Pages.
|
|
143
|
+
- ~~**Termux / lancer FletchTime sur Android sans Pydroid**~~ -- abandonné
|
|
144
|
+
délibérément pour l'instant : usage réel actuel = téléphone en simple
|
|
145
|
+
client (accès réseau local), PC pour héberger le serveur. Pas de besoin
|
|
146
|
+
d'exécuter le serveur lui-même sur Android dans ces conditions. À
|
|
147
|
+
reconsidérer si l'usage change.
|
|
@@ -28,6 +28,7 @@ from importlib import resources
|
|
|
28
28
|
from pathlib import Path
|
|
29
29
|
|
|
30
30
|
from fletchtime import __version__
|
|
31
|
+
from fletchtime.logging_setup import configure_logging
|
|
31
32
|
from fletchtime.runtime import ServerRuntime
|
|
32
33
|
|
|
33
34
|
HTTP_PORT = 8000
|
|
@@ -152,6 +153,9 @@ def _run_headless() -> None:
|
|
|
152
153
|
ensure_directories(data_root, app_web_dir)
|
|
153
154
|
assets_dir = data_root / "web" / "assets"
|
|
154
155
|
|
|
156
|
+
log_file = configure_logging(data_root / "logs")
|
|
157
|
+
print(f"Journal détaillé / Detailed log: {log_file}")
|
|
158
|
+
|
|
155
159
|
_print_banner(local_ip(), data_root)
|
|
156
160
|
|
|
157
161
|
runtime = ServerRuntime(str(app_web_dir), str(assets_dir), HTTP_PORT, WS_PORT)
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 1)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g4c8543661'
|
|
@@ -71,7 +71,28 @@ class MatchEngine:
|
|
|
71
71
|
# ce cas, le temps reste gelé tel quel.
|
|
72
72
|
deadline = restore.get("wallclock_deadline")
|
|
73
73
|
if deadline is not None:
|
|
74
|
-
|
|
74
|
+
remaining = deadline - time.time()
|
|
75
|
+
if remaining <= 0:
|
|
76
|
+
# Le temps réellement écoulé pendant l'indisponibilité
|
|
77
|
+
# dépasse ce qui restait sur cette étape -- ne PAS se
|
|
78
|
+
# contenter de figer time_left à 0 : le tick normal
|
|
79
|
+
# suivant aurait alors déclenché le rattrapage
|
|
80
|
+
# automatique de tick() (comportement volontaire et
|
|
81
|
+
# préexistant, pensé pour un simple ralentissement
|
|
82
|
+
# passager, pas une coupure de plusieurs secondes/
|
|
83
|
+
# minutes) et avancé à l'étape suivante avec une durée
|
|
84
|
+
# fraîche -- exactement ce qui ressemblait à "le
|
|
85
|
+
# chrono repart" du point de vue des archers. Une
|
|
86
|
+
# coupure trop longue pour être absorbée dans l'étape
|
|
87
|
+
# en cours marque le match terminé : au responsable du
|
|
88
|
+
# chronométrage de décider explicitement de la suite
|
|
89
|
+
# (restart/goto), plutôt qu'une reprise automatique
|
|
90
|
+
# qui devine une position dans une coupure qui a pu
|
|
91
|
+
# durer n'importe combien de temps.
|
|
92
|
+
self._finished = True
|
|
93
|
+
self._time_left = 0.0
|
|
94
|
+
else:
|
|
95
|
+
self._time_left = remaining
|
|
75
96
|
# Pas d'appel à _emit_current_step_event() ici, volontairement :
|
|
76
97
|
# une reprise après crash doit être silencieuse (pas de
|
|
77
98
|
# prep_start/shoot_start rejoué comme si l'étape venait de
|
|
@@ -39,6 +39,7 @@ from fletchtime.__main__ import (
|
|
|
39
39
|
ensure_directories,
|
|
40
40
|
local_ip,
|
|
41
41
|
)
|
|
42
|
+
from fletchtime.logging_setup import configure_logging
|
|
42
43
|
from fletchtime.runtime import ServerRuntime
|
|
43
44
|
|
|
44
45
|
_TRANSLATIONS = {
|
|
@@ -54,6 +55,7 @@ _TRANSLATIONS = {
|
|
|
54
55
|
"status_running": "Serveur en cours -- {ip}",
|
|
55
56
|
"log_title": "Journal",
|
|
56
57
|
"club_data": "Données du club :",
|
|
58
|
+
"addressCaption": "Adresse :",
|
|
57
59
|
},
|
|
58
60
|
"en": {
|
|
59
61
|
"title": "FletchTime -- Server",
|
|
@@ -67,6 +69,7 @@ _TRANSLATIONS = {
|
|
|
67
69
|
"status_running": "Server running -- {ip}",
|
|
68
70
|
"log_title": "Log",
|
|
69
71
|
"club_data": "Club data:",
|
|
72
|
+
"addressCaption": "Address:",
|
|
70
73
|
},
|
|
71
74
|
}
|
|
72
75
|
|
|
@@ -193,6 +196,14 @@ class FletchTimeApp(ctk.CTk):
|
|
|
193
196
|
sys.stdout = _QueueWriter(sys.stdout, self.log_queue)
|
|
194
197
|
sys.stderr = _QueueWriter(sys.stderr, self.log_queue)
|
|
195
198
|
|
|
199
|
+
# Appelé APRÈS la redirection ci-dessus, volontairement : le
|
|
200
|
+
# StreamHandler créé par configure_logging() se lie au sys.stderr
|
|
201
|
+
# courant au moment de sa création -- fait après le remplacement,
|
|
202
|
+
# les journaux applicatifs (commandes reçues, pertes de connexion...)
|
|
203
|
+
# apparaissent donc aussi dans le widget de journal de la fenêtre,
|
|
204
|
+
# pas seulement dans le fichier persistant.
|
|
205
|
+
self.log_file = configure_logging(self.data_root / "logs")
|
|
206
|
+
|
|
196
207
|
self.title(self._t("title"))
|
|
197
208
|
self.geometry("820x600")
|
|
198
209
|
self.minsize(640, 480)
|
|
@@ -281,6 +292,29 @@ class FletchTimeApp(ctk.CTk):
|
|
|
281
292
|
)
|
|
282
293
|
self.display_button.pack(side="left", padx=(0, 12), pady=10, expand=True, fill="x")
|
|
283
294
|
|
|
295
|
+
# -- adresse du serveur -----------------------------------------
|
|
296
|
+
# CTkEntry (désactivée après affichage) plutôt qu'un simple
|
|
297
|
+
# libellé : l'intention est de permettre de sélectionner/copier
|
|
298
|
+
# l'adresse (utile pour la retaper à la main sur un appareil sans
|
|
299
|
+
# bouton de raccourci, ex. un téléphone d'archer) -- non vérifié
|
|
300
|
+
# visuellement si "disabled" préserve la sélection de texte sur
|
|
301
|
+
# toutes les plateformes ; à confirmer en conditions réelles.
|
|
302
|
+
address_frame = ctk.CTkFrame(self)
|
|
303
|
+
address_frame.pack(fill="x", padx=16, pady=(0, 8))
|
|
304
|
+
|
|
305
|
+
self.address_caption = ctk.CTkLabel(
|
|
306
|
+
address_frame,
|
|
307
|
+
text=self._t("addressCaption"),
|
|
308
|
+
font=ctk.CTkFont(size=11),
|
|
309
|
+
text_color="gray60",
|
|
310
|
+
)
|
|
311
|
+
self.address_caption.pack(side="left", padx=(12, 8), pady=8)
|
|
312
|
+
|
|
313
|
+
self.address_entry = ctk.CTkEntry(
|
|
314
|
+
address_frame, font=ctk.CTkFont(family="monospace", size=12)
|
|
315
|
+
)
|
|
316
|
+
self.address_entry.pack(side="left", padx=(0, 12), pady=8, expand=True, fill="x")
|
|
317
|
+
|
|
284
318
|
# -- journal ----------------------------------------------------
|
|
285
319
|
self.log_label = ctk.CTkLabel(self, text=self._t("log_title"), anchor="w")
|
|
286
320
|
self.log_label.pack(fill="x", padx=20, pady=(8, 0))
|
|
@@ -303,8 +337,8 @@ class FletchTimeApp(ctk.CTk):
|
|
|
303
337
|
self,
|
|
304
338
|
text=self._t("quit"),
|
|
305
339
|
command=self._on_quit,
|
|
306
|
-
fg_color="
|
|
307
|
-
|
|
340
|
+
fg_color="#3a4354",
|
|
341
|
+
hover_color="#4a5568",
|
|
308
342
|
)
|
|
309
343
|
self.quit_button.pack(padx=16, pady=(0, 12))
|
|
310
344
|
|
|
@@ -332,6 +366,21 @@ class FletchTimeApp(ctk.CTk):
|
|
|
332
366
|
else:
|
|
333
367
|
self.status_dot.configure(text_color="gray50")
|
|
334
368
|
self.status_label.configure(text=self._t("status_stopped"))
|
|
369
|
+
self._refresh_address()
|
|
370
|
+
|
|
371
|
+
def _refresh_address(self) -> None:
|
|
372
|
+
"""L'adresse ne dépend pas de si le serveur tourne réellement --
|
|
373
|
+
affichée dans tous les cas (utile même à l'arrêt, pour préparer
|
|
374
|
+
la config réseau à l'avance sur un autre appareil)."""
|
|
375
|
+
address = f"http://{local_ip()}:{HTTP_PORT}/"
|
|
376
|
+
# CTkEntry ne supporte que "normal"/"disabled" (contrairement à
|
|
377
|
+
# ttk.Entry, qui a un vrai état "readonly") -- il faut donc la
|
|
378
|
+
# débloquer temporairement pour la mettre à jour, puis la
|
|
379
|
+
# reverrouiller aussitôt après.
|
|
380
|
+
self.address_entry.configure(state="normal")
|
|
381
|
+
self.address_entry.delete(0, "end")
|
|
382
|
+
self.address_entry.insert(0, address)
|
|
383
|
+
self.address_entry.configure(state="disabled")
|
|
335
384
|
|
|
336
385
|
def _on_language_change(self, value: str) -> None:
|
|
337
386
|
self.language = value.lower()
|
|
@@ -341,6 +390,7 @@ class FletchTimeApp(ctk.CTk):
|
|
|
341
390
|
self.home_button.configure(text=self._t("home"))
|
|
342
391
|
self.control_button.configure(text=self._t("control"))
|
|
343
392
|
self.display_button.configure(text=self._t("display"))
|
|
393
|
+
self.address_caption.configure(text=self._t("addressCaption"))
|
|
344
394
|
self.log_label.configure(text=self._t("log_title"))
|
|
345
395
|
self.quit_button.configure(text=self._t("quit"))
|
|
346
396
|
self.footer_label.configure(text=f"{self._t('club_data')} {self.data_root}")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Configuration du journal applicatif : fichier persistant avec rotation,
|
|
2
|
+
en plus de la sortie standard (déjà captée par la fenêtre graphique et
|
|
3
|
+
visible dans un terminal, mais perdue à la fermeture de l'appli -- voir
|
|
4
|
+
fletchtime.gui._QueueWriter). Utile pour comprendre après coup ce qui
|
|
5
|
+
s'est passé pendant un concours (quelles commandes reçues, pertes de
|
|
6
|
+
connexion, erreurs...) sans dépendre de cette mémoire volatile.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import logging.handlers
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
LOG_FORMAT = "%(asctime)s %(levelname)-8s %(name)s: %(message)s"
|
|
16
|
+
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
17
|
+
|
|
18
|
+
# 1 Mo par fichier, 5 fichiers conservés (dont le courant) -- quelques
|
|
19
|
+
# concours de suite avant qu'un fichier ne soit écrasé, sans grossir
|
|
20
|
+
# indéfiniment sur un usage au long cours.
|
|
21
|
+
MAX_BYTES = 1_000_000
|
|
22
|
+
BACKUP_COUNT = 5
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def configure_logging(log_dir: Path) -> Path:
|
|
26
|
+
"""Idempotent -- rappelable sans dupliquer les handlers si déjà
|
|
27
|
+
configuré (utile si un test ou un rechargement appelle ceci plusieurs
|
|
28
|
+
fois dans le même processus). Retourne le chemin du fichier de
|
|
29
|
+
journal, pour affichage à l'utilisateur si besoin."""
|
|
30
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
log_file = log_dir / "fletchtime.log"
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger("fletchtime")
|
|
34
|
+
logger.setLevel(logging.INFO)
|
|
35
|
+
|
|
36
|
+
already_has_file_handler = any(
|
|
37
|
+
isinstance(h, logging.handlers.RotatingFileHandler) for h in logger.handlers
|
|
38
|
+
)
|
|
39
|
+
if not already_has_file_handler:
|
|
40
|
+
formatter = logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT)
|
|
41
|
+
|
|
42
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
43
|
+
log_file, maxBytes=MAX_BYTES, backupCount=BACKUP_COUNT, encoding="utf-8"
|
|
44
|
+
)
|
|
45
|
+
file_handler.setFormatter(formatter)
|
|
46
|
+
logger.addHandler(file_handler)
|
|
47
|
+
|
|
48
|
+
# La sortie standard (stdout) est déjà affichée dans un terminal ou
|
|
49
|
+
# captée par la fenêtre graphique (voir fletchtime.gui) -- ce
|
|
50
|
+
# handler-ci fait en sorte que les logs applicatifs y apparaissent
|
|
51
|
+
# aussi, avec les mêmes horodatages/niveaux que dans le fichier.
|
|
52
|
+
stream_handler = logging.StreamHandler()
|
|
53
|
+
stream_handler.setFormatter(formatter)
|
|
54
|
+
logger.addHandler(stream_handler)
|
|
55
|
+
|
|
56
|
+
return log_file
|
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import json
|
|
14
|
+
import logging
|
|
14
15
|
import time
|
|
15
16
|
from dataclasses import asdict, replace
|
|
16
17
|
|
|
@@ -25,6 +26,8 @@ from . import config_store
|
|
|
25
26
|
|
|
26
27
|
TICK_INTERVAL = 0.2 # seconds between engine ticks / broadcasts
|
|
27
28
|
|
|
29
|
+
logger = logging.getLogger("fletchtime.server")
|
|
30
|
+
|
|
28
31
|
# Actions qui changent l'état du match ou son affichage -- protégées par mot
|
|
29
32
|
# de passe si un mot de passe est configuré (voir _auth_required). Tout le
|
|
30
33
|
# reste (get_config, register_display, authenticate) reste toujours
|
|
@@ -110,12 +113,19 @@ class MatchServer:
|
|
|
110
113
|
|
|
111
114
|
async def register(self, websocket) -> None:
|
|
112
115
|
self.clients.add(websocket)
|
|
116
|
+
logger.info("Connexion établie (total: %d client(s))", len(self.clients))
|
|
113
117
|
await self._send_state_to(websocket)
|
|
114
118
|
|
|
115
119
|
async def unregister(self, websocket) -> None:
|
|
120
|
+
lane = self._display_lanes.get(websocket)
|
|
116
121
|
self.clients.discard(websocket)
|
|
117
122
|
self._display_lanes.pop(websocket, None)
|
|
118
123
|
self._authenticated_connections.discard(websocket)
|
|
124
|
+
logger.info(
|
|
125
|
+
"Connexion perdue%s (total: %d client(s) restant(s))",
|
|
126
|
+
f" (lane={lane})" if lane else "",
|
|
127
|
+
len(self.clients),
|
|
128
|
+
)
|
|
119
129
|
|
|
120
130
|
# -- authentication -------------------------------------------------------
|
|
121
131
|
|
|
@@ -148,9 +158,17 @@ class MatchServer:
|
|
|
148
158
|
try:
|
|
149
159
|
data = json.loads(raw_message)
|
|
150
160
|
except json.JSONDecodeError:
|
|
161
|
+
logger.warning("Message reçu invalide (pas du JSON) : %r", raw_message[:200])
|
|
151
162
|
return
|
|
152
163
|
action = data.get("action")
|
|
153
164
|
|
|
165
|
+
# Ne jamais journaliser `data` tel quel : l'action "authenticate"
|
|
166
|
+
# transporte le mot de passe en clair, qui n'a rien à faire dans un
|
|
167
|
+
# fichier de log. Le nom de l'action et la lane (si connue) suffisent
|
|
168
|
+
# amplement pour comprendre après coup ce qui s'est passé.
|
|
169
|
+
lane = self._display_lanes.get(websocket)
|
|
170
|
+
logger.info("Commande reçue : %s%s", action, f" (lane={lane})" if lane else "")
|
|
171
|
+
|
|
154
172
|
if action == "authenticate":
|
|
155
173
|
await self._handle_authenticate(data, websocket)
|
|
156
174
|
return
|
|
@@ -411,6 +429,25 @@ class MatchServer:
|
|
|
411
429
|
if self.engine is not None:
|
|
412
430
|
self.engine.tick(elapsed)
|
|
413
431
|
events = self.engine.pop_pending_events()
|
|
432
|
+
if events:
|
|
433
|
+
state = self.engine.current_state
|
|
434
|
+
for event in events:
|
|
435
|
+
# countdown_tick exclu volontairement : se
|
|
436
|
+
# déclenche une fois par seconde dans les
|
|
437
|
+
# dernières secondes de CHAQUE volée --
|
|
438
|
+
# noierait le journal sans valeur diagnostique
|
|
439
|
+
# ajoutée (le passage à l'orange, lui, est déjà
|
|
440
|
+
# journalisé séparément via warning_orange).
|
|
441
|
+
if event == "countdown_tick":
|
|
442
|
+
continue
|
|
443
|
+
logger.info(
|
|
444
|
+
"Transition : %s (unité %d, volée %d%s, phase=%s)",
|
|
445
|
+
event,
|
|
446
|
+
state.unit_number,
|
|
447
|
+
state.end_number,
|
|
448
|
+
f", {state.current_turn}" if state.current_turn else "",
|
|
449
|
+
state.phase.value,
|
|
450
|
+
)
|
|
414
451
|
# À chaque tick (~5x/seconde), pas seulement
|
|
415
452
|
# périodiquement : un instantané périodique trop
|
|
416
453
|
# espacé (auparavant toutes les 5s) peut être visible
|
|
@@ -9,9 +9,12 @@ pages) is served separately by ``fletchtime.server.http_static``.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
|
+
import logging
|
|
12
13
|
|
|
13
14
|
from .match_server import MatchServer
|
|
14
15
|
|
|
16
|
+
logger = logging.getLogger("fletchtime.server")
|
|
17
|
+
|
|
15
18
|
|
|
16
19
|
async def run_ws_server(
|
|
17
20
|
port: int,
|
|
@@ -38,8 +41,11 @@ async def run_ws_server(
|
|
|
38
41
|
# Expected when a phone/tablet screen locks or the browser tab
|
|
39
42
|
# is backgrounded: Android suspends network activity, the
|
|
40
43
|
# client stops answering keepalive pings, and the connection
|
|
41
|
-
# times out. Not an error condition worth a traceback
|
|
42
|
-
|
|
44
|
+
# times out. Not an error condition worth a traceback, mais
|
|
45
|
+
# utile à journaliser : distingue une perte de connexion réseau
|
|
46
|
+
# abrupte d'une fermeture normale (onglet fermé proprement),
|
|
47
|
+
# voir aussi le journal générique de MatchServer.unregister.
|
|
48
|
+
logger.warning("Perte de connexion réseau détectée (WiFi, veille...)")
|
|
43
49
|
finally:
|
|
44
50
|
await server_state.unregister(websocket)
|
|
45
51
|
|
|
@@ -675,7 +675,15 @@ class TestMatchEngineSnapshotRestore(unittest.TestCase):
|
|
|
675
675
|
engine2 = MatchEngine(IndoorMode(self._cfg()), restore=snapshot)
|
|
676
676
|
self.assertEqual(engine2.current_state.time_left, 240.0) # inchangé
|
|
677
677
|
|
|
678
|
-
def
|
|
678
|
+
def test_restore_marks_match_finished_after_an_outage_longer_than_time_left(self) -> None:
|
|
679
|
+
"""Une coupure plus longue que ce qui restait sur l'étape en cours
|
|
680
|
+
marque le match terminé, plutôt que de figer time_left à 0 --
|
|
681
|
+
sans ça, le tick normal suivant aurait déclenché le rattrapage
|
|
682
|
+
automatique de tick() (pensé pour un simple ralentissement
|
|
683
|
+
passager, pas une coupure de plusieurs minutes) et fait
|
|
684
|
+
redémarrer le chrono sur l'étape suivante avec une durée fraîche,
|
|
685
|
+
ce qui ressemblait exactement à "le chrono repart" du point de
|
|
686
|
+
vue des archers."""
|
|
679
687
|
from unittest.mock import patch
|
|
680
688
|
|
|
681
689
|
with patch("fletchtime.engine.engine.time.time", return_value=2000.0):
|
|
@@ -688,7 +696,12 @@ class TestMatchEngineSnapshotRestore(unittest.TestCase):
|
|
|
688
696
|
# 500 vraies secondes plus tard -- largement au-delà des 220s restantes
|
|
689
697
|
with patch("fletchtime.engine.engine.time.time", return_value=2500.0):
|
|
690
698
|
engine2 = MatchEngine(IndoorMode(self._cfg()), restore=snapshot)
|
|
691
|
-
self.
|
|
699
|
+
self.assertTrue(engine2.current_state.finished)
|
|
700
|
+
self.assertEqual(engine2.current_state.time_left, 0.0)
|
|
701
|
+
# Un tick normal supplémentaire ne doit rien faire avancer :
|
|
702
|
+
# un match terminé reste terminé, pas de redémarrage du chrono.
|
|
703
|
+
engine2.tick(0.2)
|
|
704
|
+
self.assertTrue(engine2.current_state.finished)
|
|
692
705
|
|
|
693
706
|
|
|
694
707
|
if __name__ == "__main__":
|
|
@@ -1111,5 +1111,121 @@ class TestMatchServerCrashRecovery(unittest.IsolatedAsyncioTestCase):
|
|
|
1111
1111
|
self.assertIsNone(server.engine)
|
|
1112
1112
|
|
|
1113
1113
|
|
|
1114
|
+
class TestMatchServerLogging(unittest.IsolatedAsyncioTestCase):
|
|
1115
|
+
"""Couvre le journal applicatif (voir fletchtime.logging_setup) --
|
|
1116
|
+
commandes reçues, (dé)connexions, et surtout l'absence de fuite du
|
|
1117
|
+
mot de passe dans les journaux."""
|
|
1118
|
+
|
|
1119
|
+
async def asyncSetUp(self) -> None:
|
|
1120
|
+
# Efface tout instantané laissé par un test précédent -- voir
|
|
1121
|
+
# setUpModule/tearDownModule plus haut, qui isole déjà le chemin
|
|
1122
|
+
# lui-même, mais pas son contenu entre deux tests de ce fichier.
|
|
1123
|
+
config_store.clear_match_snapshot()
|
|
1124
|
+
# Isole aussi INDOOR_TOML/FLINT_TOML/APP_TOML -- un des tests de
|
|
1125
|
+
# cette classe appelle une vraie sauvegarde de config (mode
|
|
1126
|
+
# indoor, shoot_time=10.0) : sans cette isolation, cet appel
|
|
1127
|
+
# écrivait directement dans le vrai config/indoor.toml du projet
|
|
1128
|
+
# à chaque exécution de la suite de tests (bug réel constaté,
|
|
1129
|
+
# pas hypothétique -- voir l'historique de ce fichier).
|
|
1130
|
+
self._tmpdir = tempfile.TemporaryDirectory()
|
|
1131
|
+
self._original_indoor = config_store.INDOOR_TOML
|
|
1132
|
+
self._original_flint = config_store.FLINT_TOML
|
|
1133
|
+
self._original_app = config_store.APP_TOML
|
|
1134
|
+
config_store.INDOOR_TOML = Path(self._tmpdir.name) / "indoor.toml"
|
|
1135
|
+
config_store.FLINT_TOML = Path(self._tmpdir.name) / "flint.toml"
|
|
1136
|
+
config_store.APP_TOML = Path(self._tmpdir.name) / "app.toml"
|
|
1137
|
+
|
|
1138
|
+
self.server = MatchServer()
|
|
1139
|
+
self.control = FakeWebSocket()
|
|
1140
|
+
|
|
1141
|
+
async def asyncTearDown(self) -> None:
|
|
1142
|
+
config_store.clear_match_snapshot()
|
|
1143
|
+
config_store.INDOOR_TOML = self._original_indoor
|
|
1144
|
+
config_store.FLINT_TOML = self._original_flint
|
|
1145
|
+
config_store.APP_TOML = self._original_app
|
|
1146
|
+
self._tmpdir.cleanup()
|
|
1147
|
+
config_store.clear_match_snapshot()
|
|
1148
|
+
|
|
1149
|
+
async def test_received_command_is_logged(self) -> None:
|
|
1150
|
+
with self.assertLogs("fletchtime.server", level="INFO") as ctx:
|
|
1151
|
+
await self.server.handle_command(json.dumps({"action": "start_indoor"}), self.control)
|
|
1152
|
+
self.assertTrue(any("start_indoor" in line for line in ctx.output))
|
|
1153
|
+
|
|
1154
|
+
async def test_connect_and_disconnect_are_logged(self) -> None:
|
|
1155
|
+
with self.assertLogs("fletchtime.server", level="INFO") as ctx:
|
|
1156
|
+
await self.server.register(self.control)
|
|
1157
|
+
await self.server.unregister(self.control)
|
|
1158
|
+
joined = "\n".join(ctx.output)
|
|
1159
|
+
self.assertIn("établie", joined)
|
|
1160
|
+
self.assertIn("perdue", joined)
|
|
1161
|
+
|
|
1162
|
+
async def test_invalid_json_message_is_logged_as_warning(self) -> None:
|
|
1163
|
+
with self.assertLogs("fletchtime.server", level="WARNING") as ctx:
|
|
1164
|
+
await self.server.handle_command("pas du json {{{", self.control)
|
|
1165
|
+
self.assertTrue(any("invalide" in line for line in ctx.output))
|
|
1166
|
+
|
|
1167
|
+
async def test_password_never_appears_in_logs(self) -> None:
|
|
1168
|
+
"""Test de sécurité : l'action authenticate transporte le mot de
|
|
1169
|
+
passe en clair -- il ne doit jamais se retrouver dans un message
|
|
1170
|
+
de journal, où il traînerait en clair dans un fichier persistant."""
|
|
1171
|
+
secret = "SUPER_SECRET_PASSWORD_xyz789"
|
|
1172
|
+
with self.assertLogs("fletchtime.server", level="INFO") as ctx:
|
|
1173
|
+
await self.server.handle_command(
|
|
1174
|
+
json.dumps({"action": "authenticate", "password": secret}), self.control
|
|
1175
|
+
)
|
|
1176
|
+
joined = "\n".join(ctx.output)
|
|
1177
|
+
self.assertNotIn(secret, joined)
|
|
1178
|
+
|
|
1179
|
+
async def test_state_transition_is_logged_with_context(self) -> None:
|
|
1180
|
+
await self.server.handle_command(json.dumps({"action": "start_indoor"}), self.control)
|
|
1181
|
+
self.server.engine.tick(10) # RED -> GREEN
|
|
1182
|
+
self.server.engine.pop_pending_events()
|
|
1183
|
+
|
|
1184
|
+
with self.assertLogs("fletchtime.server", level="INFO") as ctx:
|
|
1185
|
+
await self.server.handle_command(json.dumps({"action": "next"}), self.control)
|
|
1186
|
+
task = asyncio.ensure_future(self.server.tick_loop())
|
|
1187
|
+
await asyncio.sleep(0.3)
|
|
1188
|
+
task.cancel()
|
|
1189
|
+
try:
|
|
1190
|
+
await task
|
|
1191
|
+
except asyncio.CancelledError:
|
|
1192
|
+
pass
|
|
1193
|
+
|
|
1194
|
+
joined = "\n".join(ctx.output)
|
|
1195
|
+
self.assertIn("Transition", joined)
|
|
1196
|
+
self.assertIn("prep_start", joined)
|
|
1197
|
+
|
|
1198
|
+
async def test_countdown_tick_is_never_logged(self) -> None:
|
|
1199
|
+
"""countdown_tick se déclenche une fois par seconde dans les
|
|
1200
|
+
dernières secondes de chaque volée -- noierait le journal sans
|
|
1201
|
+
valeur diagnostique ajoutée (contrairement à warning_orange, qui
|
|
1202
|
+
lui doit apparaître)."""
|
|
1203
|
+
await self.server.handle_command(
|
|
1204
|
+
json.dumps(
|
|
1205
|
+
{
|
|
1206
|
+
"action": "save_config",
|
|
1207
|
+
"mode": "indoor",
|
|
1208
|
+
"values": {"shoot_time": 10.0, "orange_warning_time": 3.0},
|
|
1209
|
+
}
|
|
1210
|
+
)
|
|
1211
|
+
)
|
|
1212
|
+
await self.server.handle_command(json.dumps({"action": "start_indoor"}), self.control)
|
|
1213
|
+
self.server.engine.tick(10) # RED -> GREEN(10s)
|
|
1214
|
+
self.server.engine.pop_pending_events()
|
|
1215
|
+
self.server.engine.tick(7.5) # 2.5s restantes -- franchit orange et des ticks
|
|
1216
|
+
|
|
1217
|
+
with self.assertLogs("fletchtime.server", level="INFO") as ctx:
|
|
1218
|
+
task = asyncio.ensure_future(self.server.tick_loop())
|
|
1219
|
+
await asyncio.sleep(0.3)
|
|
1220
|
+
task.cancel()
|
|
1221
|
+
try:
|
|
1222
|
+
await task
|
|
1223
|
+
except asyncio.CancelledError:
|
|
1224
|
+
pass
|
|
1225
|
+
|
|
1226
|
+
joined = "\n".join(ctx.output)
|
|
1227
|
+
self.assertNotIn("countdown_tick", joined)
|
|
1228
|
+
|
|
1229
|
+
|
|
1114
1230
|
if __name__ == "__main__":
|
|
1115
1231
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/targets/flint_20cm_4spot.jpg
RENAMED
|
File without changes
|
{fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/targets/flint_35cm_1spot.jpg
RENAMED
|
File without changes
|
{fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/targets/indoor_compound.jpg
RENAMED
|
File without changes
|
{fletchtime-0.2.0 → fletchtime-0.2.1}/src/fletchtime/web/_defaults/targets/indoor_recurve.jpg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|