Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5715da67de | |||
| a2a723a49f | |||
| deb1cc55fa | |||
| 93cbd4eb01 | |||
| 17b2747130 | |||
| 4677df4b80 | |||
| e0916f4845 | |||
| 1cc28b18a1 | |||
| 0c365665c0 | |||
| e730729493 | |||
| 53c33c888d | |||
| 692bcb3b4b | |||
| 4d0f601c7c | |||
| c0a46c7bef | |||
| 04923cc449 | |||
| 4beb01118a | |||
| e06e297540 | |||
| 8d5aac56d1 | |||
| 45c98308fb | |||
| 9896a811f8 | |||
| fd18fcf0bd | |||
| 7928d17ea9 | |||
| 5a873b4e6e | |||
| cac380ca5e | |||
| 792694f6ac | |||
| e644daff99 | |||
| 3b88f4c8eb | |||
| a2fbabedce | |||
| 23084f4cb4 | |||
| 65b3989099 | |||
| a73459ea87 | |||
| c8a439c2f3 | |||
| 311cb768b9 | |||
| 61a8cb40a4 | |||
| ca4a3b6523 |
@@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*__pycache__/
|
||||
*.log
|
||||
*.tmp
|
||||
@@ -0,0 +1,9 @@
|
||||
*out
|
||||
*logs
|
||||
*actions
|
||||
*notifications
|
||||
*tools
|
||||
plugins
|
||||
user_trunk.yaml
|
||||
user.yaml
|
||||
tmp
|
||||
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
profile=black
|
||||
@@ -0,0 +1,2 @@
|
||||
# Prettier friendly markdownlint config (all formatting rules disabled)
|
||||
extends: markdownlint/style/prettier
|
||||
@@ -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"]
|
||||
@@ -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
@@ -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)
|
||||
@@ -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,6 +57,7 @@ DRY_RUN: bool = False # globalny tryb symulacji (nic nie zmienia)
|
||||
ZIP_MODE: str = "ALL" # "ALL" | "MODULE_ONLY" | "OFF"
|
||||
# Katalog wyjściowy na archiwa .zip; jeśli None, użyje SOURCE_BASE_DIR/_zips
|
||||
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
|
||||
@@ -316,23 +317,24 @@ def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> None:
|
||||
def run(cmd: List[str]) -> None:
|
||||
rc, out = sh(cmd, repo_dir)
|
||||
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():
|
||||
print(out)
|
||||
|
||||
run(["git", "add", "-A"])
|
||||
rc, out = sh(["git", "commit", "-m", commit_msg], repo_dir)
|
||||
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():
|
||||
print(out)
|
||||
run(["git", "push"])
|
||||
|
||||
|
||||
|
||||
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.
|
||||
Zwraca (module_id, new_version).
|
||||
Zwraca (module_id, new_version, commit_sha lub None przy dry-run).
|
||||
"""
|
||||
manifest_path = src_dir / "module.json"
|
||||
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 jest -> inkrementuj x
|
||||
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"
|
||||
if not nfo.exists():
|
||||
@@ -378,14 +380,16 @@ def preflight_non_module(src_dir: Path, dry_run: bool) -> str:
|
||||
|
||||
# istnieje -> wczytaj i bumpnij
|
||||
content = nfo.read_text(encoding="utf-8").strip()
|
||||
if not content.startswith("0."):
|
||||
raise ValueError(f"{norm(nfo)}: oczekiwano formatu '0.x', otrzymano '{content}'")
|
||||
if not (content.startswith("0.") or content.startswith("1.")):
|
||||
raise ValueError(f"{norm(nfo)}: oczekiwano formatu '0/1.x', otrzymano '{content}'")
|
||||
tail = content.split(".", 1)[1]
|
||||
head = content.split(".",1)[0]
|
||||
try:
|
||||
_ = int(head)
|
||||
x = int(tail)
|
||||
except ValueError:
|
||||
raise ValueError(f"{norm(nfo)}: część po '0.' nie jest liczbą: '{tail}'")
|
||||
new_version = f"0.{x+1}"
|
||||
raise ValueError(f"{norm(nfo)}: część po '0/1.' nie jest liczbą: '{tail}'")
|
||||
new_version = f"{head}.{x+1}"
|
||||
print(f"[PRE][FOLDER] {src_dir.name}: version {content} -> {new_version}")
|
||||
|
||||
if not dry_run:
|
||||
@@ -520,6 +524,12 @@ def parse_args() -> argparse.Namespace:
|
||||
default=ZIP_OUTPUT_DIR,
|
||||
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()
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
# ZIP po commicie, przed podmianą
|
||||
zip_path: Optional[Path] = None
|
||||
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)
|
||||
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:
|
||||
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
|
||||
else f"[DRY-RUN][MODULE] {src_dir.name} -> commit planowany.")
|
||||
|
||||
|
||||
def process_non_module(src_dir: Path, dry_run: bool) -> None:
|
||||
new_ver = preflight_non_module(src_dir, dry_run=dry_run)
|
||||
# ZIP po commicie (jeśli ustawienie pozwala)
|
||||
zip_path: Optional[Path] = None
|
||||
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)
|
||||
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:
|
||||
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
|
||||
else f"[DRY-RUN][FOLDER] {src_dir.name} -> version.nfo będzie {new_ver}; commit planowany.")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
global ARGS
|
||||
ARGS = parse_args()
|
||||
|
||||
check_mode = ARGS.check
|
||||
if check_mode:
|
||||
ARGS.dry_run = True
|
||||
|
||||
mode = ARGS.mode
|
||||
user = ARGS.user
|
||||
appdata_kind = ARGS.appdata_kind if ARGS.appdata_kind != "None" else None
|
||||
@@ -595,39 +609,57 @@ def main() -> int:
|
||||
dry_run = ARGS.dry_run
|
||||
|
||||
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)
|
||||
|
||||
# Ustal bazę modules (tylko raz)
|
||||
modules_base = resolve_modules_base(user, relative_pattern, appdata_kind, modules_override)
|
||||
print(f"[INFO] Katalog modules base: {norm(modules_base)}")
|
||||
|
||||
pending_actions = False
|
||||
|
||||
if mode == "single":
|
||||
src_dir = source_base / module_subdir_single
|
||||
if not src_dir.is_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():
|
||||
print(f"[MODE=SINGLE][MODULE] {src_dir.name}")
|
||||
process_module(src_dir, modules_base, confirm_delete, dry_run)
|
||||
else:
|
||||
print(f"[MODE=SINGLE][FOLDER] {src_dir.name}")
|
||||
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
|
||||
|
||||
# AUTO: wykryj zmienione katalogi
|
||||
changed = git_changed_top_level_dirs(source_base)
|
||||
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
|
||||
maybe_bump_root_version(source_base, changed_any=bool(changed), dry_run=dry_run)
|
||||
if changed:
|
||||
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:
|
||||
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
|
||||
|
||||
# Przetwarzaj w stałej kolejności
|
||||
# Przetwarzaj w stalej kolejnosci
|
||||
for name in changed:
|
||||
src_dir = source_base / name
|
||||
if (src_dir / "module.json").is_file():
|
||||
@@ -637,7 +669,10 @@ def main() -> int:
|
||||
print(f"\n[AUTO][FOLDER] {name}")
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.3
|
||||
0.5
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -1 +1 @@
|
||||
0.1
|
||||
0.4
|
||||
|
||||
Reference in New Issue
Block a user