fletchtime 0.2.1__tar.gz → 0.2.2__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 (95) hide show
  1. {fletchtime-0.2.1 → fletchtime-0.2.2}/.gitignore +4 -0
  2. {fletchtime-0.2.1/src/fletchtime.egg-info → fletchtime-0.2.2}/PKG-INFO +29 -1
  3. {fletchtime-0.2.1 → fletchtime-0.2.2}/README.md +28 -0
  4. {fletchtime-0.2.1 → fletchtime-0.2.2}/docs/index.md +11 -0
  5. {fletchtime-0.2.1 → fletchtime-0.2.2}/docs/roadmap.md +17 -8
  6. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/__main__.py +24 -8
  7. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/_version.py +3 -3
  8. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/gui.py +165 -10
  9. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/server/config_store.py +37 -0
  10. {fletchtime-0.2.1 → fletchtime-0.2.2/src/fletchtime.egg-info}/PKG-INFO +29 -1
  11. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime.egg-info/scm_version.json +2 -2
  12. {fletchtime-0.2.1 → fletchtime-0.2.2}/.github/workflows/build.yml +0 -0
  13. {fletchtime-0.2.1 → fletchtime-0.2.2}/.github/workflows/docs.yml +0 -0
  14. {fletchtime-0.2.1 → fletchtime-0.2.2}/.github/workflows/test.yml +0 -0
  15. {fletchtime-0.2.1 → fletchtime-0.2.2}/CONTRIBUTING.md +0 -0
  16. {fletchtime-0.2.1 → fletchtime-0.2.2}/LICENSE +0 -0
  17. {fletchtime-0.2.1 → fletchtime-0.2.2}/config/app.toml +0 -0
  18. {fletchtime-0.2.1 → fletchtime-0.2.2}/config/flint.toml +0 -0
  19. {fletchtime-0.2.1 → fletchtime-0.2.2}/config/indoor.toml +0 -0
  20. {fletchtime-0.2.1 → fletchtime-0.2.2}/demo.py +0 -0
  21. {fletchtime-0.2.1 → fletchtime-0.2.2}/docs/_static/logo.svg +0 -0
  22. {fletchtime-0.2.1 → fletchtime-0.2.2}/docs/api-reference.md +0 -0
  23. {fletchtime-0.2.1 → fletchtime-0.2.2}/docs/architecture.md +0 -0
  24. {fletchtime-0.2.1 → fletchtime-0.2.2}/docs/conf.py +0 -0
  25. {fletchtime-0.2.1 → fletchtime-0.2.2}/docs/dev-guide/index.md +0 -0
  26. {fletchtime-0.2.1 → fletchtime-0.2.2}/docs/requirements.txt +0 -0
  27. {fletchtime-0.2.1 → fletchtime-0.2.2}/docs/specifications.md +0 -0
  28. {fletchtime-0.2.1 → fletchtime-0.2.2}/docs/user-guide/index.md +0 -0
  29. {fletchtime-0.2.1 → fletchtime-0.2.2}/fletchtime.spec +0 -0
  30. {fletchtime-0.2.1 → fletchtime-0.2.2}/pyproject.toml +0 -0
  31. {fletchtime-0.2.1 → fletchtime-0.2.2}/run_server.py +0 -0
  32. {fletchtime-0.2.1 → fletchtime-0.2.2}/run_tests.py +0 -0
  33. {fletchtime-0.2.1 → fletchtime-0.2.2}/scripts/generate_classic_sounds.py +0 -0
  34. {fletchtime-0.2.1 → fletchtime-0.2.2}/setup.cfg +0 -0
  35. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/__init__.py +0 -0
  36. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/engine/__init__.py +0 -0
  37. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/engine/engine.py +0 -0
  38. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/engine/models.py +0 -0
  39. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/engine/modes/__init__.py +0 -0
  40. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/engine/modes/base.py +0 -0
  41. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/engine/modes/flint.py +0 -0
  42. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/engine/modes/indoor.py +0 -0
  43. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/engine/sequence.py +0 -0
  44. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/engine/turn_modes.py +0 -0
  45. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/logging_setup.py +0 -0
  46. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/runtime.py +0 -0
  47. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/server/__init__.py +0 -0
  48. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/server/http_static.py +0 -0
  49. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/server/match_server.py +0 -0
  50. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/server/ws_server.py +0 -0
  51. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/__init__.py +0 -0
  52. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/banners/README.md +0 -0
  53. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/club/README.md +0 -0
  54. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/gui/theme.json +0 -0
  55. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/sounds/packs/README.md +0 -0
  56. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/sounds/packs/classic/countdown_tick.wav +0 -0
  57. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/sounds/packs/classic/emergency_end.wav +0 -0
  58. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/sounds/packs/classic/emergency_start.wav +0 -0
  59. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/sounds/packs/classic/end_of_match.wav +0 -0
  60. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/sounds/packs/classic/end_of_volee.wav +0 -0
  61. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/sounds/packs/classic/pause_end.wav +0 -0
  62. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/sounds/packs/classic/pause_start.wav +0 -0
  63. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/sounds/packs/classic/prep_start.wav +0 -0
  64. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/sounds/packs/classic/shoot_start.wav +0 -0
  65. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/sounds/packs/classic/warning_orange.wav +0 -0
  66. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/targets/flint_20cm_4spot.jpg +0 -0
  67. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/targets/flint_35cm_1spot.jpg +0 -0
  68. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/targets/indoor_compound.jpg +0 -0
  69. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/_defaults/targets/indoor_recurve.jpg +0 -0
  70. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/config.html +0 -0
  71. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/control.html +0 -0
  72. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/display.html +0 -0
  73. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/i18n.js +0 -0
  74. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/index.html +0 -0
  75. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/logo.svg +0 -0
  76. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/manual.html +0 -0
  77. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/screenshots/config.png +0 -0
  78. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/screenshots/control.png +0 -0
  79. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/screenshots/display.png +0 -0
  80. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/screenshots/index.png +0 -0
  81. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime/web/theme.js +0 -0
  82. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime.egg-info/SOURCES.txt +0 -0
  83. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime.egg-info/dependency_links.txt +0 -0
  84. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime.egg-info/entry_points.txt +0 -0
  85. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime.egg-info/requires.txt +0 -0
  86. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime.egg-info/scm_file_list.json +0 -0
  87. {fletchtime-0.2.1 → fletchtime-0.2.2}/src/fletchtime.egg-info/top_level.txt +0 -0
  88. {fletchtime-0.2.1 → fletchtime-0.2.2}/tests/test_config_store.py +0 -0
  89. {fletchtime-0.2.1 → fletchtime-0.2.2}/tests/test_engine.py +0 -0
  90. {fletchtime-0.2.1 → fletchtime-0.2.2}/tests/test_flint_mode.py +0 -0
  91. {fletchtime-0.2.1 → fletchtime-0.2.2}/tests/test_indoor_mode.py +0 -0
  92. {fletchtime-0.2.1 → fletchtime-0.2.2}/tests/test_match_server.py +0 -0
  93. {fletchtime-0.2.1 → fletchtime-0.2.2}/tests/test_runtime.py +0 -0
  94. {fletchtime-0.2.1 → fletchtime-0.2.2}/web/assets/banners/README.md +0 -0
  95. {fletchtime-0.2.1 → fletchtime-0.2.2}/web/assets/sounds/packs/README.md +0 -0
@@ -27,6 +27,10 @@ docs/_build/
27
27
  # vrai secret et ne doit jamais être versionné.
28
28
  config/auth.toml
29
29
 
30
+ # Préférences de la fenêtre graphique (thème...) -- propres à la machine/
31
+ # personne qui lance la fenêtre, pas un réglage de club à partager.
32
+ config/gui.toml
33
+
30
34
  # Instantané du match en cours (récupération après crash) -- propre à une
31
35
  # session de match en cours, pas un réglage du club à partager/versionner.
32
36
  config/match_state.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fletchtime
3
- Version: 0.2.1
3
+ Version: 0.2.2
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
@@ -21,6 +21,11 @@ Dynamic: license-file
21
21
 
22
22
  <img src="https://mrfanghodev.github.io/fletchtime/_static/logo.svg" width="80" height="80" alt="FletchTime logo">
23
23
 
24
+ [![Python](https://img.shields.io/badge/python-version-from-pep-621-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FMrFanghoDev%2Ffletchtime%2Fmaster%2Fpyproject.toml)](pyproject.toml)
25
+ [![Licence](https://img.shields.io/github/license/MrFanghoDev/fletchtime)](LICENSE)
26
+ [![Tests](https://github.com/MrFanghoDev/fletchtime/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/MrFanghoDev/fletchtime/actions/workflows/test.yml)
27
+ [![PyPI](https://img.shields.io/pypi/v/fletchtime)](https://pypi.org/project/fletchtime/)
28
+
24
29
  # FletchTime
25
30
 
26
31
  Logiciel open source de gestion du temps pour compétitions d'archerie FFTL
@@ -122,6 +127,29 @@ from fletchtime.engine import IndoorMode, IndoorConfig, MatchEngine
122
127
  engine = MatchEngine(IndoorMode(IndoorConfig()))
123
128
  ```
124
129
 
130
+ ## Plusieurs salles de compétition sur un même PC
131
+
132
+ Chaque installation de FletchTime (dossier `pip install`, ou dossier de
133
+ l'exécutable autoporteur) fait tourner **une seule salle** -- son propre
134
+ match, sa propre urgence, sa propre config. Pour plusieurs salles en
135
+ parallèle sur le même PC :
136
+
137
+ 1. **Copie le dossier FletchTime une fois par salle** (ex. `salle-A/`,
138
+ `salle-B/`) -- ça isole automatiquement `config/` (temps de tir,
139
+ urgence...), les journaux, et l'instantané de récupération après
140
+ plantage : chaque copie a sa propre urgence, indépendante des autres.
141
+ 2. Dans la fenêtre de chaque copie, section **Ports (HTTP/WS)** : donne
142
+ des ports différents à chaque salle (ex. salle A garde `8000`/`8765`
143
+ par défaut, salle B passe à `8001`/`8766`) -- **Appliquer** redémarre
144
+ le serveur automatiquement si besoin.
145
+ 3. Lance chaque copie séparément. Les écrans de chaque salle se
146
+ connectent à l'adresse (IP + port) affichée dans la fenêtre de leur
147
+ salle respective.
148
+
149
+ **Windows** : le pare-feu autorise généralement par chemin d'exécutable --
150
+ avec des copies de dossier séparées, prévoir de valider l'alerte une fois
151
+ par copie au premier lancement.
152
+
125
153
  ## Contribuer
126
154
 
127
155
  Envie de proposer un correctif, une idée, ou juste signaler un bug ? Voir
@@ -1,5 +1,10 @@
1
1
  <img src="https://mrfanghodev.github.io/fletchtime/_static/logo.svg" width="80" height="80" alt="FletchTime logo">
2
2
 
3
+ [![Python](https://img.shields.io/badge/python-version-from-pep-621-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FMrFanghoDev%2Ffletchtime%2Fmaster%2Fpyproject.toml)](pyproject.toml)
4
+ [![Licence](https://img.shields.io/github/license/MrFanghoDev/fletchtime)](LICENSE)
5
+ [![Tests](https://github.com/MrFanghoDev/fletchtime/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/MrFanghoDev/fletchtime/actions/workflows/test.yml)
6
+ [![PyPI](https://img.shields.io/pypi/v/fletchtime)](https://pypi.org/project/fletchtime/)
7
+
3
8
  # FletchTime
4
9
 
5
10
  Logiciel open source de gestion du temps pour compétitions d'archerie FFTL
@@ -101,6 +106,29 @@ from fletchtime.engine import IndoorMode, IndoorConfig, MatchEngine
101
106
  engine = MatchEngine(IndoorMode(IndoorConfig()))
102
107
  ```
103
108
 
109
+ ## Plusieurs salles de compétition sur un même PC
110
+
111
+ Chaque installation de FletchTime (dossier `pip install`, ou dossier de
112
+ l'exécutable autoporteur) fait tourner **une seule salle** -- son propre
113
+ match, sa propre urgence, sa propre config. Pour plusieurs salles en
114
+ parallèle sur le même PC :
115
+
116
+ 1. **Copie le dossier FletchTime une fois par salle** (ex. `salle-A/`,
117
+ `salle-B/`) -- ça isole automatiquement `config/` (temps de tir,
118
+ urgence...), les journaux, et l'instantané de récupération après
119
+ plantage : chaque copie a sa propre urgence, indépendante des autres.
120
+ 2. Dans la fenêtre de chaque copie, section **Ports (HTTP/WS)** : donne
121
+ des ports différents à chaque salle (ex. salle A garde `8000`/`8765`
122
+ par défaut, salle B passe à `8001`/`8766`) -- **Appliquer** redémarre
123
+ le serveur automatiquement si besoin.
124
+ 3. Lance chaque copie séparément. Les écrans de chaque salle se
125
+ connectent à l'adresse (IP + port) affichée dans la fenêtre de leur
126
+ salle respective.
127
+
128
+ **Windows** : le pare-feu autorise généralement par chemin d'exécutable --
129
+ avec des copies de dossier séparées, prévoir de valider l'alerte une fois
130
+ par copie au premier lancement.
131
+
104
132
  ## Contribuer
105
133
 
106
134
  Envie de proposer un correctif, une idée, ou juste signaler un bug ? Voir
@@ -1,3 +1,14 @@
1
+ ```{image} _static/logo.svg
2
+ :alt: FletchTime
3
+ :width: 140px
4
+ :align: center
5
+ ```
6
+
7
+ [![Python](https://img.shields.io/badge/python-version-from-pep-621-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FMrFanghoDev%2Ffletchtime%2Fmaster%2Fpyproject.toml)](https://github.com/MrFanghoDev/fletchtime/blob/master/pyproject.toml)
8
+ [![Licence](https://img.shields.io/github/license/MrFanghoDev/fletchtime)](https://github.com/MrFanghoDev/fletchtime/blob/master/LICENSE)
9
+ [![Tests](https://github.com/MrFanghoDev/fletchtime/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/MrFanghoDev/fletchtime/actions/workflows/test.yml)
10
+ [![PyPI](https://img.shields.io/pypi/v/fletchtime)](https://pypi.org/project/fletchtime/)
11
+
1
12
  # FletchTime
2
13
 
3
14
  Logiciel de gestion du temps pour compétitions d'archerie FFTL (Indoor et Flint),
@@ -79,20 +79,29 @@ dépendent d'extensions compilées (Rust/C) peu fiables sur Pydroid 3, voir
79
79
  (`logs/fletchtime.log`), en plus du journal déjà affiché dans la
80
80
  fenêtre (en mémoire, perdu à la fermeture). Voir {doc}`architecture`,
81
81
  section dédiée -- vérifié que le mot de passe ne s'y retrouve jamais.
82
- - **Fenêtre graphique -- améliorations restant à faire** :
83
- - Paramétrage du serveur (ports, etc.) et de l'adresse IP exposée sur le
84
- réseau local, directement depuis la fenêtre plutôt que fichiers de
85
- config seuls.
82
+ - ~~**Fenêtre graphique -- améliorations**~~ -- toutes faites (paramétrage
83
+ réseau, affichage des adresses, thème, couleur du bouton Quitter) :
84
+ - ~~Paramétrage du serveur et de l'adresse IP exposée~~ -- fait :
85
+ ports HTTP/WebSocket modifiables directement dans la fenêtre
86
+ (`config/gui.toml`), avec redémarrage automatique du serveur si les
87
+ ports changent pendant qu'il tourne. Objectif principal : plusieurs
88
+ salles de compétition sur un même PC -- copier le dossier FletchTime
89
+ une fois par salle (isole aussi `config/`, `logs/`, l'instantané de
90
+ récupération, donc des urgences bien séparées), donner des ports
91
+ différents à chaque copie, lancer chacune séparément. Testé
92
+ concrètement : deux instances simultanées sur le même processus,
93
+ ports différents, contenu totalement isolé entre les deux.
86
94
  - ~~Affichage des adresses exposées~~ -- fait : champ dédié (adresse
87
95
  d'accueil) dans la fenêtre elle-même, en plus des boutons de
88
96
  raccourci -- utile pour retaper l'adresse à la main sur un appareil
89
97
  sans bouton de raccourci. Non vérifié visuellement si le texte reste
90
98
  bien sélectionnable/copiable une fois le champ désactivé (`CTkEntry`
91
99
  n'a pas de vrai état "readonly" contrairement à `ttk.Entry`).
92
- - Choix du thème clair/sombre dans la fenêtre -- actuellement forcé en
93
- sombre (`ctk.set_appearance_mode("dark")`), cohérent avec
94
- `display.html` mais pas ajustable par l'utilisateur comme le sont les
95
- autres pages web.
100
+ - ~~Choix du thème clair/sombre dans la fenêtre~~ -- fait : sélecteur
101
+ Système/Clair/Sombre dans l'en-tête, préférence persistée dans
102
+ `config/gui.toml` (jamais versionné, propre à la machine -- voir
103
+ `.gitignore`). Indépendant de `display.html`, qui reste lui, par
104
+ choix délibéré, toujours sombre.
96
105
  - ~~Couleur du bouton Quitter à revoir~~ -- fait : couleur pleine
97
106
  (`#3a4354`) au lieu de transparent+contour, visible sans être
98
107
  alarmante (distincte du rouge d'Arrêter et du doré de Démarrer).
@@ -30,9 +30,15 @@ from pathlib import Path
30
30
  from fletchtime import __version__
31
31
  from fletchtime.logging_setup import configure_logging
32
32
  from fletchtime.runtime import ServerRuntime
33
+ from fletchtime.server import config_store
33
34
 
34
- HTTP_PORT = 8000
35
- WS_PORT = 8765
35
+ # Anciennement des constantes HTTP_PORT/WS_PORT figées ici -- les ports
36
+ # par défaut vivent maintenant uniquement dans
37
+ # config_store.DEFAULT_GUI_CONFIG, seule source de vérité (voir
38
+ # _run_headless ci-dessous et fletchtime.gui, qui lisent tous deux
39
+ # config_store.load_gui_config() plutôt qu'une constante figée). Permet
40
+ # de faire tourner plusieurs salles de compétition sur un même PC : une
41
+ # copie de dossier par salle, chacune avec son propre config/gui.toml.
36
42
 
37
43
  # Dossiers pré-remplis avec un contenu par défaut fourni avec FletchTime
38
44
  # (copié une seule fois, au tout premier lancement -- jamais écrasé
@@ -113,14 +119,14 @@ def local_ip() -> str:
113
119
  s.close()
114
120
 
115
121
 
116
- def _print_banner(ip: str, data_root: Path) -> None:
122
+ def _print_banner(ip: str, data_root: Path, http_port: int) -> None:
117
123
  print("=" * 60)
118
124
  print(f" FletchTime {__version__} -- serveur de contrôle et d'affichage")
119
125
  print(f" FletchTime {__version__} -- control and display server")
120
126
  print("=" * 60)
121
- print(f" Accueil / Home : http://{ip}:{HTTP_PORT}/")
122
- print(f" Contrôle / Control : http://{ip}:{HTTP_PORT}/control.html")
123
- print(f" Affichage / Display : http://{ip}:{HTTP_PORT}/display.html?lane=1")
127
+ print(f" Accueil / Home : http://{ip}:{http_port}/")
128
+ print(f" Contrôle / Control : http://{ip}:{http_port}/control.html")
129
+ print(f" Affichage / Display : http://{ip}:{http_port}/display.html?lane=1")
124
130
  print()
125
131
  print(" Depuis CET appareil, remplace l'IP par 127.0.0.1 si besoin.")
126
132
  print(" From THIS device, replace the IP with 127.0.0.1 if needed.")
@@ -156,9 +162,19 @@ def _run_headless() -> None:
156
162
  log_file = configure_logging(data_root / "logs")
157
163
  print(f"Journal détaillé / Detailed log: {log_file}")
158
164
 
159
- _print_banner(local_ip(), data_root)
165
+ # Ports lus depuis config/gui.toml (mêmes préférences que la fenêtre
166
+ # graphique, voir config_store.load_gui_config) -- permet de faire
167
+ # tourner plusieurs salles de compétition sur le même PC : une copie
168
+ # de dossier par salle, chacune avec son propre gui.toml donnant des
169
+ # ports différents. "gui.toml" malgré le nom concerne aussi ce mode
170
+ # terminal, pour que les deux se comportent de façon cohérente.
171
+ gui_config = config_store.load_gui_config()
172
+ http_port = gui_config["http_port"]
173
+ ws_port = gui_config["ws_port"]
160
174
 
161
- runtime = ServerRuntime(str(app_web_dir), str(assets_dir), HTTP_PORT, WS_PORT)
175
+ _print_banner(local_ip(), data_root, http_port)
176
+
177
+ runtime = ServerRuntime(str(app_web_dir), str(assets_dir), http_port, ws_port)
162
178
  runtime.start()
163
179
 
164
180
  stop_event = threading.Event()
@@ -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.1'
22
- __version_tuple__ = version_tuple = (0, 2, 1)
21
+ __version__ = version = '0.2.2'
22
+ __version_tuple__ = version_tuple = (0, 2, 2)
23
23
 
24
- __commit_id__ = commit_id = 'g4c8543661'
24
+ __commit_id__ = commit_id = 'gceb71f360'
@@ -32,8 +32,6 @@ import customtkinter as ctk
32
32
 
33
33
  from fletchtime import __version__
34
34
  from fletchtime.__main__ import (
35
- HTTP_PORT,
36
- WS_PORT,
37
35
  _app_web_dir,
38
36
  _data_root,
39
37
  ensure_directories,
@@ -41,6 +39,7 @@ from fletchtime.__main__ import (
41
39
  )
42
40
  from fletchtime.logging_setup import configure_logging
43
41
  from fletchtime.runtime import ServerRuntime
42
+ from fletchtime.server import config_store
44
43
 
45
44
  _TRANSLATIONS = {
46
45
  "fr": {
@@ -56,6 +55,15 @@ _TRANSLATIONS = {
56
55
  "log_title": "Journal",
57
56
  "club_data": "Données du club :",
58
57
  "addressCaption": "Adresse :",
58
+ "themeSystem": "Système",
59
+ "themeLight": "Clair",
60
+ "themeDark": "Sombre",
61
+ "networkCaption": "Ports (HTTP/WS) :",
62
+ "apply": "Appliquer",
63
+ "networkErrorNotANumber": "Les ports doivent être des nombres",
64
+ "networkErrorRange": "Les ports doivent être entre 1 et 65535",
65
+ "networkErrorSame": "Les deux ports doivent être différents",
66
+ "networkApplied": "Appliqué",
59
67
  },
60
68
  "en": {
61
69
  "title": "FletchTime -- Server",
@@ -70,6 +78,15 @@ _TRANSLATIONS = {
70
78
  "log_title": "Log",
71
79
  "club_data": "Club data:",
72
80
  "addressCaption": "Address:",
81
+ "themeSystem": "System",
82
+ "themeLight": "Light",
83
+ "themeDark": "Dark",
84
+ "networkCaption": "Ports (HTTP/WS):",
85
+ "apply": "Apply",
86
+ "networkErrorNotANumber": "Ports must be numbers",
87
+ "networkErrorRange": "Ports must be between 1 and 65535",
88
+ "networkErrorSame": "Both ports must be different",
89
+ "networkApplied": "Applied",
73
90
  },
74
91
  }
75
92
 
@@ -160,10 +177,7 @@ class FletchTimeApp(ctk.CTk):
160
177
  # Doit être fait AVANT super().__init__() : customtkinter applique
161
178
  # le thème au moment de la construction de chaque widget, fenêtre
162
179
  # racine comprise -- appelé après, la fenêtre elle-même garderait
163
- # le thème par défaut. Mode sombre forcé (pas "system") : reprend
164
- # la même palette que les pages web en thème sombre, cohérent
165
- # avec display.html qui reste lui aussi toujours sombre par choix
166
- # délibéré (voir docs/architecture.md).
180
+ # le thème par défaut.
167
181
  #
168
182
  # Part d'un thème intégré à customtkinter ("dark-blue" -- garanti
169
183
  # complet pour la version installée) plutôt qu'un JSON entièrement
@@ -174,7 +188,20 @@ class FletchTimeApp(ctk.CTk):
174
188
  # les couleurs sont ensuite surchargées (voir _apply_brand_colors),
175
189
  # tout le reste (rayons, épaisseurs de bordure...) reste celui,
176
190
  # déjà testé, du thème intégré.
177
- ctk.set_appearance_mode("dark")
191
+ #
192
+ # Thème choisi par la personne qui lance la fenêtre (voir
193
+ # config/gui.toml, config_store.load_gui_config) -- "system" par
194
+ # défaut, qui suit le thème du système d'exploitation (sauf sous
195
+ # Linux, limitation connue de customtkinter -- retombe sur
196
+ # "light"). Indépendant de display.html, qui reste lui, par choix
197
+ # délibéré, toujours sombre (voir docs/architecture.md) : l'écran
198
+ # vu par les archers et la fenêtre de contrôle du responsable du
199
+ # chronométrage sont deux préoccupations distinctes.
200
+ gui_config = config_store.load_gui_config()
201
+ self.theme = gui_config["theme"]
202
+ self.http_port = gui_config["http_port"]
203
+ self.ws_port = gui_config["ws_port"]
204
+ ctk.set_appearance_mode(self.theme)
178
205
  ctk.set_default_color_theme("dark-blue")
179
206
  _apply_brand_colors()
180
207
 
@@ -188,7 +215,7 @@ class FletchTimeApp(ctk.CTk):
188
215
  ensure_directories(self.data_root, self.app_web_dir)
189
216
  self.assets_dir = self.data_root / "web" / "assets"
190
217
  self.runtime = ServerRuntime(
191
- str(self.app_web_dir), str(self.assets_dir), HTTP_PORT, WS_PORT
218
+ str(self.app_web_dir), str(self.assets_dir), self.http_port, self.ws_port
192
219
  )
193
220
 
194
221
  # Capte le journal d'accès HTTP (http.server écrit sur stderr) et
@@ -236,6 +263,15 @@ class FletchTimeApp(ctk.CTk):
236
263
  text_color="gray60",
237
264
  ).pack(side="left", padx=(0, 16), pady=12)
238
265
 
266
+ self.theme_menu = ctk.CTkOptionMenu(
267
+ header,
268
+ values=[self._t("themeSystem"), self._t("themeLight"), self._t("themeDark")],
269
+ width=110,
270
+ command=self._on_theme_change,
271
+ )
272
+ self.theme_menu.set(self._theme_label(self.theme))
273
+ self.theme_menu.pack(side="right", padx=(0, 8), pady=12)
274
+
239
275
  self.lang_menu = ctk.CTkOptionMenu(
240
276
  header, values=["FR", "EN"], width=70, command=self._on_language_change
241
277
  )
@@ -315,6 +351,43 @@ class FletchTimeApp(ctk.CTk):
315
351
  )
316
352
  self.address_entry.pack(side="left", padx=(0, 12), pady=8, expand=True, fill="x")
317
353
 
354
+ # -- réseau (ports) -----------------------------------------------
355
+ # Ports modifiables plutôt que figés : permet de faire tourner
356
+ # plusieurs salles de compétition sur le même PC -- une copie de
357
+ # dossier par salle, chacune avec des ports différents (voir
358
+ # config/gui.toml, config_store). Changer un port ici redémarre le
359
+ # serveur automatiquement s'il tournait déjà, pour que le
360
+ # changement prenne effet immédiatement (voir _on_apply_network).
361
+ network_frame = ctk.CTkFrame(self)
362
+ network_frame.pack(fill="x", padx=16, pady=(0, 8))
363
+
364
+ self.network_caption = ctk.CTkLabel(
365
+ network_frame,
366
+ text=self._t("networkCaption"),
367
+ font=ctk.CTkFont(size=11),
368
+ text_color="gray60",
369
+ )
370
+ self.network_caption.pack(side="left", padx=(12, 8), pady=8)
371
+
372
+ self.http_port_entry = ctk.CTkEntry(network_frame, width=70, justify="center")
373
+ self.http_port_entry.insert(0, str(self.http_port))
374
+ self.http_port_entry.pack(side="left", padx=(0, 4), pady=8)
375
+
376
+ self.network_separator = ctk.CTkLabel(network_frame, text="/", text_color="gray60")
377
+ self.network_separator.pack(side="left", padx=2, pady=8)
378
+
379
+ self.ws_port_entry = ctk.CTkEntry(network_frame, width=70, justify="center")
380
+ self.ws_port_entry.insert(0, str(self.ws_port))
381
+ self.ws_port_entry.pack(side="left", padx=(4, 12), pady=8)
382
+
383
+ self.network_apply_button = ctk.CTkButton(
384
+ network_frame, text=self._t("apply"), width=90, command=self._on_apply_network
385
+ )
386
+ self.network_apply_button.pack(side="left", padx=(0, 8), pady=8)
387
+
388
+ self.network_status_label = ctk.CTkLabel(network_frame, text="", text_color="gray60")
389
+ self.network_status_label.pack(side="left", padx=(4, 12), pady=8, fill="x", expand=True)
390
+
318
391
  # -- journal ----------------------------------------------------
319
392
  self.log_label = ctk.CTkLabel(self, text=self._t("log_title"), anchor="w")
320
393
  self.log_label.pack(fill="x", padx=20, pady=(8, 0))
@@ -345,7 +418,7 @@ class FletchTimeApp(ctk.CTk):
345
418
  # -- actions ---------------------------------------------------------
346
419
 
347
420
  def _open_link(self, path: str) -> None:
348
- webbrowser.open(f"http://127.0.0.1:{HTTP_PORT}{path}")
421
+ webbrowser.open(f"http://127.0.0.1:{self.http_port}{path}")
349
422
 
350
423
  def _start_server(self) -> None:
351
424
  if self.runtime.is_running:
@@ -372,7 +445,7 @@ class FletchTimeApp(ctk.CTk):
372
445
  """L'adresse ne dépend pas de si le serveur tourne réellement --
373
446
  affichée dans tous les cas (utile même à l'arrêt, pour préparer
374
447
  la config réseau à l'avance sur un autre appareil)."""
375
- address = f"http://{local_ip()}:{HTTP_PORT}/"
448
+ address = f"http://{local_ip()}:{self.http_port}/"
376
449
  # CTkEntry ne supporte que "normal"/"disabled" (contrairement à
377
450
  # ttk.Entry, qui a un vrai état "readonly") -- il faut donc la
378
451
  # débloquer temporairement pour la mettre à jour, puis la
@@ -394,6 +467,88 @@ class FletchTimeApp(ctk.CTk):
394
467
  self.log_label.configure(text=self._t("log_title"))
395
468
  self.quit_button.configure(text=self._t("quit"))
396
469
  self.footer_label.configure(text=f"{self._t('club_data')} {self.data_root}")
470
+ # Les valeurs du menu de thème sont des libellés traduits (pas des
471
+ # codes internes comme "FR"/"EN") -- il faut donc reconstruire la
472
+ # liste ET repositionner la sélection sur le thème actuel, pas
473
+ # juste changer le texte d'un widget existant.
474
+ self.theme_menu.configure(
475
+ values=[self._t("themeSystem"), self._t("themeLight"), self._t("themeDark")]
476
+ )
477
+ self.theme_menu.set(self._theme_label(self.theme))
478
+ self.network_caption.configure(text=self._t("networkCaption"))
479
+ self.network_apply_button.configure(text=self._t("apply"))
480
+ self._refresh_status()
481
+
482
+ def _theme_label(self, theme: str) -> str:
483
+ return {
484
+ "system": self._t("themeSystem"),
485
+ "light": self._t("themeLight"),
486
+ "dark": self._t("themeDark"),
487
+ }.get(theme, self._t("themeSystem"))
488
+
489
+ def _on_theme_change(self, label: str) -> None:
490
+ reverse = {
491
+ self._t("themeSystem"): "system",
492
+ self._t("themeLight"): "light",
493
+ self._t("themeDark"): "dark",
494
+ }
495
+ theme = reverse.get(label, "system")
496
+ self.theme = theme
497
+ ctk.set_appearance_mode(theme)
498
+ config_store.save_gui_config({"theme": theme})
499
+
500
+ def _on_apply_network(self) -> None:
501
+ """Change les ports HTTP/WebSocket -- reconstruit ServerRuntime
502
+ avec les nouvelles valeurs (les ports sont figés à la
503
+ construction, voir fletchtime.runtime.ServerRuntime) et
504
+ redémarre automatiquement si le serveur tournait déjà, pour que
505
+ le changement prenne effet tout de suite plutôt que de laisser
506
+ la fenêtre dans un état incohérent (ports affichés différents de
507
+ ceux réellement utilisés par le serveur en cours)."""
508
+ try:
509
+ http_port = int(self.http_port_entry.get())
510
+ ws_port = int(self.ws_port_entry.get())
511
+ except ValueError:
512
+ self.network_status_label.configure(
513
+ text=self._t("networkErrorNotANumber"), text_color="#d6534a"
514
+ )
515
+ return
516
+
517
+ if not (1 <= http_port <= 65535) or not (1 <= ws_port <= 65535):
518
+ self.network_status_label.configure(
519
+ text=self._t("networkErrorRange"), text_color="#d6534a"
520
+ )
521
+ return
522
+ if http_port == ws_port:
523
+ self.network_status_label.configure(
524
+ text=self._t("networkErrorSame"), text_color="#d6534a"
525
+ )
526
+ return
527
+
528
+ try:
529
+ config_store.save_gui_config({"http_port": http_port, "ws_port": ws_port})
530
+ except ValueError:
531
+ # Filet de sécurité seulement -- les cas attendus sont déjà
532
+ # couverts par les vérifications ci-dessus, donc ce chemin ne
533
+ # devrait normalement jamais s'exécuter.
534
+ self.network_status_label.configure(
535
+ text=self._t("networkErrorRange"), text_color="#d6534a"
536
+ )
537
+ return
538
+
539
+ was_running = self.runtime.is_running
540
+ if was_running:
541
+ self.runtime.stop()
542
+
543
+ self.http_port = http_port
544
+ self.ws_port = ws_port
545
+ self.runtime = ServerRuntime(
546
+ str(self.app_web_dir), str(self.assets_dir), self.http_port, self.ws_port
547
+ )
548
+ if was_running:
549
+ self.runtime.start()
550
+
551
+ self.network_status_label.configure(text=self._t("networkApplied"), text_color="#2fb344")
397
552
  self._refresh_status()
398
553
 
399
554
  def _on_quit(self) -> None:
@@ -47,6 +47,11 @@ AUTH_TOML = CONFIG_DIR / "auth.toml"
47
47
  # (voir .gitignore) : propre à une session de match en cours, pas un
48
48
  # réglage du club.
49
49
  MATCH_STATE_JSON = CONFIG_DIR / "match_state.json"
50
+ # Préférences de la fenêtre graphique (thème...) -- fichier séparé de
51
+ # app.toml exprès : ce ne sont pas des réglages du match/serveur diffusés
52
+ # aux écrans web, juste des préférences purement locales à qui lance la
53
+ # fenêtre sur cette machine précise.
54
+ GUI_TOML = CONFIG_DIR / "gui.toml"
50
55
 
51
56
  APP_COMMENTS: dict[str, str] = {
52
57
  "sound_pack": "Nom du dossier dans web/assets/sounds/packs/ à utiliser (ex. classic)",
@@ -54,6 +59,13 @@ APP_COMMENTS: dict[str, str] = {
54
59
  }
55
60
  DEFAULT_APP_CONFIG: dict[str, Any] = {"sound_pack": "classic", "countdown_tick_seconds": 5}
56
61
 
62
+ GUI_COMMENTS: dict[str, str] = {
63
+ "theme": 'Thème de la fenêtre : "system" (suit le thème du système), "light" ou "dark"',
64
+ "http_port": "Port HTTP (pages web) -- change ceci pour faire tourner plusieurs salles sur le même PC",
65
+ "ws_port": "Port WebSocket (temps réel) -- doit être différent du port HTTP",
66
+ }
67
+ DEFAULT_GUI_CONFIG: dict[str, Any] = {"theme": "system", "http_port": 8000, "ws_port": 8765}
68
+
57
69
  AUTH_COMMENTS: dict[str, str] = {
58
70
  "password": (
59
71
  "Mot de passe requis pour piloter le match et modifier la "
@@ -148,6 +160,31 @@ def save_app_config(overrides: dict[str, Any]) -> dict[str, Any]:
148
160
  return merged
149
161
 
150
162
 
163
+ def load_gui_config() -> dict[str, Any]:
164
+ """Préférences de la fenêtre graphique -- voir GUI_TOML. Comme
165
+ load_app_config, un plain dict plutôt qu'une dataclass formelle : un
166
+ seul champ aujourd'hui, peut grandir."""
167
+ data = _load_toml(GUI_TOML)
168
+ merged = dict(DEFAULT_GUI_CONFIG)
169
+ merged.update({k: v for k, v in data.items() if k in DEFAULT_GUI_CONFIG})
170
+ return merged
171
+
172
+
173
+ def save_gui_config(overrides: dict[str, Any]) -> dict[str, Any]:
174
+ merged = load_gui_config()
175
+ merged.update({k: v for k, v in overrides.items() if k in DEFAULT_GUI_CONFIG})
176
+ if merged["theme"] not in ("system", "light", "dark"):
177
+ raise ValueError(f'theme must be "system", "light" or "dark", got {merged["theme"]!r}')
178
+ for key in ("http_port", "ws_port"):
179
+ port = merged[key]
180
+ if not isinstance(port, int) or not (1 <= port <= 65535):
181
+ raise ValueError(f"{key} must be an integer between 1 and 65535, got {port!r}")
182
+ if merged["http_port"] == merged["ws_port"]:
183
+ raise ValueError("http_port and ws_port must be different")
184
+ _write_toml(GUI_TOML, merged, GUI_COMMENTS)
185
+ return merged
186
+
187
+
151
188
  def load_auth_config() -> dict[str, Any]:
152
189
  data = _load_toml(AUTH_TOML)
153
190
  merged = dict(DEFAULT_AUTH_CONFIG)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fletchtime
3
- Version: 0.2.1
3
+ Version: 0.2.2
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
@@ -21,6 +21,11 @@ Dynamic: license-file
21
21
 
22
22
  <img src="https://mrfanghodev.github.io/fletchtime/_static/logo.svg" width="80" height="80" alt="FletchTime logo">
23
23
 
24
+ [![Python](https://img.shields.io/badge/python-version-from-pep-621-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FMrFanghoDev%2Ffletchtime%2Fmaster%2Fpyproject.toml)](pyproject.toml)
25
+ [![Licence](https://img.shields.io/github/license/MrFanghoDev/fletchtime)](LICENSE)
26
+ [![Tests](https://github.com/MrFanghoDev/fletchtime/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/MrFanghoDev/fletchtime/actions/workflows/test.yml)
27
+ [![PyPI](https://img.shields.io/pypi/v/fletchtime)](https://pypi.org/project/fletchtime/)
28
+
24
29
  # FletchTime
25
30
 
26
31
  Logiciel open source de gestion du temps pour compétitions d'archerie FFTL
@@ -122,6 +127,29 @@ from fletchtime.engine import IndoorMode, IndoorConfig, MatchEngine
122
127
  engine = MatchEngine(IndoorMode(IndoorConfig()))
123
128
  ```
124
129
 
130
+ ## Plusieurs salles de compétition sur un même PC
131
+
132
+ Chaque installation de FletchTime (dossier `pip install`, ou dossier de
133
+ l'exécutable autoporteur) fait tourner **une seule salle** -- son propre
134
+ match, sa propre urgence, sa propre config. Pour plusieurs salles en
135
+ parallèle sur le même PC :
136
+
137
+ 1. **Copie le dossier FletchTime une fois par salle** (ex. `salle-A/`,
138
+ `salle-B/`) -- ça isole automatiquement `config/` (temps de tir,
139
+ urgence...), les journaux, et l'instantané de récupération après
140
+ plantage : chaque copie a sa propre urgence, indépendante des autres.
141
+ 2. Dans la fenêtre de chaque copie, section **Ports (HTTP/WS)** : donne
142
+ des ports différents à chaque salle (ex. salle A garde `8000`/`8765`
143
+ par défaut, salle B passe à `8001`/`8766`) -- **Appliquer** redémarre
144
+ le serveur automatiquement si besoin.
145
+ 3. Lance chaque copie séparément. Les écrans de chaque salle se
146
+ connectent à l'adresse (IP + port) affichée dans la fenêtre de leur
147
+ salle respective.
148
+
149
+ **Windows** : le pare-feu autorise généralement par chemin d'exécutable --
150
+ avec des copies de dossier séparées, prévoir de valider l'alerte une fois
151
+ par copie au premier lancement.
152
+
125
153
  ## Contribuer
126
154
 
127
155
  Envie de proposer un correctif, une idée, ou juste signaler un bug ? Voir
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "0.2.1",
2
+ "tag": "0.2.2",
3
3
  "distance": 0,
4
- "node": "g4c854366173a38e914c10115cdfde6cc1c085eab",
4
+ "node": "gceb71f360e75d8fddce1758cdf80d1ca1f4431b7",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-07-19"
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