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.
Files changed (103) hide show
  1. {fletchtime-0.2.4/src/fletchtime.egg-info → fletchtime-0.2.6}/PKG-INFO +1 -1
  2. {fletchtime-0.2.4 → fletchtime-0.2.6}/config/flint.toml +3 -3
  3. {fletchtime-0.2.4 → fletchtime-0.2.6}/config/indoor.toml +2 -2
  4. {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/architecture.md +83 -2
  5. {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/roadmap.md +82 -0
  6. {fletchtime-0.2.4 → fletchtime-0.2.6}/fletchtime.spec +7 -0
  7. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/_version.py +3 -3
  8. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/modes/flint.py +3 -3
  9. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/modes/indoor.py +2 -2
  10. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/gui.py +172 -6
  11. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/runtime.py +12 -2
  12. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/server/config_store.py +41 -4
  13. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/server/http_static.py +66 -8
  14. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/server/match_server.py +67 -44
  15. fletchtime-0.2.6/src/fletchtime/web/_defaults/targets/flint_20cm_4spot.svg +237 -0
  16. fletchtime-0.2.6/src/fletchtime/web/_defaults/targets/flint_35cm_1spot.svg +90 -0
  17. fletchtime-0.2.6/src/fletchtime/web/_defaults/targets/indoor_compound.svg +231 -0
  18. fletchtime-0.2.6/src/fletchtime/web/_defaults/targets/indoor_recurve.svg +90 -0
  19. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/display.html +13 -3
  20. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/i18n.js +6 -0
  21. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/index.html +16 -4
  22. fletchtime-0.2.6/src/fletchtime/web/logo.ico +0 -0
  23. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/manual.html +2 -0
  24. {fletchtime-0.2.4 → fletchtime-0.2.6/src/fletchtime.egg-info}/PKG-INFO +1 -1
  25. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/SOURCES.txt +5 -4
  26. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/scm_file_list.json +5 -4
  27. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/scm_version.json +2 -2
  28. {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_config_store.py +42 -0
  29. {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_match_server.py +36 -0
  30. fletchtime-0.2.4/src/fletchtime/web/_defaults/targets/flint_20cm_4spot.jpg +0 -0
  31. fletchtime-0.2.4/src/fletchtime/web/_defaults/targets/flint_35cm_1spot.jpg +0 -0
  32. fletchtime-0.2.4/src/fletchtime/web/_defaults/targets/indoor_compound.jpg +0 -0
  33. fletchtime-0.2.4/src/fletchtime/web/_defaults/targets/indoor_recurve.jpg +0 -0
  34. {fletchtime-0.2.4 → fletchtime-0.2.6}/.github/workflows/build.yml +0 -0
  35. {fletchtime-0.2.4 → fletchtime-0.2.6}/.github/workflows/docs.yml +0 -0
  36. {fletchtime-0.2.4 → fletchtime-0.2.6}/.github/workflows/test.yml +0 -0
  37. {fletchtime-0.2.4 → fletchtime-0.2.6}/.gitignore +0 -0
  38. {fletchtime-0.2.4 → fletchtime-0.2.6}/CONTRIBUTING.md +0 -0
  39. {fletchtime-0.2.4 → fletchtime-0.2.6}/LICENSE +0 -0
  40. {fletchtime-0.2.4 → fletchtime-0.2.6}/README.md +0 -0
  41. {fletchtime-0.2.4 → fletchtime-0.2.6}/REMERCIEMENTS.md +0 -0
  42. {fletchtime-0.2.4 → fletchtime-0.2.6}/config/app.toml +0 -0
  43. {fletchtime-0.2.4 → fletchtime-0.2.6}/demo.py +0 -0
  44. {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/_static/logo.svg +0 -0
  45. {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/api-reference.md +0 -0
  46. {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/conf.py +0 -0
  47. {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/dev-guide/index.md +0 -0
  48. {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/index.md +0 -0
  49. {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/premier-club.md +0 -0
  50. {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/remerciements.md +0 -0
  51. {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/requirements.txt +0 -0
  52. {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/specifications.md +0 -0
  53. {fletchtime-0.2.4 → fletchtime-0.2.6}/docs/user-guide/index.md +0 -0
  54. {fletchtime-0.2.4 → fletchtime-0.2.6}/pyproject.toml +0 -0
  55. {fletchtime-0.2.4 → fletchtime-0.2.6}/run_server.py +0 -0
  56. {fletchtime-0.2.4 → fletchtime-0.2.6}/run_tests.py +0 -0
  57. {fletchtime-0.2.4 → fletchtime-0.2.6}/scripts/generate_classic_sounds.py +0 -0
  58. {fletchtime-0.2.4 → fletchtime-0.2.6}/setup.cfg +0 -0
  59. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/__init__.py +0 -0
  60. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/__main__.py +0 -0
  61. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/__init__.py +0 -0
  62. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/engine.py +0 -0
  63. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/models.py +0 -0
  64. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/modes/__init__.py +0 -0
  65. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/modes/base.py +0 -0
  66. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/sequence.py +0 -0
  67. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/engine/turn_modes.py +0 -0
  68. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/logging_setup.py +0 -0
  69. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/server/__init__.py +0 -0
  70. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/server/ws_server.py +0 -0
  71. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/__init__.py +0 -0
  72. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/banners/README.md +0 -0
  73. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/club/README.md +0 -0
  74. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/README.md +0 -0
  75. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/countdown_tick.wav +0 -0
  76. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/emergency_end.wav +0 -0
  77. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/emergency_start.wav +0 -0
  78. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/end_of_match.wav +0 -0
  79. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/end_of_volee.wav +0 -0
  80. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/pause_end.wav +0 -0
  81. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/pause_start.wav +0 -0
  82. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/prep_start.wav +0 -0
  83. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/shoot_start.wav +0 -0
  84. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/_defaults/sounds/packs/classic/warning_orange.wav +0 -0
  85. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/config.html +0 -0
  86. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/control.html +0 -0
  87. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/logo.svg +0 -0
  88. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/screenshots/config.png +0 -0
  89. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/screenshots/control.png +0 -0
  90. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/screenshots/display.png +0 -0
  91. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/screenshots/index.png +0 -0
  92. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime/web/theme.js +0 -0
  93. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/dependency_links.txt +0 -0
  94. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/entry_points.txt +0 -0
  95. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/requires.txt +0 -0
  96. {fletchtime-0.2.4 → fletchtime-0.2.6}/src/fletchtime.egg-info/top_level.txt +0 -0
  97. {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_engine.py +0 -0
  98. {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_flint_mode.py +0 -0
  99. {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_indoor_mode.py +0 -0
  100. {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_main_cli.py +0 -0
  101. {fletchtime-0.2.4 → fletchtime-0.2.6}/tests/test_runtime.py +0 -0
  102. {fletchtime-0.2.4 → fletchtime-0.2.6}/web/assets/banners/README.md +0 -0
  103. {fletchtime-0.2.4 → fletchtime-0.2.6}/web/assets/sounds/packs/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fletchtime
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Chronométrage de compétitions d'archerie FFTL (Indoor, Flint) -- serveur + interfaces web incluses
5
5
  License: GPL-3.0-or-later
6
6
  Requires-Python: >=3.11
@@ -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.jpg"
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.jpg"
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.jpg"
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.jpg"
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.jpg"
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.jpg",
74
- "target_image_2": "assets/targets/indoor_compound.jpg",
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.4'
22
- __version_tuple__ = version_tuple = (0, 2, 4)
21
+ __version__ = version = '0.2.6'
22
+ __version_tuple__ = version_tuple = (0, 2, 6)
23
23
 
24
- __commit_id__ = commit_id = 'g5efc3a10b'
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.jpg"
51
- standard_target_image_4spot: str = "assets/targets/flint_20cm_4spot.jpg"
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.jpg"
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.jpg"
47
- target_image_compound: str = "assets/targets/indoor_compound.jpg"
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 journal de la fenêtre,
231
- # pas seulement dans le fichier persistant.
232
- self.log_file = configure_logging(self.data_root / "logs")
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=lambda: self._open_link("/display.html?lane=1"),
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(run_ws_server(self.ws_port, self._ws_stop_event))
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