fletchtime 0.2.4__tar.gz → 0.2.6__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.4/src/fletchtime.egg-info → fletchtime-0.2.6}/PKG-INFO +1 -1
- {fletchtime-0.2.4 → fletchtime-0.2.6}/config/flint.toml +3 -3
- {fletchtime-0.2.4 → fletchtime-0.2.6}/config/indoor.toml +2 -2
- {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/architecture.md +83 -2
- {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/roadmap.md +82 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/fletchtime.spec +7 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/_version.py +3 -3
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/modes/flint.py +3 -3
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/modes/indoor.py +2 -2
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/gui.py +172 -6
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/runtime.py +12 -2
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/server/config_store.py +41 -4
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/server/http_static.py +66 -8
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/server/match_server.py +67 -44
- fletchtime-0.2.6/src/fletchtime/web/_defaults/targets/flint_20cm_4spot.svg +237 -0
- fletchtime-0.2.6/src/fletchtime/web/_defaults/targets/flint_35cm_1spot.svg +90 -0
- fletchtime-0.2.6/src/fletchtime/web/_defaults/targets/indoor_compound.svg +231 -0
- fletchtime-0.2.6/src/fletchtime/web/_defaults/targets/indoor_recurve.svg +90 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/display.html +13 -3
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/i18n.js +6 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/index.html +16 -4
- fletchtime-0.2.6/src/fletchtime/web/logo.ico +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/manual.html +2 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6/src/fletchtime.egg-info}/PKG-INFO +1 -1
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/SOURCES.txt +5 -4
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/scm_file_list.json +5 -4
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/scm_version.json +2 -2
- {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_config_store.py +42 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_match_server.py +36 -0
- fletchtime-0.2.4/src/fletchtime/web/_defaults/targets/flint_20cm_4spot.jpg +0 -0
- fletchtime-0.2.4/src/fletchtime/web/_defaults/targets/flint_35cm_1spot.jpg +0 -0
- fletchtime-0.2.4/src/fletchtime/web/_defaults/targets/indoor_compound.jpg +0 -0
- fletchtime-0.2.4/src/fletchtime/web/_defaults/targets/indoor_recurve.jpg +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/.github/workflows/build.yml +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/.github/workflows/docs.yml +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/.github/workflows/test.yml +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/.gitignore +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/CONTRIBUTING.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/LICENSE +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/README.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/REMERCIEMENTS.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/config/app.toml +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/demo.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/_static/logo.svg +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/api-reference.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/conf.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/dev-guide/index.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/index.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/premier-club.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/remerciements.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/requirements.txt +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/specifications.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/user-guide/index.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/pyproject.toml +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/run_server.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/run_tests.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/scripts/generate_classic_sounds.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/setup.cfg +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/__init__.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/__main__.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/__init__.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/engine.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/models.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/modes/__init__.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/modes/base.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/sequence.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/turn_modes.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/logging_setup.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/server/__init__.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/server/ws_server.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/__init__.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/banners/README.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/club/README.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/README.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/countdown_tick.wav +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/emergency_end.wav +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/emergency_start.wav +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/end_of_match.wav +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/end_of_volee.wav +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/pause_end.wav +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/pause_start.wav +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/prep_start.wav +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/shoot_start.wav +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/warning_orange.wav +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/config.html +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/control.html +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/logo.svg +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/screenshots/config.png +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/screenshots/control.png +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/screenshots/display.png +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/screenshots/index.png +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/theme.js +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/dependency_links.txt +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/entry_points.txt +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/requires.txt +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/top_level.txt +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_engine.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_flint_mode.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_indoor_mode.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_main_cli.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_runtime.py +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/web/assets/banners/README.md +0 -0
- {fletchtime-0.2.4 → fletchtime-0.2.6}/web/assets/sounds/packs/README.md +0 -0
|
@@ -20,10 +20,10 @@ standard_orange_warning_time = 20.0
|
|
|
20
20
|
standard_distances = ["25 yards", "20 pieds", "30 yards", "15 yards", "20 yards", "10 yards"]
|
|
21
21
|
|
|
22
22
|
# Blason 1 spot (volées impaires)
|
|
23
|
-
standard_target_image_1spot = "assets/targets/flint_35cm_1spot.
|
|
23
|
+
standard_target_image_1spot = "assets/targets/flint_35cm_1spot.svg"
|
|
24
24
|
|
|
25
25
|
# Blason 4 spots (volées paires)
|
|
26
|
-
standard_target_image_4spot = "assets/targets/flint_20cm_4spot.
|
|
26
|
+
standard_target_image_4spot = "assets/targets/flint_20cm_4spot.svg"
|
|
27
27
|
|
|
28
28
|
# Nombre de flèches du walk-up
|
|
29
29
|
walkup_arrows = 4
|
|
@@ -41,7 +41,7 @@ walkup_orange_warning_time = 10.0
|
|
|
41
41
|
walkup_distances = ["30 yards", "25 yards", "20 yards", "15 yards"]
|
|
42
42
|
|
|
43
43
|
# Blason du walk-up
|
|
44
|
-
walkup_target_image = "assets/targets/flint_35cm_1spot.
|
|
44
|
+
walkup_target_image = "assets/targets/flint_35cm_1spot.svg"
|
|
45
45
|
|
|
46
46
|
# Ordre des relais par défaut
|
|
47
47
|
turn_mode = "ab_then_cd"
|
|
@@ -20,10 +20,10 @@ orange_warning_time = 30.0
|
|
|
20
20
|
distance_label = "20 yards"
|
|
21
21
|
|
|
22
22
|
# Image du blason recourbe/trad (chemin relatif à web/)
|
|
23
|
-
target_image_recurve = "assets/targets/indoor_recurve.
|
|
23
|
+
target_image_recurve = "assets/targets/indoor_recurve.svg"
|
|
24
24
|
|
|
25
25
|
# Image du blason poulies (chemin relatif à web/)
|
|
26
|
-
target_image_compound = "assets/targets/indoor_compound.
|
|
26
|
+
target_image_compound = "assets/targets/indoor_compound.svg"
|
|
27
27
|
|
|
28
28
|
# Ordre des relais par défaut : ab_then_cd, cd_then_ab, ab_only, cd_only
|
|
29
29
|
turn_mode = "ab_then_cd"
|
|
@@ -70,8 +70,8 @@ granularité de l'ordonnanceur est plus grossière que sous Linux). Voir
|
|
|
70
70
|
"arrow_in_end": 0,
|
|
71
71
|
"total_arrows_in_end": 0,
|
|
72
72
|
"distance_label": "20 yards",
|
|
73
|
-
"target_image": "assets/targets/indoor_recurve.
|
|
74
|
-
"target_image_2": "assets/targets/indoor_compound.
|
|
73
|
+
"target_image": "assets/targets/indoor_recurve.svg",
|
|
74
|
+
"target_image_2": "assets/targets/indoor_compound.svg",
|
|
75
75
|
"finished": false
|
|
76
76
|
},
|
|
77
77
|
"message": null,
|
|
@@ -343,6 +343,60 @@ 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
|
+
## Résilience de la boucle de décompte
|
|
347
|
+
|
|
348
|
+
`MatchServer.tick_loop()` capture désormais toute exception imprévue en
|
|
349
|
+
son sein (journalisée avec la trace complète) plutôt que de laisser
|
|
350
|
+
mourir la boucle silencieusement -- filet de sécurité ajouté suite à un
|
|
351
|
+
symptôme signalé en pratique : le chrono se figeait indéfiniment, sans
|
|
352
|
+
aucune erreur visible, le reste du serveur (connexions, réponse aux
|
|
353
|
+
commandes) continuant de fonctionner normalement à côté.
|
|
354
|
+
|
|
355
|
+
```{important}
|
|
356
|
+
**Cause la plus probable identifiée** : sous Windows, remplacer un
|
|
357
|
+
fichier (`Path.replace`) peut échouer avec une "violation de partage"
|
|
358
|
+
si un autre processus a le fichier cible ouvert au même instant
|
|
359
|
+
(antivirus, surveillance de fichiers d'un IDE, Git Bash...) -- une
|
|
360
|
+
différence fondamentale avec la sémantique POSIX (Linux/macOS), où ceci
|
|
361
|
+
n'arrive jamais. Observé une fois en pratique, précisément sur
|
|
362
|
+
`config/match_state.json` -- écrit à chaque tick depuis la persistance
|
|
363
|
+
après plantage (voir plus bas). Sans gestion d'erreur, cette exception
|
|
364
|
+
tuait silencieusement `tick_loop` pour de bon : exactement le symptôme
|
|
365
|
+
rapporté (gel permanent, aucune déconnexion, se produisant aussi bien
|
|
366
|
+
en fenêtre graphique qu'en mode terminal -- sans lien réel avec le
|
|
367
|
+
focus d'une fenêtre, malgré la corrélation observée au départ).
|
|
368
|
+
|
|
369
|
+
Deux correctifs complémentaires : ce filet dans `tick_loop` (n'importe
|
|
370
|
+
quelle exception, pas seulement celle-ci), et
|
|
371
|
+
`config_store.save_match_snapshot` qui retente quelques fois avant
|
|
372
|
+
d'abandonner proprement (jamais d'exception qui remonterait perturber
|
|
373
|
+
la diffusion de l'état aux écrans pour ce tick). Testé concrètement :
|
|
374
|
+
un échec transitoire (une fois puis réussite) est absorbé et le
|
|
375
|
+
contenu final reste correct ; un échec permanent (toutes les tentatives
|
|
376
|
+
échouent) est abandonné proprement, sans jamais lever d'exception.
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Statut technique exposé via HTTP (`/api/status`)
|
|
380
|
+
|
|
381
|
+
Les mêmes données déjà affichées dans `control.html` (écrans connectés,
|
|
382
|
+
mode actif, phase en cours, pack de sons, mot de passe configuré ou non)
|
|
383
|
+
sont aussi exposées via un simple GET HTTP, lues directement depuis
|
|
384
|
+
l'instance `MatchServer` partagée avec le serveur WebSocket (voir
|
|
385
|
+
`ServerRuntime`, qui construit maintenant ce `MatchServer` une seule fois
|
|
386
|
+
et le fait circuler vers les deux serveurs plutôt que de le laisser
|
|
387
|
+
`run_ws_server` en créer un nouveau à chaque démarrage).
|
|
388
|
+
|
|
389
|
+
Utilisé par la fenêtre graphique pour afficher ce même statut sans
|
|
390
|
+
dupliquer la logique de rendu HTML -- interrogé par sondage périodique
|
|
391
|
+
(toutes les 2s) depuis un thread séparé plutôt qu'en temps réel via une
|
|
392
|
+
vraie connexion WebSocket, volontairement : pas besoin de la précision
|
|
393
|
+
temps réel d'une vraie connexion juste pour un affichage de statut, et
|
|
394
|
+
ça évite de dupliquer toute la logique de reconnexion/état déjà présente
|
|
395
|
+
côté web. La requête HTTP elle-même tourne toujours dans un thread à
|
|
396
|
+
part, jamais directement depuis le thread principal de la fenêtre (une
|
|
397
|
+
requête bloquante, même locale, gèlerait sinon l'interface le temps de
|
|
398
|
+
sa réponse).
|
|
399
|
+
|
|
346
400
|
## Journal applicatif persistant
|
|
347
401
|
|
|
348
402
|
En plus du journal affiché dans la fenêtre graphique (en mémoire,
|
|
@@ -370,6 +424,21 @@ volatile de la fenêtre.
|
|
|
370
424
|
dernières secondes de chaque volée -- noierait le journal sans valeur
|
|
371
425
|
diagnostique ajoutée).
|
|
372
426
|
|
|
427
|
+
```{important}
|
|
428
|
+
`configure_logging` prend un `console_level` séparé du niveau du fichier
|
|
429
|
+
(toujours INFO par défaut, lui) -- la fenêtre graphique doit l'appeler
|
|
430
|
+
avec `console_level=logging.INFO` explicitement, **pas** le défaut
|
|
431
|
+
(`WARNING`, pensé pour un terminal silencieux par défaut, voir
|
|
432
|
+
`fletchtime.__main__`, `-v`/`--verbose`). Un oubli de ce paramètre a
|
|
433
|
+
laissé le widget de journal de la fenêtre silencieux en usage normal
|
|
434
|
+
pendant un temps -- tous les journaux applicatifs ci-dessus sont à
|
|
435
|
+
INFO, donc filtrés par le `WARNING` par défaut, exactement l'inverse de
|
|
436
|
+
ce que ce widget est censé montrer. Confirmé par comparaison directe
|
|
437
|
+
avant/après correctif : file d'attente vide avant, message présent
|
|
438
|
+
après, avec un appel par ailleurs identique.
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
|
|
373
442
|
```{important}
|
|
374
443
|
Le mot de passe (action `authenticate`) n'est **jamais** journalisé -- le
|
|
375
444
|
code ne journalise jamais `data` tel quel, seulement le nom de l'action
|
|
@@ -565,3 +634,15 @@ La miniature d'aperçu de `control.html` est un vrai `display.html` chargé
|
|
|
565
634
|
dans une `<iframe>` (mise à l'échelle en CSS) plutôt qu'une logique de rendu
|
|
566
635
|
dupliquée -- elle s'enregistre avec la lane spéciale `"apercu"`, exclue du
|
|
567
636
|
comptage des écrans connectés.
|
|
637
|
+
|
|
638
|
+
```{note}
|
|
639
|
+
**Cette même lane `"apercu"` ne joue jamais de son** (voir `display.html`,
|
|
640
|
+
`soundEnabled`) : sans ça, ouvrir la page de contrôle et un vrai écran
|
|
641
|
+
d'affichage sur le même PC faisait entendre chaque son deux fois --
|
|
642
|
+
l'aperçu est une simple vue visuelle pour le responsable du
|
|
643
|
+
chronométrage, pas un écran destiné aux archers. Un paramètre d'URL
|
|
644
|
+
`?mute=1` permet en plus de couper le son sur n'importe quel autre onglet
|
|
645
|
+
ouvert en trop sur la même machine (ex. pour surveiller une autre lane
|
|
646
|
+
sans dupliquer le son de l'écran qui joue réellement pour les archers).
|
|
647
|
+
```
|
|
648
|
+
|
|
@@ -77,6 +77,88 @@ dépendent d'extensions compilées (Rust/C) peu fiables sur Pydroid 3, voir
|
|
|
77
77
|
|
|
78
78
|
## Backlog — à discuter / non encore programmé dans une étape précise
|
|
79
79
|
|
|
80
|
+
- ~~**Couleur orange trop marron**~~ -- fait : `#78350f` remplacé par
|
|
81
|
+
`#b45309` -- plus saturé et plus lumineux, se lit comme un vrai orange
|
|
82
|
+
plutôt qu'un brun terne, tout en restant cohérent avec la noirceur du
|
|
83
|
+
rouge/vert déjà en place. Vérifié avec un vrai rendu (Chromium), pas
|
|
84
|
+
seulement en théorie.
|
|
85
|
+
- ~~**Cibles par défaut en SVG**~~ -- fait : les 4 cibles par défaut
|
|
86
|
+
(`indoor_recurve`, `indoor_compound`, `flint_35cm_1spot`,
|
|
87
|
+
`flint_20cm_4spot`) régénérées en SVG plutôt que converties depuis les
|
|
88
|
+
JPG existants -- proportions et couleurs officielles World Archery (10
|
|
89
|
+
zones de largeur égale, blanc/noir/bleu/rouge/or), reconstruites
|
|
90
|
+
proprement plutôt que vectorisées avec perte. Reste net à n'importe
|
|
91
|
+
quelle taille d'écran, contrairement à un JPG qui peut devenir flou sur
|
|
92
|
+
un grand affichage. `.svg` ajouté aussi à la détection des bannières
|
|
93
|
+
(`display.html`), pour la cohérence. Testé avec un vrai rendu de
|
|
94
|
+
`display.html` : les deux images se chargent et s'affichent
|
|
95
|
+
correctement.
|
|
96
|
+
|
|
97
|
+
- ~~**Choix de la lane/muet à l'ouverture d'un écran**~~ -- fait : la page
|
|
98
|
+
d'accueil permet maintenant de cocher "muet" à côté du champ de lane
|
|
99
|
+
déjà existant (ex. plusieurs écrans pilotés par le même PC, qui
|
|
100
|
+
partagent le même haut-parleur -- voir la FAQ du manuel). Même chose
|
|
101
|
+
dans la fenêtre graphique : le raccourci "Affichage", auparavant figé
|
|
102
|
+
sur `lane=1`, permet maintenant de choisir la lane et de cocher "muet"
|
|
103
|
+
avant l'ouverture. Testé avec un vrai navigateur (page d'accueil) et
|
|
104
|
+
vérifié via la documentation officielle de `customtkinter` (case à
|
|
105
|
+
cocher, fenêtre).
|
|
106
|
+
|
|
107
|
+
- ~~**Icône de l'exécutable**~~ -- fait : `web/logo.ico` (multi-résolution,
|
|
108
|
+
16 à 256px, généré depuis `web/logo.svg`), utilisé par `fletchtime.spec`
|
|
109
|
+
pour l'exécutable Windows. Sans effet sous Linux, qui n'a pas ce
|
|
110
|
+
concept de métadonnées d'icône pour un simple binaire (vérifié). Le
|
|
111
|
+
favicon des pages web était en fait déjà en place, rien à faire là.
|
|
112
|
+
- ~~**Son dupliqué sur plusieurs onglets**~~ -- fait : l'aperçu de
|
|
113
|
+
`control.html` (une vraie instance de `display.html` en iframe) ne
|
|
114
|
+
joue plus jamais de son -- c'est une simple vue visuelle, pas un écran
|
|
115
|
+
destiné aux archers. `?mute=1` permet en plus de couper le son sur
|
|
116
|
+
n'importe quel autre onglet ouvert en trop sur le même PC.
|
|
117
|
+
- ~~**Chrono figé (sous Windows, pas lié au focus en réalité)**~~ --
|
|
118
|
+
cause la plus probable identifiée : sous Windows, remplacer un fichier
|
|
119
|
+
peut échouer avec une "violation de partage" si un autre processus l'a
|
|
120
|
+
ouvert au même instant (antivirus, surveillance de fichiers d'un
|
|
121
|
+
IDE...) -- observé une fois en pratique, précisément sur
|
|
122
|
+
`config/match_state.json`, écrit à chaque tick depuis la persistance
|
|
123
|
+
après plantage. Sans gestion d'erreur, ça tuait silencieusement
|
|
124
|
+
`tick_loop` pour de bon -- exactement le symptôme rapporté (gel
|
|
125
|
+
permanent, pas de déconnexion, se produisant aussi bien en GUI qu'en
|
|
126
|
+
headless, sans lien réel avec le focus). Deux correctifs : `tick_loop`
|
|
127
|
+
capture désormais toute exception imprévue en son sein (journalisée
|
|
128
|
+
avec sa trace complète, ne meurt plus silencieusement) et
|
|
129
|
+
`save_match_snapshot` retente quelques fois avant d'abandonner
|
|
130
|
+
proprement (jamais d'exception qui remonterait perturber la diffusion
|
|
131
|
+
de l'état aux écrans). Testé concrètement : échec transitoire récupéré,
|
|
132
|
+
échec permanent absorbé sans lever. **Confirmé résolu en conditions
|
|
133
|
+
réelles sur Windows** (plus de blocage constaté après ce correctif).
|
|
134
|
+
- ~~**Bouton Quitter hors de la fenêtre**~~ -- fait : la taille de départ
|
|
135
|
+
de la fenêtre datait d'avant l'ajout de plusieurs sections (ports,
|
|
136
|
+
statut technique...), devenue trop petite pour tout montrer. Calcule
|
|
137
|
+
désormais la taille réellement nécessaire à partir du contenu construit
|
|
138
|
+
(`update_idletasks` + `winfo_reqheight`/`reqwidth`), plutôt qu'un
|
|
139
|
+
nombre fixe qui redeviendrait insuffisant à la prochaine section
|
|
140
|
+
ajoutée. **Réserve honnête** : ce motif est standard en Tkinter, mais
|
|
141
|
+
une source a signalé un comportement parfois différent avec
|
|
142
|
+
`customtkinter` selon si on l'applique à la fenêtre ou à un cadre
|
|
143
|
+
interne -- non vérifiable ici (pas d'affichage disponible), à confirmer
|
|
144
|
+
visuellement.
|
|
145
|
+
- ~~**Journal absent du widget de la fenêtre**~~ -- vrai bug trouvé en
|
|
146
|
+
creusant le point précédent : `configure_logging` était appelé sans
|
|
147
|
+
préciser `console_level`, retombant sur le défaut `WARNING` (pensé
|
|
148
|
+
pour un terminal silencieux) -- qui filtrait justement tout ce que ce
|
|
149
|
+
widget est censé montrer (commandes, connexions, transitions, toutes
|
|
150
|
+
en `INFO`). Corrigé avec `console_level=INFO` explicite pour la
|
|
151
|
+
fenêtre. Confirmé par comparaison directe avant/après : file vide
|
|
152
|
+
avant, message présent après. **Confirmé résolu en conditions
|
|
153
|
+
réelles** (le bon niveau de journal s'affiche bien dans la fenêtre).
|
|
154
|
+
- ~~**Données techniques dans la fenêtre**~~ -- fait : nouvel endpoint
|
|
155
|
+
`/api/status` (les mêmes données déjà affichées dans `control.html`),
|
|
156
|
+
affiché dans la fenêtre via sondage périodique. `ServerRuntime`
|
|
157
|
+
construit maintenant le `MatchServer` une seule fois et le partage
|
|
158
|
+
entre les deux serveurs, plutôt que d'en laisser `run_ws_server` créer
|
|
159
|
+
un nouveau à chaque démarrage.
|
|
160
|
+
|
|
161
|
+
|
|
80
162
|
- ~~**Remerciements**~~ -- structure prête, à compléter : `REMERCIEMENTS.md`
|
|
81
163
|
(modèle à remplir avec les noms), lié depuis le README et intégré à la
|
|
82
164
|
doc Sphinx ({doc}`remerciements` -- même contenu, pas dupliqué, via une
|
|
@@ -71,6 +71,13 @@ exe = EXE(
|
|
|
71
71
|
# assets/ du bootstrap), d'où un serveur qui tourne mais ne sert rien
|
|
72
72
|
# d'utile. "." restaure explicitement l'ancien comportement.
|
|
73
73
|
contents_directory=".",
|
|
74
|
+
# Icône de l'exécutable (barre des tâches, explorateur de fichiers,
|
|
75
|
+
# raccourci) -- fichier .ico multi-résolution (16 à 256px), généré à
|
|
76
|
+
# partir de web/logo.svg. PyInstaller ignore ce paramètre sous Linux
|
|
77
|
+
# (les .ico n'y ont pas de sens -- l'icône affichée dépend du
|
|
78
|
+
# gestionnaire de fichiers/bureau, hors du contrôle de l'exécutable
|
|
79
|
+
# lui-même), donc rien à prévoir de spécial pour cette plateforme.
|
|
80
|
+
icon=str(project_root / "src" / "fletchtime" / "web" / "logo.ico"),
|
|
74
81
|
)
|
|
75
82
|
|
|
76
83
|
coll = COLLECT(
|
|
@@ -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.6'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 6)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g4502981d1'
|
|
@@ -47,8 +47,8 @@ class FlintConfig:
|
|
|
47
47
|
)
|
|
48
48
|
# Les volées impaires (1, 3, 5) tirent sur le blason 35cm à 1 spot ; les
|
|
49
49
|
# volées paires (2, 4, 6) sur le blason 20cm à 4 spots.
|
|
50
|
-
standard_target_image_1spot: str = "assets/targets/flint_35cm_1spot.
|
|
51
|
-
standard_target_image_4spot: str = "assets/targets/flint_20cm_4spot.
|
|
50
|
+
standard_target_image_1spot: str = "assets/targets/flint_35cm_1spot.svg"
|
|
51
|
+
standard_target_image_4spot: str = "assets/targets/flint_20cm_4spot.svg"
|
|
52
52
|
|
|
53
53
|
walkup_arrows: int = 4
|
|
54
54
|
walkup_time_per_arrow: float = 45.0
|
|
@@ -63,7 +63,7 @@ class FlintConfig:
|
|
|
63
63
|
]
|
|
64
64
|
)
|
|
65
65
|
# La volée 7 (walk-up) suit le même schéma que les volées impaires : 1 spot.
|
|
66
|
-
walkup_target_image: str = "assets/targets/flint_35cm_1spot.
|
|
66
|
+
walkup_target_image: str = "assets/targets/flint_35cm_1spot.svg"
|
|
67
67
|
|
|
68
68
|
# Comment les archers se relaient sur le parcours : "ab_then_cd",
|
|
69
69
|
# "cd_then_ab" (les deux relais, un après l'autre -- chacun tirant
|
|
@@ -43,8 +43,8 @@ class IndoorConfig:
|
|
|
43
43
|
distance_label: str = "20 yards"
|
|
44
44
|
# Deux blasons affichés côte à côte : recourbe/trad à gauche, poulies à
|
|
45
45
|
# droite -- les deux catégories tirent souvent ensemble en Indoor.
|
|
46
|
-
target_image_recurve: str = "assets/targets/indoor_recurve.
|
|
47
|
-
target_image_compound: str = "assets/targets/indoor_compound.
|
|
46
|
+
target_image_recurve: str = "assets/targets/indoor_recurve.svg"
|
|
47
|
+
target_image_compound: str = "assets/targets/indoor_compound.svg"
|
|
48
48
|
|
|
49
49
|
# Comment les archers se relaient sur un même blason pendant une volée :
|
|
50
50
|
# "ab_then_cd", "cd_then_ab" (les deux relais, dans un ordre ou
|
|
@@ -24,8 +24,14 @@ Pydroid reste nécessaire pour confirmer le rendu et l'ergonomie tactile.
|
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
27
29
|
import queue
|
|
28
30
|
import sys
|
|
31
|
+
import threading
|
|
32
|
+
import urllib.error
|
|
33
|
+
import urllib.parse
|
|
34
|
+
import urllib.request
|
|
29
35
|
import webbrowser
|
|
30
36
|
|
|
31
37
|
import customtkinter as ctk
|
|
@@ -59,11 +65,24 @@ _TRANSLATIONS = {
|
|
|
59
65
|
"themeLight": "Clair",
|
|
60
66
|
"themeDark": "Sombre",
|
|
61
67
|
"networkCaption": "Ports (HTTP/WS) :",
|
|
68
|
+
"displayOptionsCaption": "Lane à ouvrir :",
|
|
69
|
+
"muteLabel": "Muet",
|
|
62
70
|
"apply": "Appliquer",
|
|
63
71
|
"networkErrorNotANumber": "Les ports doivent être des nombres",
|
|
64
72
|
"networkErrorRange": "Les ports doivent être entre 1 et 65535",
|
|
65
73
|
"networkErrorSame": "Les deux ports doivent être différents",
|
|
66
74
|
"networkApplied": "Appliqué",
|
|
75
|
+
"techStatusUnavailable": "Statut technique : indisponible (serveur en cours de démarrage ?)",
|
|
76
|
+
"techStatusClients": "Clients connectés :",
|
|
77
|
+
"techStatusLanes": "Écrans :",
|
|
78
|
+
"techStatusNoLanes": "aucun",
|
|
79
|
+
"techStatusMode": "Mode :",
|
|
80
|
+
"techStatusNoMode": "aucun",
|
|
81
|
+
"techStatusPhase": "Phase :",
|
|
82
|
+
"techStatusSound": "Sons :",
|
|
83
|
+
"techStatusAuth": "Mot de passe :",
|
|
84
|
+
"yes": "oui",
|
|
85
|
+
"no": "non",
|
|
67
86
|
},
|
|
68
87
|
"en": {
|
|
69
88
|
"title": "FletchTime -- Server",
|
|
@@ -82,11 +101,24 @@ _TRANSLATIONS = {
|
|
|
82
101
|
"themeLight": "Light",
|
|
83
102
|
"themeDark": "Dark",
|
|
84
103
|
"networkCaption": "Ports (HTTP/WS):",
|
|
104
|
+
"displayOptionsCaption": "Lane to open:",
|
|
105
|
+
"muteLabel": "Muted",
|
|
85
106
|
"apply": "Apply",
|
|
86
107
|
"networkErrorNotANumber": "Ports must be numbers",
|
|
87
108
|
"networkErrorRange": "Ports must be between 1 and 65535",
|
|
88
109
|
"networkErrorSame": "Both ports must be different",
|
|
89
110
|
"networkApplied": "Applied",
|
|
111
|
+
"techStatusUnavailable": "Technical status: unavailable (server starting?)",
|
|
112
|
+
"techStatusClients": "Connected clients:",
|
|
113
|
+
"techStatusLanes": "Screens:",
|
|
114
|
+
"techStatusNoLanes": "none",
|
|
115
|
+
"techStatusMode": "Mode:",
|
|
116
|
+
"techStatusNoMode": "none",
|
|
117
|
+
"techStatusPhase": "Phase:",
|
|
118
|
+
"techStatusSound": "Sounds:",
|
|
119
|
+
"techStatusAuth": "Password:",
|
|
120
|
+
"yes": "yes",
|
|
121
|
+
"no": "no",
|
|
90
122
|
},
|
|
91
123
|
}
|
|
92
124
|
|
|
@@ -209,6 +241,7 @@ class FletchTimeApp(ctk.CTk):
|
|
|
209
241
|
|
|
210
242
|
self.language = "fr"
|
|
211
243
|
self.log_queue: queue.Queue[str] = queue.Queue()
|
|
244
|
+
self.status_queue: queue.Queue = queue.Queue()
|
|
212
245
|
|
|
213
246
|
self.data_root = _data_root()
|
|
214
247
|
self.app_web_dir = _app_web_dir()
|
|
@@ -226,18 +259,47 @@ class FletchTimeApp(ctk.CTk):
|
|
|
226
259
|
# Appelé APRÈS la redirection ci-dessus, volontairement : le
|
|
227
260
|
# StreamHandler créé par configure_logging() se lie au sys.stderr
|
|
228
261
|
# courant au moment de sa création -- fait après le remplacement,
|
|
229
|
-
# les journaux applicatifs (commandes reçues, pertes de connexion
|
|
230
|
-
# apparaissent donc aussi dans le widget de
|
|
231
|
-
# pas seulement dans le fichier persistant.
|
|
232
|
-
|
|
262
|
+
# les journaux applicatifs (commandes reçues, pertes de connexion,
|
|
263
|
+
# transitions...) apparaissent donc aussi dans le widget de
|
|
264
|
+
# journal de la fenêtre, pas seulement dans le fichier persistant.
|
|
265
|
+
#
|
|
266
|
+
# console_level=INFO explicite, pas le défaut (WARNING, pensé
|
|
267
|
+
# pour un terminal silencieux par défaut -- voir
|
|
268
|
+
# fletchtime.__main__, -v/--verbose) : sans ce paramètre, le
|
|
269
|
+
# widget de journal de la fenêtre restait silencieux en usage
|
|
270
|
+
# normal, puisque tous les journaux applicatifs sont à INFO --
|
|
271
|
+
# exactement l'inverse de ce que ce widget est censé montrer.
|
|
272
|
+
self.log_file = configure_logging(self.data_root / "logs", console_level=logging.INFO)
|
|
233
273
|
|
|
234
274
|
self.title(self._t("title"))
|
|
235
|
-
self.geometry("820x600")
|
|
275
|
+
self.geometry("820x600") # taille de départ -- corrigée juste après
|
|
236
276
|
self.minsize(640, 480)
|
|
237
277
|
self.protocol("WM_DELETE_WINDOW", self._on_quit)
|
|
238
278
|
|
|
239
279
|
self._build_ui()
|
|
280
|
+
|
|
281
|
+
# La taille de départ ci-dessus datait d'avant l'ajout de plusieurs
|
|
282
|
+
# sections (ports, statut technique...) -- trop petite pour tout
|
|
283
|
+
# montrer, le bouton Quitter se retrouvant hors de la fenêtre
|
|
284
|
+
# visible (constaté en pratique). Plutôt que de deviner un nouveau
|
|
285
|
+
# nombre fixe qui redeviendrait insuffisant à la prochaine section
|
|
286
|
+
# ajoutée, calcule la taille réellement nécessaire à partir du
|
|
287
|
+
# contenu construit juste au-dessus : update_idletasks() force
|
|
288
|
+
# Tkinter à calculer la taille demandée par chaque widget avant
|
|
289
|
+
# qu'on ne la lise, sans quoi winfo_reqheight/reqwidth
|
|
290
|
+
# renverraient une valeur pas encore à jour.
|
|
291
|
+
self.update_idletasks()
|
|
292
|
+
required_width = max(820, self.winfo_reqwidth())
|
|
293
|
+
required_height = max(600, self.winfo_reqheight())
|
|
294
|
+
self.geometry(f"{required_width}x{required_height}")
|
|
295
|
+
# Empêche aussi de redescendre en dessous de cette taille par un
|
|
296
|
+
# redimensionnement manuel -- le bouton Quitter (et tout le reste)
|
|
297
|
+
# doit rester accessible sans avoir à agrandir la fenêtre.
|
|
298
|
+
self.minsize(required_width, required_height)
|
|
299
|
+
|
|
240
300
|
self._poll_log_queue()
|
|
301
|
+
self._poll_technical_status()
|
|
302
|
+
self._drain_status_queue()
|
|
241
303
|
self._start_server()
|
|
242
304
|
|
|
243
305
|
# -- traductions ---------------------------------------------------
|
|
@@ -324,10 +386,36 @@ class FletchTimeApp(ctk.CTk):
|
|
|
324
386
|
self.display_button = ctk.CTkButton(
|
|
325
387
|
links_frame,
|
|
326
388
|
text=self._t("display"),
|
|
327
|
-
command=
|
|
389
|
+
command=self._open_display,
|
|
328
390
|
)
|
|
329
391
|
self.display_button.pack(side="left", padx=(0, 12), pady=10, expand=True, fill="x")
|
|
330
392
|
|
|
393
|
+
# -- options d'affichage (lane à ouvrir, muet ou non) --------------
|
|
394
|
+
# Auparavant un simple raccourci vers lane=1 -- rendu configurable
|
|
395
|
+
# pour pouvoir ouvrir n'importe quelle lane et couper le son sur cet
|
|
396
|
+
# onglet précis (voir display.html, soundEnabled) directement
|
|
397
|
+
# depuis la fenêtre, comme c'est déjà possible depuis la page
|
|
398
|
+
# d'accueil.
|
|
399
|
+
display_options_frame = ctk.CTkFrame(self)
|
|
400
|
+
display_options_frame.pack(fill="x", padx=16, pady=(0, 8))
|
|
401
|
+
|
|
402
|
+
self.display_options_caption = ctk.CTkLabel(
|
|
403
|
+
display_options_frame,
|
|
404
|
+
text=self._t("displayOptionsCaption"),
|
|
405
|
+
font=ctk.CTkFont(size=11),
|
|
406
|
+
text_color="gray60",
|
|
407
|
+
)
|
|
408
|
+
self.display_options_caption.pack(side="left", padx=(12, 8), pady=8)
|
|
409
|
+
|
|
410
|
+
self.lane_entry = ctk.CTkEntry(display_options_frame, width=90, justify="center")
|
|
411
|
+
self.lane_entry.insert(0, "1")
|
|
412
|
+
self.lane_entry.pack(side="left", padx=(0, 12), pady=8)
|
|
413
|
+
|
|
414
|
+
self.mute_checkbox = ctk.CTkCheckBox(
|
|
415
|
+
display_options_frame, text=self._t("muteLabel"), width=20
|
|
416
|
+
)
|
|
417
|
+
self.mute_checkbox.pack(side="left", padx=(0, 12), pady=8)
|
|
418
|
+
|
|
331
419
|
# -- adresse du serveur -----------------------------------------
|
|
332
420
|
# CTkEntry (désactivée après affichage) plutôt qu'un simple
|
|
333
421
|
# libellé : l'intention est de permettre de sélectionner/copier
|
|
@@ -388,6 +476,20 @@ class FletchTimeApp(ctk.CTk):
|
|
|
388
476
|
self.network_status_label = ctk.CTkLabel(network_frame, text="", text_color="gray60")
|
|
389
477
|
self.network_status_label.pack(side="left", padx=(4, 12), pady=8, fill="x", expand=True)
|
|
390
478
|
|
|
479
|
+
# -- statut technique ---------------------------------------------
|
|
480
|
+
# Les mêmes données techniques que celles déjà affichées dans la
|
|
481
|
+
# page de contrôle (écrans connectés, mode actif...), lues via
|
|
482
|
+
# /api/status -- voir _poll_technical_status ci-dessous, et
|
|
483
|
+
# fletchtime.server.http_static._DualRootHandler._build_status_body
|
|
484
|
+
# côté serveur.
|
|
485
|
+
tech_frame = ctk.CTkFrame(self)
|
|
486
|
+
tech_frame.pack(fill="x", padx=16, pady=(0, 8))
|
|
487
|
+
|
|
488
|
+
self.tech_status_label = ctk.CTkLabel(
|
|
489
|
+
tech_frame, text=self._t("techStatusUnavailable"), anchor="w", justify="left"
|
|
490
|
+
)
|
|
491
|
+
self.tech_status_label.pack(fill="x", padx=12, pady=8)
|
|
492
|
+
|
|
391
493
|
# -- journal ----------------------------------------------------
|
|
392
494
|
self.log_label = ctk.CTkLabel(self, text=self._t("log_title"), anchor="w")
|
|
393
495
|
self.log_label.pack(fill="x", padx=20, pady=(8, 0))
|
|
@@ -420,6 +522,13 @@ class FletchTimeApp(ctk.CTk):
|
|
|
420
522
|
def _open_link(self, path: str) -> None:
|
|
421
523
|
webbrowser.open(f"http://127.0.0.1:{self.http_port}{path}")
|
|
422
524
|
|
|
525
|
+
def _open_display(self) -> None:
|
|
526
|
+
lane = self.lane_entry.get().strip() or "1"
|
|
527
|
+
path = f"/display.html?lane={urllib.parse.quote(lane)}"
|
|
528
|
+
if self.mute_checkbox.get():
|
|
529
|
+
path += "&mute=1"
|
|
530
|
+
self._open_link(path)
|
|
531
|
+
|
|
423
532
|
def _start_server(self) -> None:
|
|
424
533
|
if self.runtime.is_running:
|
|
425
534
|
return
|
|
@@ -463,6 +572,8 @@ class FletchTimeApp(ctk.CTk):
|
|
|
463
572
|
self.home_button.configure(text=self._t("home"))
|
|
464
573
|
self.control_button.configure(text=self._t("control"))
|
|
465
574
|
self.display_button.configure(text=self._t("display"))
|
|
575
|
+
self.display_options_caption.configure(text=self._t("displayOptionsCaption"))
|
|
576
|
+
self.mute_checkbox.configure(text=self._t("muteLabel"))
|
|
466
577
|
self.address_caption.configure(text=self._t("addressCaption"))
|
|
467
578
|
self.log_label.configure(text=self._t("log_title"))
|
|
468
579
|
self.quit_button.configure(text=self._t("quit"))
|
|
@@ -557,6 +668,61 @@ class FletchTimeApp(ctk.CTk):
|
|
|
557
668
|
|
|
558
669
|
# -- journal (file thread-safe -> widget) ------------------------------
|
|
559
670
|
|
|
671
|
+
# -- statut technique (thread séparé pour la requête HTTP -> file) -----
|
|
672
|
+
|
|
673
|
+
def _poll_technical_status(self) -> None:
|
|
674
|
+
# Une requête HTTP, même locale, peut mettre du temps à répondre
|
|
675
|
+
# (serveur qui démarre, port qui change...) -- jamais faite
|
|
676
|
+
# directement depuis le thread principal (gèlerait la fenêtre le
|
|
677
|
+
# temps de la requête). Un thread par sondage, plutôt qu'un seul
|
|
678
|
+
# thread persistant : plus simple, et chaque requête se termine en
|
|
679
|
+
# quelques millisecondes en pratique (même machine).
|
|
680
|
+
threading.Thread(target=self._fetch_technical_status, daemon=True).start()
|
|
681
|
+
self.after(2000, self._poll_technical_status)
|
|
682
|
+
|
|
683
|
+
def _fetch_technical_status(self) -> None:
|
|
684
|
+
# Tourne dans un thread séparé -- ne touche JAMAIS un widget
|
|
685
|
+
# Tkinter directement ici (pas thread-safe) ; pousse seulement le
|
|
686
|
+
# résultat dans une file, lue depuis le thread principal via
|
|
687
|
+
# _drain_status_queue, exactement comme le journal (self.log_queue).
|
|
688
|
+
try:
|
|
689
|
+
url = f"http://127.0.0.1:{self.http_port}/api/status"
|
|
690
|
+
with urllib.request.urlopen(url, timeout=2) as res:
|
|
691
|
+
data = json.loads(res.read())
|
|
692
|
+
except (urllib.error.URLError, OSError, ValueError):
|
|
693
|
+
# Serveur pas encore démarré, port en cours de changement,
|
|
694
|
+
# réponse invalide... -- affiche juste "indisponible", jamais
|
|
695
|
+
# une exception qui remonterait jusqu'au thread principal.
|
|
696
|
+
data = None
|
|
697
|
+
self.status_queue.put(data)
|
|
698
|
+
|
|
699
|
+
def _drain_status_queue(self) -> None:
|
|
700
|
+
try:
|
|
701
|
+
while True:
|
|
702
|
+
data = self.status_queue.get_nowait()
|
|
703
|
+
self._render_technical_status(data)
|
|
704
|
+
except queue.Empty:
|
|
705
|
+
pass
|
|
706
|
+
self.after(200, self._drain_status_queue)
|
|
707
|
+
|
|
708
|
+
def _render_technical_status(self, data: dict | None) -> None:
|
|
709
|
+
if not data or not data.get("available"):
|
|
710
|
+
self.tech_status_label.configure(text=self._t("techStatusUnavailable"))
|
|
711
|
+
return
|
|
712
|
+
lanes = ", ".join(data["connected_lanes"]) or self._t("techStatusNoLanes")
|
|
713
|
+
mode = data["active_mode"] or self._t("techStatusNoMode")
|
|
714
|
+
phase = data["match_phase"] or "--"
|
|
715
|
+
text = (
|
|
716
|
+
f"{self._t('techStatusClients')} {data['connected_clients']} | "
|
|
717
|
+
f"{self._t('techStatusLanes')} {lanes} | "
|
|
718
|
+
f"{self._t('techStatusMode')} {mode} | "
|
|
719
|
+
f"{self._t('techStatusPhase')} {phase} | "
|
|
720
|
+
f"{self._t('techStatusSound')} {data['sound_pack']} | "
|
|
721
|
+
f"{self._t('techStatusAuth')} "
|
|
722
|
+
f"{self._t('yes') if data['password_configured'] else self._t('no')}"
|
|
723
|
+
)
|
|
724
|
+
self.tech_status_label.configure(text=text)
|
|
725
|
+
|
|
560
726
|
def _poll_log_queue(self) -> None:
|
|
561
727
|
try:
|
|
562
728
|
while True:
|
|
@@ -18,6 +18,7 @@ import threading
|
|
|
18
18
|
from http.server import ThreadingHTTPServer
|
|
19
19
|
|
|
20
20
|
from fletchtime.server.http_static import start_http_server
|
|
21
|
+
from fletchtime.server.match_server import MatchServer
|
|
21
22
|
from fletchtime.server.ws_server import run_ws_server
|
|
22
23
|
|
|
23
24
|
|
|
@@ -33,6 +34,13 @@ class ServerRuntime:
|
|
|
33
34
|
self.http_port = http_port
|
|
34
35
|
self.ws_port = ws_port
|
|
35
36
|
|
|
37
|
+
# Créé une seule fois ici (pas dans run_ws_server, qui en
|
|
38
|
+
# construirait un nouveau à chaque start()) -- partagé avec le
|
|
39
|
+
# serveur HTTP (voir start_http_server, paramètre match_server)
|
|
40
|
+
# pour exposer l'état du match en lecture via /api/status, sans
|
|
41
|
+
# dupliquer d'état entre les deux serveurs.
|
|
42
|
+
self.match_server = MatchServer()
|
|
43
|
+
|
|
36
44
|
self._httpd: ThreadingHTTPServer | None = None
|
|
37
45
|
self._http_thread: threading.Thread | None = None
|
|
38
46
|
self._ws_thread: threading.Thread | None = None
|
|
@@ -52,7 +60,7 @@ class ServerRuntime:
|
|
|
52
60
|
return
|
|
53
61
|
|
|
54
62
|
self._httpd = start_http_server(
|
|
55
|
-
self.app_web_dir, self.http_port, self.assets_dir, self.ws_port
|
|
63
|
+
self.app_web_dir, self.http_port, self.assets_dir, self.ws_port, self.match_server
|
|
56
64
|
)
|
|
57
65
|
self._http_thread = threading.Thread(target=self._httpd.serve_forever, daemon=True)
|
|
58
66
|
self._http_thread.start()
|
|
@@ -72,7 +80,9 @@ class ServerRuntime:
|
|
|
72
80
|
self._ws_stop_event = asyncio.Event()
|
|
73
81
|
self._ws_ready.set()
|
|
74
82
|
try:
|
|
75
|
-
loop.run_until_complete(
|
|
83
|
+
loop.run_until_complete(
|
|
84
|
+
run_ws_server(self.ws_port, self._ws_stop_event, self.match_server)
|
|
85
|
+
)
|
|
76
86
|
finally:
|
|
77
87
|
loop.close()
|
|
78
88
|
|