35 Commits

Author SHA1 Message Date
gitea 5715da67de chore: wydzielenie nucleares_monitor do osobnego repo
Monitor reaktora (Nucleares) przeniesiony do github.com/migatu/nucleares_monitor.
requirements.txt usunięty — pozostałe skrypty Foundry używają wyłącznie stdlib.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 00:20:31 +02:00
gitea a2a723a49f sync: help_scripts v0.5 2025-10-29 17:28:35 +01:00
gitea deb1cc55fa sync: root v0.4 2025-10-29 17:28:32 +01:00
gitea 93cbd4eb01 sync: docs v0.1 2025-10-29 16:10:31 +01:00
gitea 17b2747130 sync: nucleares_monitor v1.17 2025-10-29 15:51:29 +01:00
gitea 4677df4b80 sync: help_scripts v0.4 2025-10-29 15:51:25 +01:00
gitea e0916f4845 Update of conf 2025-10-22 19:09:20 +02:00
gitea 1cc28b18a1 v 1.17 interim 2025-10-22 14:03:19 +02:00
gitea 0c365665c0 gitignore 2025-10-21 23:11:05 +02:00
gitea e730729493 v 1.17 2025-10-21 23:09:12 +02:00
gitea 53c33c888d 1.16-interim2 2025-10-21 12:28:39 +02:00
gitea 692bcb3b4b 1.16-interim 2025-10-21 09:18:28 +02:00
gitea 4d0f601c7c v 1.15 2025-10-20 21:31:57 +02:00
gitea c0a46c7bef 1.14 2025-10-20 18:32:08 +02:00
gitea 04923cc449 1.13 2025-10-20 18:31:44 +02:00
gitea 4beb01118a 1.12 2025-10-20 18:31:30 +02:00
gitea e06e297540 1.11 2025-10-20 18:31:12 +02:00
gitea 8d5aac56d1 1.10 2025-10-20 18:30:59 +02:00
gitea 45c98308fb 1.9 2025-10-20 18:30:31 +02:00
gitea 9896a811f8 1.8 2025-10-20 18:30:17 +02:00
gitea fd18fcf0bd 1.7 2025-10-20 18:29:58 +02:00
gitea 7928d17ea9 1.6 2025-10-20 18:29:46 +02:00
gitea 5a873b4e6e 1.5 2025-10-20 18:29:29 +02:00
gitea cac380ca5e 1.4 2025-10-20 18:29:13 +02:00
gitea 792694f6ac 1.3 2025-10-20 18:28:57 +02:00
gitea e644daff99 1.2 2025-10-20 18:28:33 +02:00
gitea 3b88f4c8eb 1.1 2025-10-20 18:28:18 +02:00
gitea a2fbabedce 1.0 2025-10-20 18:27:37 +02:00
gitea 23084f4cb4 0.9 2025-10-20 18:27:06 +02:00
gitea 65b3989099 0.8 2025-10-20 18:26:49 +02:00
gitea a73459ea87 0.7 2025-10-20 18:26:33 +02:00
gitea c8a439c2f3 0.6 2025-10-20 18:26:09 +02:00
gitea 311cb768b9 0.4 2025-10-20 18:25:50 +02:00
gitea 61a8cb40a4 0.3 2025-10-20 18:25:24 +02:00
gitea ca4a3b6523 0.1 2025-10-20 18:24:38 +02:00
14 changed files with 1216 additions and 540 deletions
+4
View File
@@ -0,0 +1,4 @@
*.pyc
*__pycache__/
*.log
*.tmp
+9
View File
@@ -0,0 +1,9 @@
*out
*logs
*actions
*notifications
*tools
plugins
user_trunk.yaml
user.yaml
tmp
+2
View File
@@ -0,0 +1,2 @@
[settings]
profile=black
+2
View File
@@ -0,0 +1,2 @@
# Prettier friendly markdownlint config (all formatting rules disabled)
extends: markdownlint/style/prettier
+5
View File
@@ -0,0 +1,5 @@
# Generic, formatter-friendly config.
select = ["B", "D3", "E", "F"]
# Never enforce `E501` (line length violations). This should be handled by formatters.
ignore = ["E501"]
+35
View File
@@ -0,0 +1,35 @@
# This file controls the behavior of Trunk: https://docs.trunk.io/cli
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
version: 0.1
cli:
version: 1.25.0
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
plugins:
sources:
- id: trunk
ref: v1.7.3
uri: https://github.com/trunk-io/plugins
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
runtimes:
enabled:
- node@22.16.0
- python@3.10.8
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
lint:
enabled:
- bandit@1.8.6
- black@25.9.0
- checkov@3.2.486
- git-diff-check
- isort@7.0.0
- markdownlint@0.45.0
- prettier@3.6.2
- ruff@0.14.1
- trufflehog@3.90.11
actions:
disabled:
- trunk-announce
- trunk-check-pre-push
- trunk-fmt-pre-commit
enabled:
- trunk-upgrade-available
+737
View File
@@ -0,0 +1,737 @@
# Release 0.3 Notes
History compiled from newest to oldest commit. Release-tagged commits are highlighted.
### 0.3
- Date: 2025-10-29
- Commit: f2a10ef4
- Summary: 0.3
- Files: version.nfo
### sync: nucleares_monitor v1.17
- Date: 2025-10-29
- Commit: 17b27471
- Summary: sync: nucleares_monitor v1.17
- Files: nucleares_monitor/version.nfo
### sync: help_scripts v0.4
- Date: 2025-10-29
- Commit: 4677df4b
- Summary: sync: help_scripts v0.4
- Files: help_scripts/sync_foundry_module.py, help_scripts/version.nfo, nucleares_monitor/control_board_monitor.py, nucleares_monitor/tests/test_arrange_windows.py, nucleares_monitor/tests/test_arrangement_fixes.py, nucleares_monitor/tests/test_high_priority_fixes.py (+4 more)
### Update of conf
- Date: 2025-10-22
- Commit: e0916f48
- Summary: Update of conf
- Files: .trunk/.gitignore, .trunk/configs/.isort.cfg, .trunk/configs/.markdownlint.yaml, .trunk/configs/ruff.toml, .trunk/trunk.yaml
### v 1.17 interim
- Date: 2025-10-21
- Commit: 1cc28b18
- Summary: v 1.17 interim
- Files: nucleares_monitor/control_board_monitor.py, requirements.txt, test_arrange_windows.py, test_arrangement_fixes.py
### gitignore
- Date: 2025-10-21
- Commit: 0c365665
- Summary: gitignore
- Files: .gitignore
### v 1.17
- Date: 2025-10-21
- Commit: e7307294
- Summary: v 1.17
- Files: nucleares_monitor/control_board_monitor.py
### 1.16-interim2
- Date: 2025-10-21
- Commit: 53c33c88
- Summary: 1.16-interim2
- Files: nucleares_monitor/control_board_monitor.py
### 1.16-interim
- Date: 2025-10-21
- Commit: 692bcb3b
- Summary: 1.16-interim
- Files: nucleares_monitor/control_board_monitor.py, nucleares_monitor/version.nfo
### v 1.15
- Date: 2025-10-20
- Commit: 4d0f601c
- Summary: v 1.15
- Files: nucleares_monitor/control_board_monitor.py, nucleares_monitor/mon.json
### 1.14
- Date: 2025-10-20
- Commit: c0a46c7b
- Summary: 1.14
- Files: nucleares_monitor/control_board_monitor.py, nucleares_monitor/mon.json, nucleares_monitor/stab_ion.json
### 1.13
- Date: 2025-10-20
- Commit: 04923cc4
- Summary: 1.13
- Files: nucleares_monitor/control_board_monitor.py
### 1.12
- Date: 2025-10-20
- Commit: 4beb0111
- Summary: 1.12
- Files: nucleares_monitor/control_board_monitor.py
### 1.11
- Date: 2025-10-20
- Commit: e06e2975
- Summary: 1.11
- Files: nucleares_monitor/control_board_monitor.py
### 1.10
- Date: 2025-10-20
- Commit: 8d5aac56
- Summary: 1.10
- Files: nucleares_monitor/control_board_monitor.py
### 1.9
- Date: 2025-10-20
- Commit: 45c98308
- Summary: 1.9
- Files: nucleares_monitor/control_board_monitor.py
### 1.8
- Date: 2025-10-20
- Commit: 9896a811
- Summary: 1.8
- Files: nucleares_monitor/control_board_monitor.py
### 1.7
- Date: 2025-10-20
- Commit: fd18fcf0
- Summary: 1.7
- Files: nucleares_monitor/control_board_monitor.py
### 1.6
- Date: 2025-10-20
- Commit: 7928d17e
- Summary: 1.6
- Files: nucleares_monitor/control_board_monitor.py
### 1.5
- Date: 2025-10-20
- Commit: 5a873b4e
- Summary: 1.5
- Files: nucleares_monitor/control_board_monitor.py
### 1.4
- Date: 2025-10-20
- Commit: cac380ca
- Summary: 1.4
- Files: nucleares_monitor/control_board_monitor.py
### 1.3
- Date: 2025-10-20
- Commit: 792694f6
- Summary: 1.3
- Files: nucleares_monitor/control_board_monitor.py
### 1.2
- Date: 2025-10-20
- Commit: e644daff
- Summary: 1.2
- Files: nucleares_monitor/control_board_monitor.py
### 1.1
- Date: 2025-10-20
- Commit: 3b88f4c8
- Summary: 1.1
- Files: nucleares_monitor/control_board_monitor.py
### 1.0
- Date: 2025-10-20
- Commit: a2fbabed
- Summary: 1.0
- Files: nucleares_monitor/control_board_monitor.py
### 0.9
- Date: 2025-10-20
- Commit: 23084f4c
- Summary: 0.9
- Files: nucleares_monitor/control_board_monitor.py
### 0.8
- Date: 2025-10-20
- Commit: 65b39890
- Summary: 0.8
- Files: nucleares_monitor/control_board_monitor.py
### 0.7
- Date: 2025-10-20
- Commit: a73459ea
- Summary: 0.7
- Files: nucleares_monitor/control_board_monitor.py
### 0.6
- Date: 2025-10-20
- Commit: c8a439c2
- Summary: 0.6
- Files: nucleares_monitor/control_board_monitor.py
### 0.4
- Date: 2025-10-20
- Commit: 311cb768
- Summary: 0.4
- Files: nucleares_monitor/control_board_monitor.py
### 0.3
- Date: 2025-10-20
- Commit: 61a8cb40
- Summary: 0.3
- Files: nucleares_monitor/control_board_monitor.py
### 0.1
- Date: 2025-10-20
- Commit: ca4a3b65
- Summary: 0.1
- Files: nucleares_monitor/control_board_monitor.py
## RELEASE 0.2
### sync: nucleares_monitor v0.1
- Date: 2025-10-11
- Commit: ec8b3404
- Tags: 0.2
- Summary: sync: nucleares_monitor v0.1
- Files: nucleares_monitor/control_board_monitor.py, nucleares_monitor/version.nfo
### sync: investigation-board_v13 v0.2
- Date: 2025-10-09
- Commit: c7048e6c
- Summary: sync: investigation-board_v13 v0.2
- Files: investigation-board_v13/investigation-board/module.json, investigation-board_v13/investigation-board/scripts/investigation-board.js, investigation-board_v13/version.nfo
### sync: wg-voidships-builder v1.4.13
- Date: 2025-10-09
- Commit: 3a610b71
- Summary: sync: wg-voidships-builder v1.4.13
- Files: wg-voidships-builder/module.json
### sync: investigation-board_v13 v0.1
- Date: 2025-10-09
- Commit: 9c06d9ce
- Summary: sync: investigation-board_v13 v0.1
- Files: investigation-board_v13/version.nfo
### sync: help_scripts v0.3
- Date: 2025-10-09
- Commit: a6cdd01e
- Summary: sync: help_scripts v0.3
- Files: help_scripts/version.nfo
### sync: foundry v0.2
- Date: 2025-10-09
- Commit: 0a4148ab
- Summary: sync: foundry v0.2
- Files: foundry/availability.csv, foundry/components_supplemental_xenos_archeotech.csv, foundry/starship_weapons.csv, foundry/version.nfo, help_scripts/generate_manifests.py, help_scripts/generate_manifestsv2.py (+17 more)
### sync: wg-voidships_doc v0.2
- Date: 2025-09-19
- Commit: 95a71cb6
- Summary: sync: wg-voidships_doc v0.2
- Files: wg-voidships_doc/version.nfo
### sync: wg-voidships-journal v1.0.2
- Date: 2025-09-19
- Commit: 97f738c5
- Summary: sync: wg-voidships-journal v1.0.2
- Files: wg-voidships-journal/module.json
### sync: wg-voidships-builder v1.4.12
- Date: 2025-09-19
- Commit: 5ed6c55e
- Summary: sync: wg-voidships-builder v1.4.12
- Files: wg-voidships-builder/module.json
### sync: gk-greyknight v0.6.5.3
- Date: 2025-09-19
- Commit: 7b621f8b
- Summary: sync: gk-greyknight v0.6.5.3
- Files: wg-greyknights/module.json
### sync: help_scripts v0.2
- Date: 2025-09-19
- Commit: 64c3b8ca
- Summary: sync: help_scripts v0.2
- Files: help_scripts/version.nfo
### sync: foundry v0.1
- Date: 2025-09-19
- Commit: 298239c3
- Summary: sync: foundry v0.1
- Files: foundry/all_hulls.csv, foundry/components_supplemental_xenos_archeotech.csv, foundry/essential_components.csv, foundry/starship_weapons.csv, foundry/version.nfo, foundry/wg-voidship-hulls copy.db (+6 more)
### sync: help_scripts v0.1
- Date: 2025-09-19
- Commit: ee632ee0
- Summary: sync: help_scripts v0.1
- Files: help_scripts/sync_foundry_module.py, help_scripts/version.nfo
### Cleanup part one
- Date: 2025-09-19
- Commit: bf6e1050
- Summary: Cleanup part one
- Files: foundry.zip, help_scripts/import.py, help_scripts/patch_hulls.py, help_scripts/patch_voidships.py, help_scripts/sound.py, help_scripts/sync_foundry_module.py (+8 more)
### sync: wg-voidships-builder v1.4.11
- Date: 2025-09-19
- Commit: c2a93cbd
- Summary: sync: wg-voidships-builder v1.4.11
- Files: wg-voidships-builder/module.json, wg-voidships-builder/packs/wg-voidship-items.db, wg-voidships-builder/scripts/voidship-actor.js
### sync: wg-voidships-builder v1.4.10
- Date: 2025-09-19
- Commit: 862e0c3c
- Summary: sync: wg-voidships-builder v1.4.10
- Files: wg-voidships-builder/module.json, wg-voidships-builder/sync_foundry_module.py
### sync: wg-voidships-builder v1.4.9
- Date: 2025-09-19
- Commit: 20345f16
- Summary: sync: wg-voidships-builder v1.4.9
- Files: wg-voidships-builder/module.json, wg-voidships-builder/packs/wg-voidship-items.db, wg-voidships-builder/scripts/voidship-actor.js, wg-voidships-builder/sync_foundry_module.py
### aaa
- Date: 2025-09-19
- Commit: c11e029b
- Summary: aaa
- Files: wg-voidships-builder/packs/patch_voidships.py, wg-voidships-builder/packs/wg-voidship-hulls.db, wg-voidships-builder/packs/wg-voidship-hulls.db.bak, wg-voidships-builder/packs/wg-voidship-items.db, wg-voidships-builder/packs/wg-voidship-items.db.bak, wg-voidships-builder/sync_foundry_module.py
### updt
- Date: 2025-09-19
- Commit: 886bced1
- Summary: updt
- Files: wg-voidships-builder/packs/wg-voidship-archeotech.db, wg-voidships-builder/packs/wg-voidship-xenos.db, wg-voidships-builder/scripts/voidship-actor.js, wg-voidships-builder/templates/voidship-sheet.hbs
### Working version
- Date: 2025-09-18
- Commit: 584985ed
- Summary: Working version
- Files: wg-voidships-builder/packs/wg-voidship-hulls.db, wg-voidships-builder/packs/wg-voidship-items.db, wg-voidships-builder/scripts/voidship-actor.js, wg-voidships-builder/templates/voidship-sheet.hbs, wg-voidships-builder/wg-voidships-builder.zip
### aaa
- Date: 2025-09-18
- Commit: a4c51710
- Summary: aaa
- Files: wg-voidships-builder/packs/wg-voidship-hulls.db, wg-voidships-builder/packs/wg-voidship-items.db, wg-voidships-builder/scripts/voidship-actor.js, wg-voidships-builder/templates/voidship-sheet.hbs, wg-voidships-builder/wg-voidships-builder.zip
### 111
- Date: 2025-09-18
- Commit: 1abe31b3
- Summary: 111
- Files: foundry/wg-voidship-hulls copy.db, wg-voidships-builder/packs/wg-voidship-hulls.db, wg-voidships-builder/packs/wg-voidship-items.db, wg-voidships-builder/sample_universe.json, wg-voidships-builder/scripts/voidship-actor.js, wg-voidships-builder/scripts/voidships.js (+1 more)
### asd
- Date: 2025-09-18
- Commit: ddae4ad4
- Summary: asd
- Files: wg-voidships-builder/scripts/voidship-actor.js, wg-voidships-builder/scripts/voidships.js
### test
- Date: 2025-09-18
- Commit: 5186b264
- Summary: test
- Files: wg-voidships-builder/packs/wg-voidship-hulls.db, wg-voidships-builder/scripts/voidship-actor.js, wg-voidships-builder/templates/voidship-sheet.hbs
### 1
- Date: 2025-09-18
- Commit: e84c0127
- Summary: 1
- Files: wg-voidships-builder/packs/wg-voidship-hulls.db, wg-voidships-builder/scripts/voidship-actor.js, wg-voidships-builder/scripts/voidships.js, wg-voidships-builder/templates/voidship-sheet.hbs
### aaa
- Date: 2025-09-18
- Commit: 5892e614
- Summary: aaa
- Files: wg-voidships-builder/module.json, wg-voidships-builder/scripts/voidship-actor.js, wg-voidships-builder/templates/voidship-sheet.hbs
### 11
- Date: 2025-09-18
- Commit: e8ad5478
- Summary: 11
- Files: foundry.zip, wg-greyknights.0.6.4.9.zip, wg-voidships-builder.1.4.8.zip
### ss
- Date: 2025-09-18
- Commit: 89791655
- Summary: ss
- Files: foundry/Aegis Power Armour.json, foundry/Aegis Terminator Armour.json, foundry/Grey Knight Archetype.json, foundry/fvtt-Actor-aquila-lander-Qz0AG5vJU9HrYtxk.json, foundry/fvtt-Actor-cobra-class-destroyer-(renegade)-jPYz63kXVprYFXMi.json, foundry/fvtt-Actor-cobra-class-destroyer-b518e981d69324aa.json (+26 more)
### More context files
- Date: 2025-09-18
- Commit: 9c3d7350
- Summary: More context files
- Files: wg-greyknights/foundry/Aegis Power Armour.json, wg-greyknights/foundry/Aegis Terminator Armour.json, wg-greyknights/foundry/Grey Knight Archetype.json, wg-greyknights/foundry/fvtt-Actor-aquila-lander-Qz0AG5vJU9HrYtxk.json, wg-greyknights/foundry/fvtt-Actor-cobra-class-destroyer-(renegade)-jPYz63kXVprYFXMi.json, wg-greyknights/foundry/fvtt-Actor-cobra-class-destroyer-b518e981d69324aa.json (+13 more)
### Final final final
- Date: 2025-09-18
- Commit: d977cff4
- Summary: Final final final
- Files: wg-greyknights/module.json, wg-greyknights/packs/gk-items.db, wg-greyknights/scripts/ui-gkpanel.js
### Final
- Date: 2025-09-18
- Commit: 56ada5e1
- Summary: Final
- Files: wg-greyknights/LICENSE, wg-greyknights/README.txt, wg-greyknights/icons/placeholder.txt, wg-greyknights/manifest.json, wg-greyknights/packs/daemonhunters.db, wg-greyknights/packs/gk-items-060.db (+4 more)
### cleanup
- Date: 2025-09-18
- Commit: 7c35c1bb
- Summary: cleanup
- Files: wg-greyknights/content/Daemonhunters/Abilities/Aegis of Titan.json, wg-greyknights/content/Daemonhunters/Archetypes/Grey Knight (General).json, wg-greyknights/content/Daemonhunters/Archetypes/Grey Knight Justicar.json, wg-greyknights/content/Daemonhunters/Archetypes/Grey Knight Paladin.json, wg-greyknights/content/Daemonhunters/Archetypes/Grey Knight Purgation Marine.json, wg-greyknights/content/Daemonhunters/Archetypes/Grey Knight Purifier.json (+20 more)
### Import gk-greyknight-0.6.4.8.zip do wg-greyknights (2025-08-25T13:36:01Z)
- Date: 2025-08-25
- Commit: b2c47b68
- Summary: Import gk-greyknight-0.6.4.8.zip do wg-greyknights (2025-08-25T13:36:01Z)
- Files: wg-greyknights/module.json, wg-greyknights/packs/gk-items.db, wg-greyknights/scripts/helpers.js
### Import gk-greyknight-0.6.4.5.zip do wg-greyknights (2025-08-23T20:41:27Z)
- Date: 2025-08-23
- Commit: fbbc5eec
- Summary: Import gk-greyknight-0.6.4.5.zip do wg-greyknights (2025-08-23T20:41:27Z)
- Files: wg-greyknights/module.json, wg-greyknights/scripts/helpers.js
### Import gk-greyknight-0.6.4.4.zip do wg-greyknights (2025-08-23T20:15:27Z)
- Date: 2025-08-23
- Commit: 1d346db6
- Summary: Import gk-greyknight-0.6.4.4.zip do wg-greyknights (2025-08-23T20:15:27Z)
- Files: wg-greyknights/module.json, wg-greyknights/packs/gk-items.db, wg-greyknights/scripts/helpers.js
### Import gk-greyknight-0.6.4.3.zip do wg-greyknights (2025-08-23T19:48:54Z)
- Date: 2025-08-23
- Commit: 0e417ca6
- Summary: Import gk-greyknight-0.6.4.3.zip do wg-greyknights (2025-08-23T19:48:54Z)
- Files: wg-greyknights/module.json, wg-greyknights/scripts/helpers.js
### Import gk-greyknight-0.6.4.2.zip do wg-greyknights (2025-08-23T19:35:08Z)
- Date: 2025-08-23
- Commit: 47a0250f
- Summary: Import gk-greyknight-0.6.4.2.zip do wg-greyknights (2025-08-23T19:35:08Z)
- Files: wg-greyknights/module.json, wg-greyknights/packs/gk-items.db, wg-greyknights/scripts/helpers.js
### Import gk-greyknight-0.6.4.1-patch.zip do wg-greyknights (2025-08-23T19:02:02Z)
- Date: 2025-08-23
- Commit: f4074855
- Summary: Import gk-greyknight-0.6.4.1-patch.zip do wg-greyknights (2025-08-23T19:02:02Z)
- Files: wg-greyknights/module.json, wg-greyknights/packs/gk-items.db, wg-greyknights/scripts/helpers.js
### Import gk-greyknight-0.6.4-patch.zip do wg-greyknights (2025-08-23T19:01:42Z)
- Date: 2025-08-23
- Commit: 4dbce12b
- Summary: Import gk-greyknight-0.6.4-patch.zip do wg-greyknights (2025-08-23T19:01:42Z)
- Files: wg-greyknights/module.json, wg-greyknights/packs/gk-items.db, wg-greyknights/scripts/auto-loadout.js
### Import 0.6.1.stable.gk.zip do wg-greyknights (2025-08-22T17:44:46Z)
- Date: 2025-08-22
- Commit: e29930d0
- Summary: Import 0.6.1.stable.gk.zip do wg-greyknights (2025-08-22T17:44:46Z)
- Files: wg-greyknights/module.json, wg-greyknights/packs/gk-items.db, wg-greyknights/scripts/helpers.js
### Import wng-daemonhunters-0.6.4.2.zip do wg-greyknights (2025-08-22T17:26:07Z)
- Date: 2025-08-22
- Commit: 45b2d0d3
- Summary: Import wng-daemonhunters-0.6.4.2.zip do wg-greyknights (2025-08-22T17:26:07Z)
- Files: wg-greyknights/module.json, wg-greyknights/packs/daemonhunters.db, wg-greyknights/scripts/auto-gk-archetype.js
### Import wng-daemonhunters-0.6.4.1.zip do wg-greyknights (2025-08-22T17:07:40Z)
- Date: 2025-08-22
- Commit: e164cb6e
- Summary: Import wng-daemonhunters-0.6.4.1.zip do wg-greyknights (2025-08-22T17:07:40Z)
- Files: wg-greyknights/module.json, wg-greyknights/packs/daemonhunters.db, wg-greyknights/scripts/auto-gk-archetype.js
### Import gk-greyknight-0.6.3.zip do wg-greyknights (2025-08-22T12:08:07Z)
- Date: 2025-08-22
- Commit: fe70f879
- Summary: Import gk-greyknight-0.6.3.zip do wg-greyknights (2025-08-22T12:08:07Z)
- Files: wg-greyknights/module.json, wg-greyknights/packs/gk-items.db
### Import gk-greyknight-0.6.1.zip do wg-greyknights (2025-08-22T11:40:25Z)
- Date: 2025-08-22
- Commit: bf3fe1e7
- Summary: Import gk-greyknight-0.6.1.zip do wg-greyknights (2025-08-22T11:40:25Z)
- Files: wg-greyknights/README.txt, wg-greyknights/module.json, wg-greyknights/packs/gk-items.db, wg-greyknights/scripts/auto-loadout.js
### Import gk-greyknight-0.6.0.zip do wg-greyknights (2025-08-22T11:09:50Z)
- Date: 2025-08-22
- Commit: 4408ca89
- Summary: Import gk-greyknight-0.6.0.zip do wg-greyknights (2025-08-22T11:09:50Z)
- Files: wg-greyknights/README.txt, wg-greyknights/module.json, wg-greyknights/packs/gk-items-060.db, wg-greyknights/scripts/auto-loadout.js
### Import gk-greyknight-0.5.3.zip do wg-greyknights (2025-08-22T10:31:14Z)
- Date: 2025-08-22
- Commit: 73b70ca4
- Summary: Import gk-greyknight-0.5.3.zip do wg-greyknights (2025-08-22T10:31:14Z)
- Files: wg-greyknights/README.txt, wg-greyknights/module.json, wg-greyknights/packs/gk-items.db, wg-greyknights/scripts/auto-loadout.js
### Import gk-greyknight-last_stable.zip do wg-greyknights (2025-08-22T10:21:33Z)
- Date: 2025-08-22
- Commit: 2e44ddf7
- Summary: Import gk-greyknight-last_stable.zip do wg-greyknights (2025-08-22T10:21:33Z)
- Files: wg-greyknights/gk-greyknight-0.5.1-patch/packs/gk-items.db, wg-greyknights/module.json, wg-greyknights/packs/gk-items.db, wg-greyknights/scripts/auto-loadout.js
### Import gk-greyknight-0.5.2-patch.zip do wg-greyknights (2025-08-22T10:04:48Z)
- Date: 2025-08-22
- Commit: 235f91f0
- Summary: Import gk-greyknight-0.5.2-patch.zip do wg-greyknights (2025-08-22T10:04:48Z)
- Files: wg-greyknights/gk-greyknight-0.4.7-patch/module.json.patch.json, wg-greyknights/gk-greyknight-0.5.2-patch/module.json.patch.json, wg-greyknights/gk-greyknight-0.5.2-patch/packs/gk-items-051.db
### Import gk-greyknight-0.5.1-patch.zip do wg-greyknights (2025-08-22T09:46:13Z)
- Date: 2025-08-22
- Commit: d7bb2559
- Summary: Import gk-greyknight-0.5.1-patch.zip do wg-greyknights (2025-08-22T09:46:13Z)
- Files: wg-greyknights/gk-greyknight-0.5.0-patch/packs/gk-items.db, wg-greyknights/gk-greyknight-0.5.1-patch/packs/gk-items.db
### Import gk-greyknight-0.5.0-patch.zip do wg-greyknights (2025-08-22T00:06:01Z)
- Date: 2025-08-22
- Commit: 06ae94b8
- Summary: Import gk-greyknight-0.5.0-patch.zip do wg-greyknights (2025-08-22T00:06:01Z)
- Files: wg-greyknights/gk-greyknight-0.5.0-patch/packs/gk-items.db
### Import gk-greyknight-0.4.9-patch.zip do wg-greyknights (2025-08-21T23:12:29Z)
- Date: 2025-08-21
- Commit: 8fe7cf51
- Summary: Import gk-greyknight-0.4.9-patch.zip do wg-greyknights (2025-08-21T23:12:29Z)
- Files: wg-greyknights/gk-greyknight-0.4.9-patch/packs/gk-items.db, wg-greyknights/gk-greyknight-0.4.9-patch/scripts/auto-loadout.js
### Import gk-greyknight-0.4.8-patch.zip do wg-greyknights (2025-08-21T22:49:35Z)
- Date: 2025-08-21
- Commit: 2452cb60
- Summary: Import gk-greyknight-0.4.8-patch.zip do wg-greyknights (2025-08-21T22:49:35Z)
- Files: wg-greyknights/gk-greyknight-0.4.8-patch/packs/gk-items.db
### Import gk-greyknight-0.4.7-patch.zip do wg-greyknights (2025-08-21T22:41:13Z)
- Date: 2025-08-21
- Commit: f7b7e00c
- Summary: Import gk-greyknight-0.4.7-patch.zip do wg-greyknights (2025-08-21T22:41:13Z)
- Files: wg-greyknights/gk-greyknight-0.4.7-patch/module.json.patch.json, wg-greyknights/gk-greyknight-0.4.7-patch/scripts/auto-loadout.js
### Import gk-greyknight-0.4.6-patch.zip do wg-greyknights (2025-08-21T22:23:55Z)
- Date: 2025-08-21
- Commit: 2111d117
- Summary: Import gk-greyknight-0.4.6-patch.zip do wg-greyknights (2025-08-21T22:23:55Z)
- Files: wg-greyknights/gk-greyknight-0.4.6-patch/packs/gk-items.db, wg-greyknights/gk-greyknight-0.4.6-patch/scripts/auto-loadout.js
### Import gk-greyknight-0.4.5-patch.zip do wg-greyknights (2025-08-21T22:05:46Z)
- Date: 2025-08-21
- Commit: 5fcdc8d2
- Summary: Import gk-greyknight-0.4.5-patch.zip do wg-greyknights (2025-08-21T22:05:46Z)
- Files: wg-greyknights/gk-greyknight-0.4.5-patch/packs/gk-items.db, wg-greyknights/gk-greyknight-0.4.5-patch/scripts/auto-loadout.js
### Import gk-greyknight-0.4.4-patch.zip do wg-greyknights (2025-08-21T21:44:57Z)
- Date: 2025-08-21
- Commit: 305edfa7
- Summary: Import gk-greyknight-0.4.4-patch.zip do wg-greyknights (2025-08-21T21:44:57Z)
- Files: wg-greyknights/gk-greyknight-0.4.2-patch/scripts/auto-loadout.js, wg-greyknights/gk-greyknight-0.4.4-patch/module.json.patch.json, wg-greyknights/gk-greyknight-0.4.4-patch/scripts/auto-loadout.js
### Import gk-greyknight-0.4.3-patch.zip do wg-greyknights (2025-08-21T21:34:22Z)
- Date: 2025-08-21
- Commit: 0ab17ffe
- Summary: Import gk-greyknight-0.4.3-patch.zip do wg-greyknights (2025-08-21T21:34:22Z)
- Files: wg-greyknights/gk-greyknight-0.4.3-patch/packs/gk-items.db
### Import gk-greyknight-0.4.2-patch.zip do wg-greyknights (2025-08-21T21:19:36Z)
- Date: 2025-08-21
- Commit: cf7c4696
- Summary: Import gk-greyknight-0.4.2-patch.zip do wg-greyknights (2025-08-21T21:19:36Z)
- Files: wg-greyknights/gk-greyknight-0.4.2-patch/module.json.patch.json, wg-greyknights/gk-greyknight-0.4.2-patch/packs/gk-items.db, wg-greyknights/gk-greyknight-0.4.2-patch/scripts/auto-loadout.js, wg-greyknights/scripts/auto-loadout.js
### Import gk-greyknight-0.4.1.zip do wg-greyknights (2025-08-21T20:56:51Z)
- Date: 2025-08-21
- Commit: da34a795
- Summary: Import gk-greyknight-0.4.1.zip do wg-greyknights (2025-08-21T20:56:51Z)
- Files: wg-greyknights/README.txt, wg-greyknights/module.json
### Import gk-greyknight-compendium-0.4.0.zip do wg-greyknights (2025-08-21T20:43:39Z)
- Date: 2025-08-21
- Commit: b3017588
- Summary: Import gk-greyknight-compendium-0.4.0.zip do wg-greyknights (2025-08-21T20:43:39Z)
- Files: wg-greyknights/README.txt, wg-greyknights/docs/README.txt, wg-greyknights/module.json, wg-greyknights/packs/gk-items.db
### Import wrath-and-glory-gk-0.3.0.zip do wg-greyknights (2025-08-21T20:13:12Z)
- Date: 2025-08-21
- Commit: 10c76306
- Summary: Import wrath-and-glory-gk-0.3.0.zip do wg-greyknights (2025-08-21T20:13:12Z)
- Files: wg-greyknights/content/Daemonhunters/Abilities/Aegis of Titan.json, wg-greyknights/content/Daemonhunters/Archetypes/Grey Knight (General).json, wg-greyknights/content/Daemonhunters/Archetypes/Grey Knight Justicar.json, wg-greyknights/content/Daemonhunters/Archetypes/Grey Knight Paladin.json, wg-greyknights/content/Daemonhunters/Archetypes/Grey Knight Purgation Marine.json, wg-greyknights/content/Daemonhunters/Archetypes/Grey Knight Purifier.json (+13 more)
### Import wrath-and-glory-gk-0.2.1.zip do wg-greyknights (2025-08-21T20:13:04Z)
- Date: 2025-08-21
- Commit: cbbdb35f
- Summary: Import wrath-and-glory-gk-0.2.1.zip do wg-greyknights (2025-08-21T20:13:04Z)
- Files: wg-greyknights/docs/README.txt, wg-greyknights/manifest.json, wg-greyknights/module.json
### Import wrath-and-glory-gk-0.2.0.zip do wg-greyknights (2025-08-21T20:12:55Z)
- Date: 2025-08-21
- Commit: b4ee703f
- Summary: Import wrath-and-glory-gk-0.2.0.zip do wg-greyknights (2025-08-21T20:12:55Z)
- Files: wg-greyknights/README.txt, wg-greyknights/docs/README.txt, wg-greyknights/foundry/items/Aegis Power Armour.json, wg-greyknights/foundry/items/Aegis Terminator Armour.json, wg-greyknights/foundry/items/Aegis of Titan.json, wg-greyknights/foundry/items/Grey Knight Archetype.json (+8 more)
### Import gk-greyknight-0.1.0.zip do wg-greyknights (2025-08-21T19:51:05Z)
- Date: 2025-08-21
- Commit: 24ad39dc
- Summary: Import gk-greyknight-0.1.0.zip do wg-greyknights (2025-08-21T19:51:05Z)
- Files: wg-greyknights/LICENSE, wg-greyknights/README.txt, wg-greyknights/foundry/actors/gk-incognito.json, wg-greyknights/foundry/actors/gk-power-armour.json, wg-greyknights/foundry/actors/gk-terminator.json, wg-greyknights/foundry/fvtt-Item-ability-aegis-of-titan_v5.json (+10 more)
### Import GK_Foundry_Macros_Fix_v5.zip do wg-greyknights (2025-08-21T19:30:33Z)
- Date: 2025-08-21
- Commit: dc829069
- Summary: Import GK_Foundry_Macros_Fix_v5.zip do wg-greyknights (2025-08-21T19:30:33Z)
- Files: wg-greyknights/macro/macro-gk-auto-loadout-on-archetype.js, wg-greyknights/macros/macro-gk-auto-loadout-on-archetype.js, wg-greyknights/macros/macro-gk-glow-toggle.js, wg-greyknights/macros/macro-gk-import-world-items-FIXED.js, wg-greyknights/macros/macro-gk-loadout-manager.js
### Import GK_Foundry_GreyKnight_v5.zip do wg-greyknights (2025-08-21T19:02:44Z)
- Date: 2025-08-21
- Commit: edb3f0df
- Summary: Import GK_Foundry_GreyKnight_v5.zip do wg-greyknights (2025-08-21T19:02:44Z)
- Files: wg-greyknights/foundry/fvtt-Item-ability-aegis-of-titan_v5.json, wg-greyknights/foundry/fvtt-Item-aegis-power-armour_v2.json, wg-greyknights/foundry/fvtt-Item-archetype-grey-knight_v5.json, wg-greyknights/foundry/fvtt-Item-gk-armour-integrated-storm-bolter_v2.json, wg-greyknights/foundry/fvtt-Item-nemesis-force-halberd_v4.json, wg-greyknights/foundry/fvtt-Item-terminator-aegis-gk_v4.json (+4 more)
### Import Grey_Knight_WrathAndGlory_FoundryPack.zip do wg-greyknights (2025-08-21T17:10:55Z)
- Date: 2025-08-21
- Commit: c725793b
- Summary: Import Grey_Knight_WrathAndGlory_FoundryPack.zip do wg-greyknights (2025-08-21T17:10:55Z)
- Files: wg-greyknights/foundry/fvtt-Actor-grey-knight-FRESH.json, wg-greyknights/foundry/fvtt-Item-ability-aegis-of-titan.json, wg-greyknights/foundry/fvtt-Item-aegis-psychic-hood-gk_v2.json, wg-greyknights/foundry/fvtt-Item-archetype-grey-knight_v2.json, wg-greyknights/foundry/fvtt-Item-nemesis-force-halberd.json, wg-greyknights/foundry/fvtt-Item-pilgrim-astartes-plate.json (+2 more)
## RELEASE 0.1
### a
- Date: 2025-09-17
- Commit: ddb82921
- Tags: 0.1
- Summary: a
- Files: import.py
### aa
- Date: 2025-09-17
- Commit: 541ef488
- Summary: aa
- Files: import.py
### aa
- Date: 2025-09-17
- Commit: e256c731
- Summary: aa
- Files: import.py, wg-greyknights/module.json
### Rename
- Date: 2025-09-17
- Commit: e284eceb
- Summary: Rename
- Files: wg-voidships-builder/LICENSE, wg-voidships-builder/journal/README.md, wg-voidships-builder/module.json, wg-voidships-builder/packs/wg-voidship-archeotech.db, wg-voidships-builder/packs/wg-voidship-hulls.db, wg-voidships-builder/packs/wg-voidship-items.db (+5 more)
### Done
- Date: 2025-09-17
- Commit: 355e4ab7
- Summary: Done
- Files: wg-voidships-foundry/packs/wg-voidship-items.db, wg-voidships-foundry/packs/wg-voidship-macros.db, wg-voidships-foundry/packs/wg-voidships-items.db
### asss
- Date: 2025-09-17
- Commit: 06297a9e
- Summary: asss
- Files: wg-voidships-foundry/packs/wg-voidship-items.db, wg-voidships-foundry/packs/wg-voidship-macros.db
### 12
- Date: 2025-09-16
- Commit: 1c80b743
- Summary: 12
- Files: wg-voidships-foundry/packs/wg-voidship-archeotech.db, wg-voidships-foundry/packs/wg-voidship-items.db, wg-voidships-foundry/packs/wg-voidship-macros.db, wg-voidships-foundry/packs/wg-voidship-xenos.db, wg-voidships-foundry/packs/wg-voidships-macros.db
### dd
- Date: 2025-09-16
- Commit: 22e4553c
- Summary: dd
- Files: wg-voidships-foundry/journal/README.md, wg-voidships-foundry/packs/wg-voidship-hulls.db, wg-voidships-foundry/packs/wg-voidship-items.db, wg-voidships-foundry/packs/wg-voidship-macros.db, wg-voidships-foundry/packs/wg-voidship-xenos.db, wg-voidships-foundry/packs/wg-voidship-xewnos.db (+1 more)
### 11
- Date: 2025-09-16
- Commit: e34f3a22
- Summary: 11
- Files: wg-voidships-foundry/module.json, wg-voidships-foundry/packs/wg-voidship-archeotech.db, wg-voidships-foundry/packs/wg-voidship-xewnos.db, wg-voidships-foundry/packs/wg-voidships-items.db, wg-voidships-foundry/patch_hulls.py
### ASaa
- Date: 2025-09-16
- Commit: 5511acfb
- Summary: ASaa
- Files: sound.py, wg-voidships-foundry.zip, wg-voidships-foundry/module.json, wg-voidships-foundry/packs/wg-voidship-hulls.db, wg-voidships-foundry/packs/wg-voidships-actors.db, wg-voidships-foundry/packs/wg-voidships-items.db (+3 more)
### sss
- Date: 2025-08-29
- Commit: db350588
- Summary: sss
- Files: wg-voidships-foundry/journal/README.md, wg-voidships-foundry/module.json, wg-voidships-foundry/packs/wg-voidships-items.db
### 0.1
- Date: 2025-08-29
- Commit: 987eef4f
- Summary: 0.1
- Files: wg-voidships-foundry/LICENSE, wg-voidships-foundry/journal/README.md, wg-voidships-foundry/module.json, wg-voidships-foundry/packs/wg-voidships-actors.db, wg-voidships-foundry/packs/wg-voidships-macros.db, wg-voidships-foundry/packs/wg-voidships-tables.db (+1 more)
### Module import
- Date: 2025-08-27
- Commit: 7465a79a
- Summary: Module import
- Files: wg-voidships-journal.1.0.zip, wg-voidships-journal/LICENSE, wg-voidships-journal/README.md, wg-voidships-journal/module.json, wg-voidships-journal/packs/wg-voidships-docs.db, wg-voidships.1.0.1.zip (+3 more)
### Voidships - Ship - part 2
- Date: 2025-08-27
- Commit: 18a3ef3c
- Summary: Voidships - Ship - part 2
- Files: wg-voidships/voidship-docs-journal-import.json, wg-voidships_doc/wg-voidhsips_of_gilead_doc/Voidships_WG_ships2.html
### fix
- Date: 2025-08-27
- Commit: 769d568d
- Summary: fix
- Files: wg-voidships_doc/wg-voidhsips_of_gilead_doc/Voidships_WG_ships.html
### V 1.0.1 release candidate start of v 1.1.0 development
- Date: 2025-08-27
- Commit: 62989221
- Summary: V 1.0.1 release candidate start of v 1.1.0 development
- Files: wg-voidships.1.0.1.zip, wg-voidships_doc/wg-voidhsips_of_gilead_doc/ships.html
## RELEASE 1.0.2-
### v 1.0.2
- Date: 2025-08-26
- Commit: b4728cf1
- Tags: 1.0.2-
- Summary: v 1.0.2
- Files: wg-voidships/LICENSE.txt, wg-voidships/README.md, wg-voidships/module.json, wg-voidships/packs/campaign-silica-animus.db, wg-voidships/packs/ships-of-gilead-v3.db, wg-voidships/packs/voidships-catalogue-v3.db (+7 more)
### fx - vs 1.0.1
- Date: 2025-08-26
- Commit: f3f689b0
- Summary: fx - vs 1.0.1
- Files: wg-voidships_doc/Voidships_WG_Catalogue.html, wg-voidships_doc/Voidships_WG_Catalogue_critical_hits.html, wg-voidships_doc/Voidships_WG_Construction.html, wg-voidships_doc/Voidships_WG_Rules.html
## RELEASE voidships-1.0
### Voidships rules v.1.0
- Date: 2025-08-26
- Commit: 4ed12c28
- Tags: voidships-1.0
- Summary: Voidships rules v.1.0
- Files: wg-voidships_doc/Voidships_WG_Catalogue_v3.html, wg-voidships_doc/Voidships_WG_Construction_RT_v3.html, wg-voidships_doc/Voidships_WG_Rules_v3.html
### v2
- Date: 2025-08-26
- Commit: 1fbe8bb7
- Summary: v2
- Files: wg-voidships_doc/Voidships_WG_Rules_v3.html
### v2
- Date: 2025-08-26
- Commit: 5af5f455
- Summary: v2
- Files: wg-voidships_doc/Voidships_WG_Catalogue_v3.html, wg-voidships_doc/Voidships_WG_Construction_RT_v3.html, wg-voidships_doc/Voidships_WG_Rules_v3.html
### Initial commit voidships
- Date: 2025-08-26
- Commit: 0368c130
- Summary: Initial commit voidships
- Files: wg-voidships/LICENSE.txt, wg-voidships/README.md, wg-voidships/module.json, wg-voidships/packs/campaign-silica-animus.db, wg-voidships/packs/ships-of-gilead-v3.db, wg-voidships/packs/voidships-catalogue-v3.db (+6 more)
+356
View File
@@ -0,0 +1,356 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
manage_releases.py
Automates synchronization of Git tags with GitHub releases. On each run it:
* Verifies the repository is clean and the current branch is fully pushed.
* Ensures HEAD is tagged by comparing the root version.nfo with existing
release tags and bumping/committing when required.
* Iterates over every tag in the repository, creating GitHub releases for any
missing entries and attaching per-directory ZIP archives (excluding
version.nfo files) that reflect the repository state for that tag.
Dependencies:
- gh CLI (https://cli.github.com/)
- git available in PATH
"""
from __future__ import annotations
import argparse
import io
import os
import re
import shutil
import subprocess
import tarfile
import tempfile
import textwrap
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Sequence, Tuple
from zipfile import ZIP_DEFLATED, ZipFile
def sh(args: Sequence[str], cwd: Path, check: bool = True) -> subprocess.CompletedProcess[str]:
"""Run a subprocess and return the completed process."""
result = subprocess.run(
list(args),
cwd=str(cwd),
text=True,
capture_output=True,
)
if check and result.returncode != 0:
raise RuntimeError(
f"Command failed ({' '.join(args)}):\n"
f"{result.stdout}{result.stderr}"
)
return result
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Synchronize Git tags with GitHub releases.")
parser.add_argument(
"--repo",
default=".",
help="Ścieżka do repozytorium git (domyślnie: bieżący katalog).",
)
parser.add_argument(
"--remote",
default="origin",
help="Nazwa zdalnego repozytorium do pushowania commitów oraz tagów (domyślnie: origin).",
)
parser.add_argument(
"--output-dir",
default="Release packs",
help="Katalog wyjściowy, w którym powstaną ZIP-y uploadowane do release (domyślnie: 'Release packs').",
)
parser.add_argument(
"--notes-template",
default="Automated release for {tag}",
help="Szablon notatek release (dostępne pola: {tag}).",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Tylko plan bez commitów, tagowania i publikacji release.",
)
return parser.parse_args()
def read_text_if_exists(path: Path) -> Optional[str]:
if not path.is_file():
return None
return path.read_text(encoding="utf-8").strip()
def parse_version(version: str) -> Optional[Tuple[int, ...]]:
if not version:
return None
if not re.fullmatch(r"\d+(?:\.\d+)*", version):
return None
return tuple(int(part) for part in version.split("."))
def bump_version(version: str) -> str:
parts = parse_version(version)
if parts is None:
# fallback: append .1 to non-numeric strings
return f"{version}.1"
bumped = list(parts)
bumped[-1] += 1
return ".".join(str(piece) for piece in bumped)
def sanitize_component(value: str) -> str:
value = value.strip()
if not value:
return "unknown"
return re.sub(r"[\\s/\\\\]+", "-", value)
class ReleaseManager:
def __init__(self, args: argparse.Namespace):
self.args = args
self.repo = Path(args.repo).resolve()
self.output_dir = (self.repo / args.output_dir).resolve()
# ------------------------------------------------------------------ helpers
def git(self, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
return sh(args, cwd=self.repo, check=check)
def ensure_tooling(self) -> None:
sh(["gh", "--version"], cwd=self.repo, check=True)
def ensure_repo(self) -> None:
if not (self.repo / ".git").exists():
raise RuntimeError(f"{self.repo} nie jest repozytorium git.")
def ensure_clean_state(self) -> None:
status = self.git("git", "status", "--porcelain", check=True).stdout.strip()
if status:
raise RuntimeError(
"Wykryto niezakomitowane zmiany. Ukończ commit lub stash przed publikacją release.\n"
"Release podpisany z nieczystego drzewa może wymagać resetu HEAD do taga."
)
upstream = self.git("git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}", check=False)
if upstream.returncode != 0:
raise RuntimeError(
"Brak skonfigurowanego upstreamu dla bieżącej gałęzi nie można zweryfikować, "
"czy wszystkie commity zostały wypchnięte."
)
ahead = self.git("git", "rev-list", "--count", "@{u}..HEAD", check=True).stdout.strip()
if ahead and int(ahead) > 0:
raise RuntimeError(
"Bieżąca gałąź zawiera niewypchnięte commity. Wypchnij zmiany zanim utworzysz release."
)
def get_head_tag(self) -> Optional[str]:
result = self.git("git", "describe", "--tags", "--exact-match", "HEAD", check=False)
tag = result.stdout.strip()
if result.returncode == 0 and tag:
return tag
return None
def list_tags(self) -> List[str]:
tags = self.git("git", "tag", "--sort=creatordate", check=True).stdout.splitlines()
return [tag.strip() for tag in tags if tag.strip()]
def list_version_tags(self) -> List[str]:
tags = []
for tag in self.list_tags():
if parse_version(tag):
tags.append(tag)
return tags
def read_root_version(self) -> Optional[str]:
return read_text_if_exists(self.repo / "version.nfo")
def write_root_version(self, version: str) -> None:
target = self.repo / "version.nfo"
target.write_text(f"{version}\n", encoding="utf-8")
def commit_root_version(self, version: str) -> None:
if self.args.dry_run:
print(f"[DRY-RUN] git add version.nfo && git commit -m 'release: bump root to v{version}'")
return
self.write_root_version(version)
self.git("git", "add", "version.nfo", check=True)
self.git("git", "commit", "-m", f"release: bump root to v{version}", check=True)
self.git("git", "push", self.args.remote, check=True)
def tag_exists(self, tag: str) -> bool:
result = self.git("git", "rev-parse", "-q", "--verify", f"refs/tags/{tag}", check=False)
return result.returncode == 0
def create_tag(self, tag: str, message: str) -> None:
if self.tag_exists(tag):
print(f"[INFO] Tag {tag} już istnieje pomijam tworzenie.")
return
if self.args.dry_run:
print(f"[DRY-RUN] git tag -a {tag} -m \"{message}\"")
return
self.git("git", "tag", "-a", tag, "-m", message, check=True)
self.git("git", "push", self.args.remote, tag, check=True)
def ensure_head_tagged(self) -> str:
head_tag = self.get_head_tag()
if head_tag:
return head_tag
root_version = self.read_root_version()
root_tuple = parse_version(root_version) if root_version else None
version_tags = self.list_version_tags()
latest_version = version_tags[-1] if version_tags else None
latest_tuple = parse_version(latest_version) if latest_version else None
if root_version and (root_tuple and (latest_tuple is None or root_tuple > latest_tuple)):
new_version = root_version
else:
base_version = latest_version or root_version or "0.0"
new_version = bump_version(base_version)
if not self.args.dry_run:
print(f"[INFO] Bump root version.nfo -> {new_version}")
self.commit_root_version(new_version)
tag_message = f"release {new_version}"
self.create_tag(new_version, tag_message)
return new_version
def gather_directories_for_tag(self, tag: str) -> List[str]:
result = self.git("git", "ls-tree", "-d", "--name-only", tag, check=True)
candidates = []
for line in result.stdout.splitlines():
name = line.strip()
if not name:
continue
candidates.append(name)
return candidates
def version_for_directory(self, tag: str, directory: str) -> str:
show = self.git("git", "show", f"{tag}:{directory}/version.nfo", check=False)
if show.returncode == 0:
version = show.stdout.strip()
if version:
return version
return tag
def build_assets_for_tag(self, tag: str) -> List[Path]:
assets: List[Path] = []
directories = self.gather_directories_for_tag(tag)
if not directories:
print(f"[WARN] Tag {tag} nie zawiera katalogów do zzipowania.")
return assets
tag_output_dir = self.output_dir / sanitize_component(tag)
if not self.args.dry_run:
if tag_output_dir.exists():
shutil.rmtree(tag_output_dir)
tag_output_dir.mkdir(parents=True, exist_ok=True)
for directory in directories:
version = self.version_for_directory(tag, directory)
safe_version = sanitize_component(version)
safe_dir = sanitize_component(directory)
zip_name = f"{safe_dir}-v{safe_version}.zip"
zip_path = tag_output_dir / zip_name
if self.args.dry_run:
print(f"[DRY-RUN] Przygotuj ZIP {zip_path} z taga {tag}:{directory} (pomijając version.nfo)")
assets.append(zip_path)
continue
# Re-run capturing binary properly.
archive_proc = subprocess.run(
["git", "archive", "--format=tar", tag, directory],
cwd=str(self.repo),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if archive_proc.returncode != 0:
raise RuntimeError(
f"git archive nie powiódł się dla {tag}:{directory}:\n"
f"{archive_proc.stderr.decode('utf-8', errors='ignore')}"
)
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
with tarfile.open(fileobj=io.BytesIO(archive_proc.stdout)) as tar:
tar.extractall(tmp_path)
extracted_root = tmp_path / directory
for extra in extracted_root.rglob("version.nfo"):
if extra.is_file():
extra.unlink()
with ZipFile(zip_path, "w", ZIP_DEFLATED) as zipf:
for root, _, files in os.walk(extracted_root):
for filename in files:
file_path = Path(root) / filename
rel_path = file_path.relative_to(tmp_path)
zipf.write(file_path, rel_path.as_posix())
assets.append(zip_path)
return assets
def release_exists(self, tag: str) -> bool:
result = self.git("gh", "release", "view", tag, check=False)
return result.returncode == 0
def create_release(self, tag: str, assets: List[Path]) -> None:
notes = self.args.notes_template.format(tag=tag)
wrapped = "\n".join(textwrap.wrap(notes, width=100))
if self.args.dry_run:
print(f"[DRY-RUN] gh release create {tag} {' '.join(str(a) for a in assets)}")
print(f"[DRY-RUN] Notatki:\n{wrapped}")
return
cmd = ["gh", "release", "create", tag, "--title", tag, "--notes", wrapped]
cmd.extend(str(asset) for asset in assets)
result = self.git(*cmd, check=False)
if result.returncode != 0:
raise RuntimeError(
f"Nie udało się utworzyć release {tag}:\n{result.stdout}{result.stderr}"
)
def sync_releases(self) -> None:
tags = self.list_tags()
if not tags:
print("[INFO] Brak tagów do synchronizacji.")
return
self.output_dir.mkdir(parents=True, exist_ok=True)
for tag in tags:
if self.release_exists(tag):
print(f"[INFO] Release {tag} już istnieje pomijam.")
continue
print(f"[INFO] Przygotowuję release dla taga {tag}...")
assets = self.build_assets_for_tag(tag)
self.create_release(tag, assets)
def run(self) -> None:
self.ensure_repo()
self.ensure_tooling()
self.ensure_clean_state()
self.ensure_head_tagged()
self.sync_releases()
def main() -> int:
args = parse_args()
manager = ReleaseManager(args)
try:
manager.run()
except RuntimeError as exc:
print(f"[BŁĄD] {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+57 -22
View File
@@ -57,6 +57,7 @@ DRY_RUN: bool = False # globalny tryb symulacji (nic nie zmienia)
ZIP_MODE: str = "ALL" # "ALL" | "MODULE_ONLY" | "OFF" ZIP_MODE: str = "ALL" # "ALL" | "MODULE_ONLY" | "OFF"
# Katalog wyjściowy na archiwa .zip; jeśli None, użyje SOURCE_BASE_DIR/_zips # Katalog wyjściowy na archiwa .zip; jeśli None, użyje SOURCE_BASE_DIR/_zips
ZIP_OUTPUT_DIR: Optional[str] = r"C:\Release packs" ZIP_OUTPUT_DIR: Optional[str] = r"C:\Release packs"
CHECK_MODE: bool = False # tylko walidacja brak zmian, wyjście !=0 jeśli są akcje
# ========================== # ==========================
# KONIEC KONFIGURACJI # KONIEC KONFIGURACJI
@@ -316,23 +317,24 @@ def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> None:
def run(cmd: List[str]) -> None: def run(cmd: List[str]) -> None:
rc, out = sh(cmd, repo_dir) rc, out = sh(cmd, repo_dir)
if rc != 0: if rc != 0:
raise RuntimeError(f"Błąd podczas uruchamiania: {' '.join(cmd)}\nWyjście:\n{out}") raise RuntimeError(f"Blad podczas uruchamiania: {' '.join(cmd)}\nWyjscie:\n{out}")
if out.strip(): if out.strip():
print(out) print(out)
run(["git", "add", "-A"]) run(["git", "add", "-A"])
rc, out = sh(["git", "commit", "-m", commit_msg], repo_dir) rc, out = sh(["git", "commit", "-m", commit_msg], repo_dir)
if rc != 0: if rc != 0:
raise RuntimeError(f"Błąd podczas git commit:\n{out}") raise RuntimeError(f"Blad podczas git commit:\n{out}")
if out.strip(): if out.strip():
print(out) print(out)
run(["git", "push"]) run(["git", "push"])
def preflight_module(src_dir: Path, dry_run: bool) -> Tuple[str, str]: def preflight_module(src_dir: Path, dry_run: bool) -> Tuple[str, str]:
""" """
Moduł: odczyt module.json, bump, zapis (jeśli nie dry_run), commit i push. Moduł: odczyt module.json, bump, zapis (jeśli nie dry_run), commit i push.
Zwraca (module_id, new_version). Zwraca (module_id, new_version, commit_sha lub None przy dry-run).
""" """
manifest_path = src_dir / "module.json" manifest_path = src_dir / "module.json"
manifest = read_manifest(manifest_path) manifest = read_manifest(manifest_path)
@@ -363,7 +365,7 @@ def preflight_non_module(src_dir: Path, dry_run: bool) -> str:
- jeśli brak -> '0.1' - jeśli brak -> '0.1'
- jeśli jest -> inkrementuj x - jeśli jest -> inkrementuj x
Zapis (nie dry-run) oraz osobny commit/push. Zapis (nie dry-run) oraz osobny commit/push.
Zwraca new_version (np. '0.4'). Zwraca (new_version, commit_sha lub None przy dry-run).
""" """
nfo = src_dir / "version.nfo" nfo = src_dir / "version.nfo"
if not nfo.exists(): if not nfo.exists():
@@ -378,14 +380,16 @@ def preflight_non_module(src_dir: Path, dry_run: bool) -> str:
# istnieje -> wczytaj i bumpnij # istnieje -> wczytaj i bumpnij
content = nfo.read_text(encoding="utf-8").strip() content = nfo.read_text(encoding="utf-8").strip()
if not content.startswith("0."): if not (content.startswith("0.") or content.startswith("1.")):
raise ValueError(f"{norm(nfo)}: oczekiwano formatu '0.x', otrzymano '{content}'") raise ValueError(f"{norm(nfo)}: oczekiwano formatu '0/1.x', otrzymano '{content}'")
tail = content.split(".", 1)[1] tail = content.split(".", 1)[1]
head = content.split(".",1)[0]
try: try:
_ = int(head)
x = int(tail) x = int(tail)
except ValueError: except ValueError:
raise ValueError(f"{norm(nfo)}: część po '0.' nie jest liczbą: '{tail}'") raise ValueError(f"{norm(nfo)}: część po '0/1.' nie jest liczbą: '{tail}'")
new_version = f"0.{x+1}" new_version = f"{head}.{x+1}"
print(f"[PRE][FOLDER] {src_dir.name}: version {content} -> {new_version}") print(f"[PRE][FOLDER] {src_dir.name}: version {content} -> {new_version}")
if not dry_run: if not dry_run:
@@ -520,6 +524,12 @@ def parse_args() -> argparse.Namespace:
default=ZIP_OUTPUT_DIR, default=ZIP_OUTPUT_DIR,
help="Katalog wyjściowy na zipy (domyślnie: SOURCE_BASE_DIR/_zips)") help="Katalog wyjściowy na zipy (domyślnie: SOURCE_BASE_DIR/_zips)")
# Tryb check (tylko walidacja)
p.add_argument("--check", dest="check", action="store_true",
help="Tylko sprawdzenie zmian (wymusza dry-run, kod wyjścia 1 przy wymaganych akcjach).")
p.set_defaults(check=CHECK_MODE)
return p.parse_args() return p.parse_args()
@@ -548,10 +558,11 @@ def process_module(src_dir: Path, modules_base: Path, confirm_delete: bool, dry_
print("[INFO] (dry-run) Pytanie o potwierdzenie pominięte.") print("[INFO] (dry-run) Pytanie o potwierdzenie pominięte.")
# ZIP po commicie, przed podmianą # ZIP po commicie, przed podmianą
zip_path: Optional[Path] = None
if should_zip(is_module=True, zip_mode=ARGS.zip_mode): if should_zip(is_module=True, zip_mode=ARGS.zip_mode):
out_dir = ensure_zip_output_dir(Path(ARGS.source_base_dir), ARGS.zip_output_dir) out_dir = ensure_zip_output_dir(Path(ARGS.source_base_dir), ARGS.zip_output_dir)
try: try:
make_zip_for_folder(src_dir, new_ver, out_dir, dry_run) zip_path = make_zip_for_folder(src_dir, new_ver, out_dir, dry_run)
except Exception as e: except Exception as e:
raise RuntimeError(f"Błąd zipowania modułu {src_dir.name}: {e}") from e raise RuntimeError(f"Błąd zipowania modułu {src_dir.name}: {e}") from e
@@ -566,24 +577,27 @@ def process_module(src_dir: Path, modules_base: Path, confirm_delete: bool, dry_
print(f"[DONE][MODULE] {src_dir.name} -> commit 'sync: {module_id} v{new_ver}' wykonany." if not dry_run print(f"[DONE][MODULE] {src_dir.name} -> commit 'sync: {module_id} v{new_ver}' wykonany." if not dry_run
else f"[DRY-RUN][MODULE] {src_dir.name} -> commit planowany.") else f"[DRY-RUN][MODULE] {src_dir.name} -> commit planowany.")
def process_non_module(src_dir: Path, dry_run: bool) -> None: def process_non_module(src_dir: Path, dry_run: bool) -> None:
new_ver = preflight_non_module(src_dir, dry_run=dry_run) new_ver = preflight_non_module(src_dir, dry_run=dry_run)
# ZIP po commicie (jeśli ustawienie pozwala) # ZIP po commicie (jeśli ustawienie pozwala)
zip_path: Optional[Path] = None
if should_zip(is_module=False, zip_mode=ARGS.zip_mode): if should_zip(is_module=False, zip_mode=ARGS.zip_mode):
out_dir = ensure_zip_output_dir(Path(ARGS.source_base_dir), ARGS.zip_output_dir) out_dir = ensure_zip_output_dir(Path(ARGS.source_base_dir), ARGS.zip_output_dir)
try: try:
make_zip_for_folder(src_dir, new_ver, out_dir, dry_run) zip_path = make_zip_for_folder(src_dir, new_ver, out_dir, dry_run)
except Exception as e: except Exception as e:
raise RuntimeError(f"Błąd zipowania folderu {src_dir.name}: {e}") from e raise RuntimeError(f"Błąd zipowania folderu {src_dir.name}: {e}") from e
print(f"[DONE][FOLDER] {src_dir.name} -> version.nfo = {new_ver}; commit wykonany." if not dry_run print(f"[DONE][FOLDER] {src_dir.name} -> version.nfo = {new_ver}; commit wykonany." if not dry_run
else f"[DRY-RUN][FOLDER] {src_dir.name} -> version.nfo będzie {new_ver}; commit planowany.") else f"[DRY-RUN][FOLDER] {src_dir.name} -> version.nfo będzie {new_ver}; commit planowany.")
def main() -> int: def main() -> int:
global ARGS global ARGS
ARGS = parse_args() ARGS = parse_args()
check_mode = ARGS.check
if check_mode:
ARGS.dry_run = True
mode = ARGS.mode mode = ARGS.mode
user = ARGS.user user = ARGS.user
appdata_kind = ARGS.appdata_kind if ARGS.appdata_kind != "None" else None appdata_kind = ARGS.appdata_kind if ARGS.appdata_kind != "None" else None
@@ -595,39 +609,57 @@ def main() -> int:
dry_run = ARGS.dry_run dry_run = ARGS.dry_run
if not source_base.is_dir(): if not source_base.is_dir():
raise FileNotFoundError(f"Katalog źródłowy nie istnieje: {norm(source_base)}") raise FileNotFoundError(f"Katalog zrodlowy nie istnieje: {norm(source_base)}")
ensure_git_repo(source_base) ensure_git_repo(source_base)
# Ustal bazę modules (tylko raz)
modules_base = resolve_modules_base(user, relative_pattern, appdata_kind, modules_override) modules_base = resolve_modules_base(user, relative_pattern, appdata_kind, modules_override)
print(f"[INFO] Katalog modules base: {norm(modules_base)}") print(f"[INFO] Katalog modules base: {norm(modules_base)}")
pending_actions = False
if mode == "single": if mode == "single":
src_dir = source_base / module_subdir_single src_dir = source_base / module_subdir_single
if not src_dir.is_dir(): if not src_dir.is_dir():
raise FileNotFoundError(f"[SINGLE] Brak katalogu: {norm(src_dir)}") raise FileNotFoundError(f"[SINGLE] Brak katalogu: {norm(src_dir)}")
# Root bump (globalna wersja) tylko jeśli "są zmiany do przetworzenia" (single -> True)
maybe_bump_root_version(source_base, changed_any=True, dry_run=dry_run) root_bumped = maybe_bump_root_version(source_base, changed_any=True, dry_run=dry_run)
if root_bumped:
pending_actions = True
pending_actions = True
if (src_dir / "module.json").is_file(): if (src_dir / "module.json").is_file():
print(f"[MODE=SINGLE][MODULE] {src_dir.name}") print(f"[MODE=SINGLE][MODULE] {src_dir.name}")
process_module(src_dir, modules_base, confirm_delete, dry_run) process_module(src_dir, modules_base, confirm_delete, dry_run)
else: else:
print(f"[MODE=SINGLE][FOLDER] {src_dir.name}") print(f"[MODE=SINGLE][FOLDER] {src_dir.name}")
process_non_module(src_dir, dry_run) process_non_module(src_dir, dry_run)
print("[SUKCES] Tryb SINGLE zakończony.") print("[SUKCES] Tryb SINGLE zakonczony.")
if check_mode:
print("[CHECK] Wykryto oczekujace akcje (tryb single).")
return 1
return 0 return 0
# AUTO: wykryj zmienione katalogi
changed = git_changed_top_level_dirs(source_base) changed = git_changed_top_level_dirs(source_base)
print(f"[AUTO] Zmienione katalogi w {norm(source_base)}: {changed if changed else '(brak)'}") print(f"[AUTO] Zmienione katalogi w {norm(source_base)}: {changed if changed else '(brak)'}")
# Root bump (globalna wersja) tylko jeśli są jakiekolwiek zmiany do przetworzenia if changed:
maybe_bump_root_version(source_base, changed_any=bool(changed), dry_run=dry_run) pending_actions = True
root_bumped = maybe_bump_root_version(source_base, changed_any=bool(changed), dry_run=dry_run)
if root_bumped:
pending_actions = True
if not changed: if not changed:
print("[INFO] Brak zmian do przetworzenia.") print("[INFO] Brak zmian do przetworzenia.")
if check_mode:
if pending_actions:
print("[CHECK] Wykryto oczekujace akcje.")
return 1
print("[CHECK] OK - brak akcji wymaganych.")
return 0 return 0
# Przetwarzaj w stałej kolejności # Przetwarzaj w stalej kolejnosci
for name in changed: for name in changed:
src_dir = source_base / name src_dir = source_base / name
if (src_dir / "module.json").is_file(): if (src_dir / "module.json").is_file():
@@ -637,7 +669,10 @@ def main() -> int:
print(f"\n[AUTO][FOLDER] {name}") print(f"\n[AUTO][FOLDER] {name}")
process_non_module(src_dir, dry_run) process_non_module(src_dir, dry_run)
print("\n[SUKCES] Tryb AUTO zakończony.") print("\n[SUKCES] Tryb AUTO zakonczony.")
if check_mode and pending_actions:
print("[CHECK] Wykryto oczekujace akcje.")
return 1
return 0 return 0
+1 -1
View File
@@ -1 +1 @@
0.3 0.5
-516
View File
@@ -1,516 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Control Board Monitor - v0.1
A lightweight Windows-friendly Python GUI to monitor server variables exposed via
HTTP GET as "/?variable=VARNAME".
- Defaults: host="localhost", port=8945 (edit globals below or use the GUI).
- Refreshes automatically every minute.
- Fetches variables sequentially with a configurable Requests-Per-Second (RPS) cap
and automatic backoff on HTTP 429/5xx to avoid overwhelming the server.
- Attempts to discover the variable list via WEBSERVER_LIST_VARIABLES_JSON, then
WEBSERVER_LIST_VARIABLES. You can also paste/import a list manually in the GUI.
- First version focuses on read-only monitoring. Next versions can add POST controls
to set variables and turn some values into indicator widgets.
No external GUI frameworks are required (uses Tkinter from the standard library).
No non-stdlib HTTP client is required (uses urllib).
"""
import json
import queue
import re
import threading
import time
import urllib.parse
import urllib.request
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, List, Optional, Tuple
try:
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
except Exception as e:
raise SystemExit("Tkinter is required to run this app.")
# =====================
# Global configuration
# =====================
SERVER_HOST: str = "localhost" # default host
SERVER_PORT: int = 8785 # default port (as requested)
REFRESH_INTERVAL_S: int = 60 # run a full sweep roughly every minute
MAX_RPS: float = 5.0 # cap average requests/sec to avoid overload
REQUEST_TIMEOUT_S: float = 5.0 # per-request timeout
USER_AGENT: str = "ControlBoardMonitor/0.1 (+tkinter)"
CONNECT_TIMEOUT_S: float = 5.0
# Backoff
BACKOFF_BASE_S: float = 1.0 # initial backoff
BACKOFF_MAX_S: float = 15.0 # max backoff between retries
MAX_RETRIES: int = 2 # retries per variable within a cycle
# =====================
# Helper utilities
# =====================
def http_get(base_url: str, params: Dict[str, str]) -> Tuple[int, str, Dict[str, str]]:
"""Perform a GET with urllib. Returns (status, text, headers)."""
url = base_url + "?" + urllib.parse.urlencode(params)
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
try:
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT_S) as resp:
status = resp.getcode()
body_bytes = resp.read()
try:
body = body_bytes.decode("utf-8", errors="replace")
except Exception:
body = body_bytes.decode("latin-1", errors="replace")
headers = {k.lower(): v for k, v in resp.getheaders()}
return status, body, headers
except urllib.error.HTTPError as e:
try:
body = e.read().decode("utf-8", errors="replace")
except Exception:
body = str(e)
return e.code, body, dict(e.headers or {})
except Exception as e:
return 0, str(e), {}
def build_base_url(host: str, port: int) -> str:
scheme = "http"
return f"{scheme}://{host}:{port}/"
def parse_variable_names_from_html_index(html_text: str) -> List[str]:
"""Best-effort extraction of variable names from the simple HTML index page style."""
vars_found: List[str] = []
for m in re.finditer(r"[?&]variable=([A-Z0-9_]+)", html_text):
vars_found.append(m.group(1))
# De-duplicate preserving order
seen = set()
uniq = []
for v in vars_found:
if v not in seen:
uniq.append(v)
seen.add(v)
return uniq
def coerce_preview(value: str, maxlen: int = 80) -> str:
v = value.strip().replace("\r", " ").replace("\n", " ")
if len(v) > maxlen:
return v[:maxlen - 1] + ""
return v
def infer_status_color(value: str) -> str:
"""Very simple status → tag mapping to mimic control-board feel."""
val = value.strip().lower()
if val in {"1", "true", "on", "open", "active", "ok", "running", "enabled"}:
return "ok"
if val in {"0", "false", "off", "closed", "inactive", "error", "fault", "disabled"}:
return "bad"
# Warn if it's empty or has obvious error keywords
if val == "" or "exception" in val or "error" in val:
return "bad"
return "neutral"
# =====================
# Data model
# =====================
@dataclass
class VarInfo:
name: str
last_value: str = ""
last_updated: float = 0.0
last_status: int = 0
error: Optional[str] = None
# =====================
# Poller thread
# =====================
class Poller(threading.Thread):
def __init__(
self,
host: str,
port: int,
variables: List[str],
ui_queue: queue.Queue,
refresh_interval: int,
max_rps: float,
stop_event: threading.Event,
paused_event: threading.Event,
) -> None:
super().__init__(daemon=True)
self.host = host
self.port = port
self.variables = variables
self.ui_queue = ui_queue
self.refresh_interval = int(max(5, refresh_interval))
self.max_rps = max(0.5, max_rps)
self.stop_event = stop_event
self.paused_event = paused_event
def run(self) -> None:
base_url = build_base_url(self.host, self.port)
min_spacing = 1.0 / self.max_rps # seconds between requests
while not self.stop_event.is_set():
cycle_start = time.time()
self.ui_queue.put(("cycle_start", datetime.now().strftime("%H:%M:%S")))
for name in list(self.variables):
# Allow pausing mid-cycle
while self.paused_event.is_set() and not self.stop_event.is_set():
time.sleep(0.1)
if self.stop_event.is_set():
break
attempt = 0
backoff = BACKOFF_BASE_S
while attempt <= MAX_RETRIES and not self.stop_event.is_set():
attempt += 1
t0 = time.time()
status, body, headers = http_get(base_url, {"variable": name})
# Success path
if status == 200:
self.ui_queue.put(("update", name, body, status))
break
# Backoff on overload or server error
if status in (429, 500, 502, 503, 504, 0):
self.ui_queue.put(("error", name, status, coerce_preview(body, 200)))
time.sleep(backoff)
backoff = min(BACKOFF_MAX_S, backoff * 2.0)
else:
# Other HTTP errors: report once and stop retrying
self.ui_queue.put(("error", name, status, coerce_preview(body, 200)))
break
# Respect RPS even with errors
dt = time.time() - t0
if dt < min_spacing:
time.sleep(min_spacing - dt)
# Spacing between requests
dt = time.time() - t0
if dt < min_spacing:
time.sleep(min_spacing - dt)
# Sleep to align roughly to the refresh interval
cycle_dt = time.time() - cycle_start
remaining = self.refresh_interval - cycle_dt
if remaining > 0:
for _ in range(int(remaining * 10)):
if self.stop_event.is_set():
break
time.sleep(0.1)
self.ui_queue.put(("stopped", ""))
# =====================
# GUI
# =====================
class App(tk.Tk):
def __init__(self) -> None:
super().__init__()
self.title("Control Board Monitor")
self.geometry("1100x700")
try:
self.iconbitmap(default="") # no icon file bundled
except Exception:
pass
# State
self.vars: Dict[str, VarInfo] = {}
self.stop_event = threading.Event()
self.paused_event = threading.Event()
self.ui_queue: queue.Queue = queue.Queue()
self.poller: Optional[Poller] = None
self.variables_list: List[str] = []
# Top controls
controls = ttk.Frame(self, padding=10)
controls.pack(side=tk.TOP, fill=tk.X)
self.host_var = tk.StringVar(value=SERVER_HOST)
self.port_var = tk.IntVar(value=SERVER_PORT)
self.refresh_var = tk.IntVar(value=REFRESH_INTERVAL_S)
self.rps_var = tk.DoubleVar(value=MAX_RPS)
ttk.Label(controls, text="Host:").grid(row=0, column=0, sticky="w")
ttk.Entry(controls, textvariable=self.host_var, width=16).grid(row=0, column=1, padx=(0, 12))
ttk.Label(controls, text="Port:").grid(row=0, column=2, sticky="w")
ttk.Entry(controls, textvariable=self.port_var, width=8).grid(row=0, column=3, padx=(0, 12))
ttk.Label(controls, text="Refresh (s):").grid(row=0, column=4, sticky="w")
ttk.Entry(controls, textvariable=self.refresh_var, width=8).grid(row=0, column=5, padx=(0, 12))
ttk.Label(controls, text="Max RPS:").grid(row=0, column=6, sticky="w")
ttk.Entry(controls, textvariable=self.rps_var, width=8).grid(row=0, column=7, padx=(0, 12))
self.status_lbl = ttk.Label(controls, text="Idle", foreground="#666")
self.status_lbl.grid(row=0, column=8, padx=(10, 0))
btns = ttk.Frame(controls)
btns.grid(row=0, column=9, padx=10, sticky="e")
self.start_btn = ttk.Button(btns, text="Start", command=self.start_polling)
self.start_btn.grid(row=0, column=0, padx=4)
self.pause_btn = ttk.Button(btns, text="Pause", command=self.toggle_pause, state="disabled")
self.pause_btn.grid(row=0, column=1, padx=4)
self.reload_btn = ttk.Button(btns, text="Reload Vars", command=self.reload_variables)
self.reload_btn.grid(row=0, column=2, padx=4)
self.import_btn = ttk.Button(btns, text="Import List", command=self.import_list_dialog)
self.import_btn.grid(row=0, column=3, padx=4)
# Filter/Search
search_frame = ttk.Frame(self, padding=(10, 0, 10, 10))
search_frame.pack(side=tk.TOP, fill=tk.X)
ttk.Label(search_frame, text="Filter:").pack(side=tk.LEFT)
self.filter_var = tk.StringVar(value="")
self.filter_var.trace_add("write", lambda *_: self.refresh_tree())
ttk.Entry(search_frame, textvariable=self.filter_var, width=50).pack(side=tk.LEFT, padx=8)
# Tree (control-board style with groups)
columns = ("value", "updated", "status")
self.tree = ttk.Treeview(self, columns=columns, show="tree headings", height=26)
self.tree.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=10)
self.tree.heading("#0", text="Variable")
self.tree.heading("value", text="Value")
self.tree.heading("updated", text="Updated")
self.tree.heading("status", text="HTTP")
self.tree.column("#0", width=420, anchor="w")
self.tree.column("value", width=420, anchor="w")
self.tree.column("updated", width=120, anchor="center")
self.tree.column("status", width=60, anchor="center")
# Style tags for a control-board feel
style = ttk.Style(self)
try:
style.theme_use("clam")
except Exception:
pass
self.tree.tag_configure("ok", background="#eaffea") # soft green
self.tree.tag_configure("bad", background="#ffecec") # soft red
self.tree.tag_configure("neutral", background="#f5f7fb") # soft gray-blue
# Footer
footer = ttk.Frame(self, padding=10)
footer.pack(side=tk.BOTTOM, fill=tk.X)
self.cycle_lbl = ttk.Label(footer, text="Last cycle: —")
self.cycle_lbl.pack(side=tk.LEFT)
self.protocol("WM_DELETE_WINDOW", self.on_close)
# Initial discovery
self.after(200, self.reload_variables)
# ------------- Variable discovery -------------
def reload_variables(self) -> None:
host = self.host_var.get().strip()
port = int(self.port_var.get())
base = build_base_url(host, port)
discovered: List[str] = []
# Try JSON list first
status, body, _ = http_get(base, {"variable": "WEBSERVER_LIST_VARIABLES_JSON"})
if status == 200:
try:
data = json.loads(body)
if isinstance(data, list) and all(isinstance(x, str) for x in data):
discovered = list(dict.fromkeys(data)) # preserve order, unique
except Exception:
discovered = []
# Fallback: parse the human/HTML list
if not discovered:
status, body, _ = http_get(base, {"variable": "WEBSERVER_LIST_VARIABLES"})
if status == 200 and body:
cand = parse_variable_names_from_html_index(body)
if cand:
discovered = cand
# If still empty, offer a minimal starter set
if not discovered:
discovered = [
"TIME", "TIME_STAMP", "CORE_TEMP", "CORE_PRESSURE", "CORE_STATE",
"COOLANT_CORE_PRESSURE", "COOLANT_CORE_FLOW_IN", "COOLANT_CORE_FLOW_OUT",
"STEAM_TURBINE_0_RPM", "POWER_FROM_TURBINE_KW", "POWER_DEMAND_MW",
]
# Update local state
self.variables_list = discovered
self.vars.clear()
for v in discovered:
self.vars[v] = VarInfo(name=v)
self.status_lbl.configure(text=f"Loaded {len(discovered)} vars")
self.refresh_tree()
def import_list_dialog(self) -> None:
txt = simpledialog.askstring(
"Import variable names",
"Paste variable names separated by spaces, commas, or newlines:",
parent=self,
)
if not txt:
return
names = re.findall(r"[A-Z0-9_]+", txt.upper())
if not names:
messagebox.showwarning("Nothing found", "No variable-like tokens found.")
return
# Deduplicate while preserving order
uniq = []
seen = set()
for n in names:
if n not in seen:
uniq.append(n); seen.add(n)
self.variables_list = uniq
self.vars.clear()
for v in uniq:
self.vars[v] = VarInfo(name=v)
self.status_lbl.configure(text=f"Imported {len(uniq)} vars")
self.refresh_tree()
# ------------- Poller control -------------
def start_polling(self) -> None:
if self.poller and self.poller.is_alive():
messagebox.showinfo("Already running", "Polling is already active.")
return
if not self.variables_list:
messagebox.showwarning("No variables", "Variable list is empty. Reload or import a list first.")
return
self.stop_event.clear()
self.paused_event.clear()
self.poller = Poller(
host=self.host_var.get().strip(),
port=int(self.port_var.get()),
variables=self.variables_list,
ui_queue=self.ui_queue,
refresh_interval=int(self.refresh_var.get()),
max_rps=float(self.rps_var.get()),
stop_event=self.stop_event,
paused_event=self.paused_event,
)
self.poller.start()
self.start_btn.configure(state="disabled")
self.pause_btn.configure(state="normal", text="Pause")
self.status_lbl.configure(text="Running…")
self.after(100, self.drain_queue)
def toggle_pause(self) -> None:
if not (self.poller and self.poller.is_alive()):
return
if self.paused_event.is_set():
self.paused_event.clear()
self.pause_btn.configure(text="Pause")
self.status_lbl.configure(text="Running…")
else:
self.paused_event.set()
self.pause_btn.configure(text="Resume")
self.status_lbl.configure(text="Paused")
def drain_queue(self) -> None:
"""Receive updates from the poller and apply to the UI safely from the main thread."""
try:
while True:
item = self.ui_queue.get_nowait()
kind = item[0]
if kind == "update":
_, name, value, status = item
vi = self.vars.get(name) or VarInfo(name=name)
vi.last_value = value
vi.last_updated = time.time()
vi.last_status = status
vi.error = None
self.vars[name] = vi
elif kind == "error":
_, name, status, msg = item
vi = self.vars.get(name) or VarInfo(name=name)
vi.last_status = status
vi.error = msg
vi.last_updated = time.time()
self.vars[name] = vi
elif kind == "cycle_start":
_, hhmmss = item
self.cycle_lbl.configure(text=f"Last cycle start: {hhmmss}")
elif kind == "stopped":
self.status_lbl.configure(text="Stopped")
self.ui_queue.task_done()
except queue.Empty:
pass
self.refresh_tree()
if self.poller and self.poller.is_alive():
self.after(250, self.drain_queue) # continue pumping
def stop_polling(self) -> None:
if self.poller and self.poller.is_alive():
self.stop_event.set()
# give it a moment to end gracefully
for _ in range(20):
if not self.poller.is_alive():
break
time.sleep(0.05)
self.start_btn.configure(state="normal")
self.pause_btn.configure(state="disabled", text="Pause")
# ------------- Tree rendering -------------
def refresh_tree(self) -> None:
# Clear existing
self.tree.delete(*self.tree.get_children())
# Group by prefix (e.g. CORE_, COOLANT_, STEAM_)
groups: Dict[str, List[VarInfo]] = {}
filt = self.filter_var.get().strip().upper()
for name, vi in self.vars.items():
if filt and filt not in name.upper():
# Also allow filtering by value preview
if vi.last_value and filt not in vi.last_value.upper():
continue
prefix = name.split("_", 1)[0] if "_" in name else "MISC"
groups.setdefault(prefix, []).append(vi)
# Sort groups alphabetically, vars within group by name
for g in sorted(groups.keys()):
parent = self.tree.insert("", "end", text=g, values=("", "", ""), open=True)
for vi in sorted(groups[g], key=lambda x: x.name):
updated_str = ""
if vi.last_updated:
updated_str = datetime.fromtimestamp(vi.last_updated).strftime("%H:%M:%S")
if vi.error:
value_preview = f"[ERR] {vi.error}"
else:
# Compact preview of first line
value_preview = coerce_preview(vi.last_value, 120)
tag = infer_status_color(vi.last_value if not vi.error else "error")
self.tree.insert(
parent,
"end",
text=vi.name,
values=(value_preview, updated_str, vi.last_status or ""),
tags=(tag,),
)
# ------------- Shutdown -------------
def on_close(self) -> None:
self.stop_polling()
self.destroy()
def main() -> None:
app = App()
app.mainloop()
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
# Zależności vtt_work — wyłącznie dla help_scripts/sound.py (generator dźwięków).
# Pozostałe skrypty Foundry używają czystego stdlib.
numpy==2.3.3
scipy==1.16.2
soundfile==0.13.1
cffi==2.0.0
pycparser==2.23
+1 -1
View File
@@ -1 +1 @@
0.1 0.4