Compare commits

...

231 Commits

Author SHA1 Message Date
CI
efabf060c7 chore: release version v1.75.1
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
Flaky Test Detection / 🔍 Detect Flaky Tests (push) Has been cancelled
Flaky Test Detection / 📊 Analyze Test History (push) Has been cancelled
2025-07-30 07:11:08 +00:00
Dmitry Popov
96e434ebf5 Merge pull request #479 from guarzo/guarzo/rally 2025-07-30 11:10:42 +04:00
guarzo
d81e2567cc fix: unable to cancel ping from right click context menu 2025-07-30 03:12:27 +00:00
CI
f8d487639f chore: release version v1.75.0
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Flaky Test Detection / 🔍 Detect Flaky Tests (push) Has been cancelled
Flaky Test Detection / 📊 Analyze Test History (push) Has been cancelled
2025-07-29 22:07:57 +00:00
Dmitry Popov
cecfbb5375 Merge pull request #471 from wanderer-industries/develop
Develop
2025-07-30 02:07:03 +04:00
CI
8d35500e2f chore: release version v1.74.13
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-29 15:15:15 +00:00
Dmitry Popov
5dad5d8e03 fix(Core): Fixed issue with callback url 2025-07-29 17:11:28 +02:00
Dmitry Popov
7be64bde02 Merge pull request #476 from guarzo/guarzo/lazy
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-27 18:04:59 +04:00
guarzo
48eb7552a9 fix: remove bug with lazy delete 2025-07-27 08:12:59 -04:00
Dmitry Popov
5347b0060c Merge pull request #475 from guarzo/guarzo/moretags
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
ops: add docker push for develop branch
2025-07-24 11:38:24 +04:00
guarzo
b826c03226 ops: add docker push for develop branch 2025-07-23 21:20:03 -04:00
Dmitry Popov
1c211a8667 Merge pull request #474 from guarzo/guarzo/devfix
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
fix: update broken links and remove verbose logging
2025-07-23 15:43:30 +02:00
CI
fd4d5b90e2 chore: release version v1.74.12
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-22 11:37:44 +00:00
Dmitry Popov
1ee9f26b34 chore: fixed .tool-versions 2025-07-22 13:37:06 +02:00
Dmitry Popov
da1762934b Merge pull request #474 from guarzo/guarzo/devfix
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
fix: update broken links and remove verbose logging
2025-07-22 14:32:24 +04:00
guarzo
511457c761 fix: update broken length and remove verbose logging 2025-07-21 23:21:07 +00:00
Dmitry Popov
29b4cedb81 chore: fixed user settings missing methods
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-07-22 00:01:09 +02:00
Dmitry Popov
585de15e6b chore: fixed compile errors
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-07-18 17:41:38 +02:00
Dmitry Popov
a9bf118f3a Merge pull request #470 from wanderer-industries/test
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Test
2025-07-18 15:49:06 +04:00
Dmitry Popov
6d5a432bad Merge pull request #450 from wanderer-industries/develop
Develop
2025-07-18 15:46:44 +04:00
Dmitry Popov
f1f12abd16 Merge pull request #469 from guarzo/guarzo/devmerge
refactor: merge develop branch
2025-07-18 15:45:19 +04:00
CI
09880a54e9 chore: release version v1.74.11
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-18 11:41:22 +00:00
Dmitry Popov
0f6847b16d fix(Map): Fixed remove pings for removed systems 2025-07-18 13:39:36 +02:00
Dmitry Popov
ce82ed97f5 Merge pull request #467 from guarzo/guarzo/frig
feat: autoset connection size for c4->null and c13
2025-07-18 15:19:35 +04:00
guarzo
36b393dbde Merge branch 'develop' into guarzo/frig 2025-07-17 19:36:21 -04:00
guarzo
524c283a0d refactor: use constants for ship size 2025-07-17 23:36:11 +00:00
guarzo
afda53a9bc Fix test failures after merge
- lib/wanderer_app/map/operations/systems.ex: Restore async system creation behavior
  Returns immediate success without waiting for DB fetch, which was the original
  intended behavior before the merge

- lib/wanderer_app/kills/storage.ex: Fix killmail storage to handle missing IDs gracefully
  Filter out killmails without killmail_id instead of returning errors,
  allowing valid killmails to be stored successfully

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-17 23:28:25 +00:00
guarzo
1310d75012 Merge main into develop
Resolved merge conflicts in multiple files:
- lib/wanderer_app/application.ex: merged kills service config logic
- lib/wanderer_app/map/map_audit.ex: kept security audit functionality
- lib/wanderer_app/map/operations/connections.ex: preserved time_status support
- lib/wanderer_app/map/operations/owner.ex: kept type guard for map_id
- lib/wanderer_app/map/operations/structures.ex: preserved structure type handling
- lib/wanderer_app/map/operations/systems.ex: unified system creation approach
- lib/wanderer_app_web/controllers/map_connection_api_controller.ex: kept time_status in allowed fields
- lib/wanderer_app_web/controllers/map_system_api_controller.ex: unified delete approach
- lib/wanderer_app_web/controllers/plugs/check_map_api_key.ex: kept owner character fetching
- test/unit/kills_storage_test.exs: unified test approach for killmail handling
- test/unit/character_api_controller_test.exs: removed as intended

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-17 23:19:18 +00:00
guarzo
80bbde549d fix: removed old documents 2025-07-17 19:07:31 -04:00
Dmitry Popov
2451487593 Merge pull request #465 from guarzo/guarzo/realapi
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
feat: implement JSON:API v1 compliant interface with versioned routing
2025-07-18 02:54:50 +04:00
guarzo
ecd626f105 fix: removed unneeded api, and fixed data comparision bug 2025-07-17 15:25:06 +00:00
guarzo
123b312965 feat: autoset connection size for c4->null and c13 2025-07-17 01:55:55 +00:00
guarzo
e94de8e629 fix: ci comments 2025-07-17 00:00:36 +00:00
guarzo
956a5a04ca fix: test updates 2025-07-16 23:17:47 +00:00
guarzo
affeb7c624 feat: apiv1 and tests 2025-07-16 20:39:30 +00:00
CI
e457d94df8 chore: release version v1.74.10
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-15 12:49:00 +00:00
Dmitry Popov
e9583c928e Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-07-15 14:48:23 +02:00
Dmitry Popov
89c14628e1 chore: mix format 2025-07-15 14:48:20 +02:00
Dmitry Popov
ffba407eaf Merge pull request #461 from guarzo/guarzo/dupe
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
feat: add map duplication api
2025-07-15 14:55:54 +04:00
guarzo
33f710127c feature: add duplicate map api 2025-07-13 21:12:32 -04:00
CI
7a82b2c102 chore: release version v1.74.9
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-13 17:51:56 +00:00
Dmitry Popov
2db2a47186 Merge pull request #460 from wanderer-industries/fast-forward-bug
fix(Map): Trying to fix problem with fast forwarding after page are i…
2025-07-13 21:51:32 +04:00
Dmitry Popov
63faa43c1d Merge pull request #459 from guarzo/guarzo/dupeandrally
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
feat: add rally point events
2025-07-13 18:35:26 +04:00
DanSylvest
eabb0e8470 fix(Map): Trying to fix problem with fast forwarding after page are inactive some time. 2025-07-13 15:20:33 +03:00
guarzo
9f75ae6b03 feature: add rallypoint external events 2025-07-12 23:43:19 +00:00
Dmitry Popov
a1f28cd245 Merge pull request #458 from guarzo/guarzo/test_merge
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
refactor: add test coverage for api and operations
2025-07-13 02:53:34 +04:00
Dmitry Popov
90a04b517e Merge pull request #457 from guarzo/guarzo/ssefix
fix: properly send sse events
2025-07-13 02:49:10 +04:00
guarzo
9f6e6a333f fix: properly send sse events 2025-07-12 22:43:49 +00:00
guarzo
7b9e2c4fd9 fix: add test coverage for api 2025-07-12 22:28:59 +00:00
Dmitry Popov
63f13711cc Merge pull request #406 from DmitryPopov/feat/jest-test-setup-getState
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
chore: Add Jest testing for getState util
2025-07-11 14:41:20 +04:00
CI
c65b8e5ebd chore: release version v1.74.8
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-11 08:19:12 +00:00
Dmitry Popov
bfed1480ae Merge pull request #453 from wanderer-industries/unified-settings
Unified settings
2025-07-11 12:18:41 +04:00
DanSylvest
5ff902f185 fix(Map): removed comments 2025-07-09 21:01:30 +03:00
DanSylvest
8d38345c7f fix(Map): Fixed conflict 2025-07-09 20:23:18 +03:00
DanSylvest
14be9dbb8a Merge branch 'main' into unified-settings
# Conflicts:
#	assets/js/hooks/Mapper/components/map/components/LocalCounter/LocalCounter.tsx
2025-07-09 19:55:29 +03:00
CI
720c26db94 chore: release version v1.74.7
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-09 15:43:43 +00:00
Dmitry Popov
6d0b8b845d Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-07-09 17:43:17 +02:00
Dmitry Popov
b2767e000e chore: release version v1.74.5 2025-07-09 17:43:14 +02:00
CI
169f23c2ca chore: release version v1.74.6
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-09 14:33:43 +00:00
Dmitry Popov
81f70eafff chore: release version v1.74.5 2025-07-09 16:33:04 +02:00
Dmitry Popov
650170498a Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
2025-07-09 10:42:12 +02:00
CI
8b6f600989 chore: release version v1.74.5 2025-07-09 08:19:58 +00:00
Dmitry Popov
fe3617b39f Merge pull request #454 from wanderer-industries/gate-connections
fix(Map): Add background for Pochven's systems. Changed from Region n…
2025-07-09 12:19:32 +04:00
Dmitry Popov
0f466c51ba Merge pull request #456 from guarzo/guarzo/test-merge
fix: removed merge conflicts with develop
2025-07-09 12:19:02 +04:00
guarzo
a1a641bce3 tmp 2025-07-09 01:49:32 -04:00
guarzo
4764c25eb1 fmt 2025-07-09 01:49:14 -04:00
guarzo
d390455cf2 refactor: add tests and ci gates for quality 2025-07-09 01:47:24 -04:00
DanSylvest
7fb8d24d73 fix(Map): Add background for Pochven's systems. Changed from Region name to constellation name for pochven systems. Changed connection style for gates (display like common connection). Changed behaviour of connections. 2025-07-08 13:17:03 +03:00
Dmitry Popov
472dbaa68b Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
2025-07-08 10:41:16 +02:00
DanSylvest
f03448007d Merge branch 'main' into unified-settings 2025-07-07 17:22:03 +03:00
CI
c317a8bff9 chore: release version v1.74.4
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-07 13:59:56 +00:00
Dmitry Popov
618cca39a4 fix(Core): Fixed issue with update system positions 2025-07-07 15:59:23 +02:00
DanSylvest
fe7a98098f fix(Map): Unified settings. Second part: Import/Export 2025-07-07 16:57:06 +03:00
DanSylvest
df49939990 fix(Map): Unified settings. First part: add one place for storing settings 2025-07-06 18:59:40 +03:00
Dmitry Popov
f23f2776f4 chore: release version v1.72.1 2025-07-06 11:33:29 +02:00
CI
4419c86164 chore: release version v1.74.3
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-06 08:55:48 +00:00
Dmitry Popov
9848f49b49 fix(Core): Fixed issues with map subscription component 2025-07-06 10:55:21 +02:00
Dmitry Popov
679bd782a8 Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
2025-07-06 10:37:09 +02:00
Dmitry Popov
6a316e3906 Merge branch 'main' into develop 2025-07-06 10:06:46 +02:00
Dmitry Popov
c129db8474 Merge pull request #452 from guarzo/guarzo/tracking
fix: add more logging around character online and tracking
2025-07-06 12:04:15 +04:00
Dmitry Popov
10035b4c91 Merge pull request #451 from guarzo/guarzo/ssefix
fix: clean up SSE warnings
2025-07-06 11:59:23 +04:00
guarzo
5839271de7 fix: add more logging around character online and tracking 2025-07-06 00:48:54 -04:00
guarzo
47db8ef709 fix: clean up SSE warnings 2025-07-06 00:39:19 -04:00
Dmitry Popov
2656491aaa Merge pull request #449 from guarzo/guarzo/hooks
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Service Side Events and Per Map Webhook Enablement
2025-07-06 03:57:15 +04:00
Dmitry Popov
a7637c9cae Merge pull request #444 from guarzo/guarzo/killindicatorfix
fix: update killactivity color on nodes
2025-07-06 03:42:40 +04:00
guarzo
7b83ed8205 fix: update env variable usage for sse 2025-07-01 20:25:48 -04:00
guarzo
00cbc77f1d fix: sse cleanup 2025-07-01 03:12:58 -04:00
guarzo
4d75b256c4 feat: support webhook and sse 2025-07-01 02:21:05 -04:00
guarzo
5aeff7c40c Merge branch 'develop' into guarzo/killindicatorfix 2025-07-01 00:44:54 -04:00
Dmitry Popov
6a543bf644 chore: added build & deploy to test
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
2025-06-30 18:46:17 +02:00
Dmitry Popov
dfb035525d Merge branch 'main' into develop 2025-06-30 18:44:48 +02:00
CI
4c23069a0a chore: release version v1.74.2
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-30 16:37:22 +00:00
Dmitry Popov
4a1d7be44c fix(Core): Fixed map loading for not existing maps 2025-06-30 18:36:55 +02:00
Dmitry Popov
798aec1b74 Merge pull request #447 from guarzo/guarzo/errorfix
fix: remove misleading error
2025-06-29 00:35:41 +04:00
guarzo
7914d7e151 fix: remove misleading error 2025-06-28 15:58:43 -04:00
CI
26d0392da1 chore: release version v1.74.1
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-28 10:13:14 +00:00
Dmitry Popov
83b1406cce fix(Core): Mark connections between Pochven systems as known. 2025-06-28 12:12:43 +02:00
Dmitry Popov
8b579d6837 Merge branch 'main' into develop 2025-06-27 23:45:20 +02:00
Dmitry Popov
c0fd20dfff Merge pull request #441 from guarzo/guarzo/hooksocket
feat: add websocket and webhooks for events
2025-06-28 01:38:49 +04:00
guarzo
dd6b67c6e6 fix: update killactivity color on nodes 2025-06-27 09:20:15 -04:00
CI
fa83185cf5 chore: release version v1.74.0
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-25 15:51:44 +00:00
Dmitry Popov
97d5010d41 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-25 17:51:13 +02:00
Dmitry Popov
e73ad93920 feat(Core): Reverted showing linked signature ID as part of temporary names 2025-06-25 17:51:10 +02:00
CI
425af246fb chore: release version v1.73.0 2025-06-25 10:05:36 +00:00
Dmitry Popov
a2912ba0ff feat(Core): Allowed system temp names up to 12 characters. Deprecated showing linked signature ID as part of temporary name. 2025-06-25 12:05:04 +02:00
guarzo
48ff2f4413 feat: disable webhook/websocket by default 2025-06-24 20:28:42 -04:00
CI
61cd281a18 chore: release version v1.72.1
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-23 23:13:40 +00:00
Dmitry Popov
6e28134282 Merge pull request #442 from guarzo/guarzo/sigtrack
fix: issue with tracking signature activity
2025-06-24 03:13:15 +04:00
guarzo
d1377f44d2 fix: issue with tracking signature activity 2025-06-23 18:31:41 -04:00
guarzo
d261c6186b feat: add websocket and webhooks for events 2025-06-21 14:47:05 -04:00
CI
2a72a2612d chore: release version v1.72.0
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-21 11:00:06 +00:00
Dmitry Popov
66bb4f87d4 feat(Core): Added an ability to see & topup map balance and map subscription info (on public) 2025-06-21 12:59:39 +02:00
CI
977b1ad083 chore: release version v1.71.3
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-21 06:03:15 +00:00
Aleksei Chichenkov
94db18d42b Merge pull request #434 from wanderer-industries/fix-labels
fix(Map): Fix incorrect placing of labels
2025-06-21 09:02:51 +03:00
DanSylvest
7e0375108d fix(Map): Fix incorrect placing of labels 2025-06-21 08:59:32 +03:00
CI
094a5d7b62 chore: release version v1.71.2
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-20 21:58:58 +00:00
Dmitry Popov
8f947a5f04 Merge pull request #433 from guarzo/guarzo/killsfix
fix: resolve issue with kill service disconnect
2025-06-21 01:58:26 +04:00
guarzo
5580ad62f9 fix: fix issue with kill service disconnect 2025-06-20 17:09:08 -04:00
CI
c0953dc954 chore: release version v1.71.1
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-19 22:08:46 +00:00
Dmitry Popov
1df93da564 Merge pull request #430 from guarzo/guarzo/killcount
fix: only show previous hours kills in hover
2025-06-20 02:08:18 +04:00
guarzo
e2252a9d72 Merge branch 'main' into guarzo/killcount 2025-06-19 17:24:46 -04:00
guarzo
7cdba4b507 fix: update system kills widget timing 2025-06-19 17:24:25 -04:00
CI
b110d5afec chore: release version v1.71.0
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-19 06:53:06 +00:00
Dmitry Popov
6112b3e399 Merge pull request #425 from wanderer-industries/develop
Develop
2025-06-19 10:52:18 +04:00
Dmitry Popov
af0869a39b Merge pull request #426 from guarzo/guarzo/kill-develop
fix: update kills reconnect logic
2025-06-19 10:18:54 +04:00
guarzo
d44c339990 --amend 2025-06-18 21:54:48 -04:00
guarzo
0304f92ad9 fix: remove duplicate kills connections 2025-06-18 21:34:36 -04:00
Dmitry Popov
4a41d6e5d5 Merge branch 'main' into develop 2025-06-18 18:55:45 +02:00
CI
30893ca68e chore: release version v1.70.7
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-18 15:54:03 +00:00
Dmitry Popov
1edd02fa5c fix(Subscriptions): Added option to topup using ALL user balance available 2025-06-18 17:53:36 +02:00
CI
10a957ff0d chore: release version v1.70.6 2025-06-18 11:19:48 -04:00
Dmitry Popov
35b1b3619d fix: Fixed kills clinet init & map start/update logic 2025-06-18 16:21:40 +02:00
Dmitry Popov
777ebd0c41 Merge branch 'main' into develop 2025-06-18 16:14:37 +02:00
CI
9e7d8c08e1 chore: release version v1.70.6 2025-06-18 14:02:55 +00:00
Dmitry Popov
ad3f4cda09 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-18 16:02:23 +02:00
Dmitry Popov
98502cc6ae Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-18 16:02:19 +02:00
Dmitry Popov
0bac671eb0 Merge pull request #422 from guarzo/guarzo/kill_service
feat: use external service for killmail consolidation
2025-06-18 18:02:00 +04:00
guarzo
09c9a1e752 fix: avoid duplicate subs, and remove subs on inactive maps 2025-06-17 18:42:52 -04:00
guarzo
41e77e8336 feat: use external services for kill data 2025-06-17 12:29:58 -04:00
CI
a6e9fee2a0 chore: release version v1.70.5
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-17 09:36:58 +00:00
Dmitry Popov
c403a1cee5 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-17 11:36:27 +02:00
Dmitry Popov
02d25b370a fix(Core): Fixed character caching issues 2025-06-17 11:36:24 +02:00
CI
e5a3eec8a1 chore: release version v1.70.4
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-16 11:26:33 +00:00
Dmitry Popov
910352d66c Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-16 13:26:06 +02:00
Dmitry Popov
c4f02e7d55 fix(Core): Distribute tracking to minimal pool first 2025-06-16 13:26:03 +02:00
CI
6a1197ad83 chore: release version v1.70.3 2025-06-16 07:17:27 +00:00
Dmitry Popov
84c31bbb88 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-16 09:17:01 +02:00
Dmitry Popov
33f6c32306 fix(Core): Don't pause tracking for new pools 2025-06-16 09:16:58 +02:00
CI
5c71304d41 chore: release version v1.70.2
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-15 17:22:28 +00:00
Dmitry Popov
bbaf04e977 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-15 19:21:57 +02:00
Dmitry Popov
ad5b2d2eb3 fix(Core): Invalidate character copr and ally data on map server start 2025-06-15 19:21:44 +02:00
CI
3c064baf8a chore: release version v1.70.1
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-14 06:23:22 +00:00
Dmitry Popov
9e11b10d74 Merge pull request #424 from guarzo/guarzo/apiupdates 2025-06-14 10:22:58 +04:00
Guarzo
2fc45e00b4 fix: resolve api issue with custom name 2025-06-13 21:14:07 -04:00
CI
45eb08fc3a chore: release version v1.70.0
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-11 16:30:02 +00:00
Dmitry Popov
fbcfae0200 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-11 18:29:36 +02:00
Dmitry Popov
9c4ce013ec feat(Core): Fix admin page error 2025-06-11 18:29:32 +02:00
CI
5dba7c12f0 chore: release version v1.69.1 2025-06-11 11:52:01 +00:00
Dmitry Popov
db793c80c8 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-11 13:51:32 +02:00
Dmitry Popov
aa4a3f1aa9 chore: release version v1.68.6 2025-06-11 13:51:29 +02:00
CI
3424639309 chore: release version v1.69.0 2025-06-11 11:43:54 +00:00
Dmitry Popov
0f309a29ba Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-11 13:43:18 +02:00
Dmitry Popov
e13b8846b8 feat(Core): Added multiple tracking pools support 2025-06-11 13:43:13 +02:00
CI
d5ea4d6129 chore: release version v1.68.6
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-10 22:35:04 +00:00
Dmitry Popov
9d50bfedbd Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-11 00:34:36 +02:00
Dmitry Popov
b03410083c Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-11 00:34:27 +02:00
CI
a314b1e448 chore: release version v1.68.5 2025-06-10 22:34:26 +00:00
Dmitry Popov
e8a51a85c4 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-11 00:33:59 +02:00
Dmitry Popov
d4074f966c fix(Core): Fixed updating map options 2025-06-11 00:33:56 +02:00
CI
1413b41824 chore: release version v1.68.4
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-10 07:45:48 +00:00
Dmitry Popov
379c1edec3 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-10 09:45:17 +02:00
Dmitry Popov
58b5bade9e chore: release version v1.68.2 2025-06-10 09:45:14 +02:00
CI
71aee4cd3e chore: release version v1.68.3
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-09 17:58:52 +00:00
Dmitry Popov
10bab0cfa1 chore: release version v1.68.2 2025-06-09 19:55:36 +02:00
CI
698350b0f7 chore: release version v1.68.2
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-09 14:56:49 +00:00
Dmitry Popov
a97cf25031 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-09 16:56:11 +02:00
Dmitry Popov
8302d088bd fix(Core): Fixed character auth with wallet (on characters page) 2025-06-09 16:56:08 +02:00
CI
64788e73de chore: release version v1.68.1 2025-06-09 12:25:10 +00:00
Dmitry Popov
114fd471e8 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-09 14:24:37 +02:00
Dmitry Popov
b24a3120d3 fix(Core): Fixed auth from welcome page if invites disabled 2025-06-09 14:24:35 +02:00
CI
c5f93b3d0a chore: release version v1.68.0
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-09 08:20:31 +00:00
Dmitry Popov
79290e4721 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-09 10:19:56 +02:00
Dmitry Popov
984e126f23 feat(Core): Added invites store support 2025-06-09 10:19:53 +02:00
CI
cd1ad31aed chore: release version v1.67.5
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-08 19:05:08 +00:00
Dmitry Popov
1e3f6cf9e7 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 21:04:38 +02:00
Dmitry Popov
9c6ccd9a8a fix(Core): Added back ARM docker image build 2025-06-08 21:04:34 +02:00
CI
681ba21d39 chore: release version v1.67.4 2025-06-08 18:56:40 +00:00
Dmitry Popov
aef62189ee Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 20:56:07 +02:00
Dmitry Popov
09f70ac817 fix(Core): Fixed issue with system splash updates 2025-06-08 20:55:57 +02:00
CI
1eacb22143 chore: release version v1.67.3 2025-06-08 18:55:40 +00:00
Dmitry Popov
8524bad377 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 20:55:10 +02:00
Dmitry Popov
9d899243d1 fix(Core): Fixed issue with system splash updates 2025-06-08 20:54:59 +02:00
CI
9acf20a639 chore: release version v1.67.2 2025-06-08 16:57:01 +00:00
Dmitry Popov
71ef6b2e82 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 18:56:35 +02:00
Dmitry Popov
5e34d95dd2 chore: release version v1.66.25 2025-06-08 18:56:32 +02:00
CI
25a809c064 chore: release version v1.67.1 2025-06-08 16:46:28 +00:00
Dmitry Popov
f760498150 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 18:45:56 +02:00
Dmitry Popov
328301a375 chore: release version v1.66.25 2025-06-08 18:45:53 +02:00
CI
f28e7ebbbb chore: release version v1.67.0
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-08 14:27:48 +00:00
Dmitry Popov
bfa84af71e Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 16:24:32 +02:00
Dmitry Popov
42cd1ba976 feat(Core): Added support for WANDERER_CHARACTER_TRACKING_PAUSE_DISABLED env variable to pause inactive character trackers 2025-06-08 16:24:29 +02:00
CI
88cba866fd chore: release version v1.66.25 2025-06-08 11:33:36 +00:00
Dmitry Popov
af2876a84b Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 13:33:09 +02:00
Dmitry Popov
c5b15bfa78 fix(Core): Disabled kills fetching based on env settings 2025-06-08 13:33:05 +02:00
CI
85f00a63c2 chore: release version v1.66.24 2025-06-08 09:27:32 +00:00
Dmitry Popov
05f427bcd7 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 11:26:53 +02:00
Dmitry Popov
69f4c41534 chore: release version v1.66.15 2025-06-08 11:26:50 +02:00
CI
30b9239a8b chore: release version v1.66.23 2025-06-08 09:15:07 +00:00
Dmitry Popov
2061a83c59 chore: release version v1.66.15 2025-06-08 11:14:30 +02:00
Dmitry Popov
24e723de07 Merge branch 'tracking-controls' 2025-06-08 11:10:22 +02:00
CI
5a927e5ba5 chore: release version v1.66.22
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-07 16:11:48 +00:00
Dmitry Popov
10fafcf59f Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 18:11:17 +02:00
Dmitry Popov
be87591801 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 18:11:14 +02:00
CI
086d4378d3 chore: release version v1.66.21 2025-06-07 16:03:22 +00:00
Dmitry Popov
e982275905 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 18:02:37 +02:00
Dmitry Popov
77c02703e9 fix(Core): Fixed kills fetching based on env settings 2025-06-07 18:02:34 +02:00
CI
0ef27d4f95 chore: release version v1.66.20
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-07 15:41:18 +00:00
Dmitry Popov
5edc27744e Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 17:40:52 +02:00
Dmitry Popov
02ff887fee Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 17:40:48 +02:00
CI
3a30eeb59f chore: release version v1.66.19 2025-06-07 15:31:53 +00:00
Dmitry Popov
79af8fb601 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 17:31:09 +02:00
Dmitry Popov
f9f00faa0e fix(Core): Added check for offline characters timeouts 2025-06-07 17:31:05 +02:00
CI
a3c41e84e4 chore: release version v1.66.18 2025-06-07 14:55:06 +00:00
Dmitry Popov
7f21f33351 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 16:54:40 +02:00
Dmitry Popov
568f682cee chore: release version v1.66.16 2025-06-07 16:54:36 +02:00
CI
901c4c8ca4 chore: release version v1.66.17 2025-06-07 14:44:50 +00:00
Dmitry Popov
3dbba97f9c fix(Core): Increased tracking pause timeout for offline characters up to 10 hours 2025-06-07 16:44:18 +02:00
CI
3475620267 chore: release version v1.66.16 2025-06-07 13:41:56 +00:00
Dmitry Popov
8936a5e5d8 chore: release version v1.66.15 2025-06-07 15:41:22 +02:00
Dmitry Popov
719e34f9bc fix(Core): Increased tracking pause timeout for offline characters up to 10 hours 2025-06-07 15:41:06 +02:00
google-labs-jules[bot]
064a36fcbb feat: Add Jest testing for getState util 2025-05-24 20:57:40 +00:00
367 changed files with 42187 additions and 4277 deletions

View File

@@ -13,8 +13,8 @@
## list of tools (see `mix check` docs for a list of default curated tools)
tools: [
## curated tools may be disabled (e.g. the check for compilation warnings)
{:compiler, false},
## Allow compilation warnings for now (error budget: unlimited warnings)
{:compiler, "mix compile"},
## ...or have command & args adjusted (e.g. enable skip comments for sobelow)
# {:sobelow, "mix sobelow --exit --skip"},
@@ -22,10 +22,15 @@
## ...or reordered (e.g. to see output from dialyzer before others)
# {:dialyzer, order: -1},
## ...or reconfigured (e.g. disable parallel execution of ex_unit in umbrella)
## Credo with relaxed error budget: max 200 issues
{:credo, "mix credo --strict --max-issues 200"},
## Dialyzer but don't halt on exit (allow warnings)
{:dialyzer, "mix dialyzer"},
## Tests without warnings-as-errors for now
{:ex_unit, "mix test"},
{:doctor, false},
{:ex_unit, false},
{:npm_test, false},
{:sobelow, false}

View File

@@ -82,8 +82,6 @@
# You can customize the priority of any check
# Priority values are: `low, normal, high, higher`
#
{Credo.Check.Design.AliasUsage,
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
@@ -99,10 +97,9 @@
{Credo.Check.Readability.LargeNumbers, []},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
{Credo.Check.Readability.ModuleAttributeNames, []},
{Credo.Check.Readability.ModuleDoc, []},
{Credo.Check.Readability.ModuleDoc, false},
{Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
{Credo.Check.Readability.PredicateFunctionNames, []},
{Credo.Check.Readability.PreferImplicitTry, []},
@@ -121,14 +118,12 @@
#
{Credo.Check.Refactor.Apply, []},
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.MapJoin, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Refactor.Nesting, []},
{Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []},
{Credo.Check.Refactor.FilterFilter, []},
@@ -196,10 +191,19 @@
{Credo.Check.Warning.LeakyEnvironment, []},
{Credo.Check.Warning.MapGetUnsafePass, []},
{Credo.Check.Warning.MixEnv, []},
{Credo.Check.Warning.UnsafeToAtom, []}
{Credo.Check.Warning.UnsafeToAtom, []},
# {Credo.Check.Refactor.MapInto, []},
#
# Temporarily disable checks that generate too many issues
# to get under the 200 issue budget
#
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Design.AliasUsage, []},
{Credo.Check.Refactor.Nesting, []},
{Credo.Check.Refactor.CyclomaticComplexity, []}
#
# Custom checks can be created using `mix credo.gen.check`.
#

127
.credo.test.exs Normal file
View File

@@ -0,0 +1,127 @@
# Credo configuration specific to test files
# This enforces stricter quality standards for test code
%{
configs: [
%{
name: "test",
files: %{
included: ["test/"],
excluded: ["test/support/"]
},
requires: [],
strict: true,
color: true,
checks: [
# Consistency checks
{Credo.Check.Consistency.ExceptionNames, []},
{Credo.Check.Consistency.LineEndings, []},
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
{Credo.Check.Consistency.ParameterPatternMatching, []},
{Credo.Check.Consistency.SpaceAroundOperators, []},
{Credo.Check.Consistency.SpaceInParentheses, []},
{Credo.Check.Consistency.TabsOrSpaces, []},
# Design checks - stricter for tests
{Credo.Check.Design.AliasUsage, priority: :high},
# Lower threshold for tests
{Credo.Check.Design.DuplicatedCode, mass_threshold: 25},
{Credo.Check.Design.TagTODO, []},
{Credo.Check.Design.TagFIXME, []},
# Readability checks - very important for tests
{Credo.Check.Readability.AliasOrder, []},
{Credo.Check.Readability.FunctionNames, []},
{Credo.Check.Readability.LargeNumbers, []},
# Slightly longer for test descriptions
{Credo.Check.Readability.MaxLineLength, max_length: 120},
{Credo.Check.Readability.ModuleAttributeNames, []},
# Not required for test modules
{Credo.Check.Readability.ModuleDoc, false},
{Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PredicateFunctionNames, []},
{Credo.Check.Readability.PreferImplicitTry, []},
{Credo.Check.Readability.RedundantBlankLines, []},
{Credo.Check.Readability.Semicolons, []},
{Credo.Check.Readability.SpaceAfterCommas, []},
{Credo.Check.Readability.StringSigils, []},
{Credo.Check.Readability.TrailingBlankLine, []},
{Credo.Check.Readability.TrailingWhiteSpace, []},
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
{Credo.Check.Readability.VariableNames, []},
{Credo.Check.Readability.WithSingleClause, []},
# Test-specific readability checks
# Discourage single pipes in tests
{Credo.Check.Readability.SinglePipe, []},
# Specs not needed in tests
{Credo.Check.Readability.Specs, false},
{Credo.Check.Readability.StrictModuleLayout, []},
# Refactoring opportunities - important for test maintainability
# Higher limit for complex test setups
{Credo.Check.Refactor.ABCSize, max_size: 50},
{Credo.Check.Refactor.AppendSingleItem, []},
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, max_complexity: 10},
# Lower for test helpers
{Credo.Check.Refactor.FunctionArity, max_arity: 4},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MapInto, []},
{Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
# Keep tests flat
{Credo.Check.Refactor.Nesting, max_nesting: 3},
{Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []},
{Credo.Check.Refactor.FilterFilter, []},
{Credo.Check.Refactor.RejectReject, []},
{Credo.Check.Refactor.RedundantWithClauseResult, []},
# Warnings - all should be fixed
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
{Credo.Check.Warning.BoolOperationOnSameValues, []},
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.OperationOnSameValues, []},
{Credo.Check.Warning.OperationWithConstantResult, []},
{Credo.Check.Warning.RaiseInsideRescue, []},
{Credo.Check.Warning.UnusedEnumOperation, []},
{Credo.Check.Warning.UnusedFileOperation, []},
{Credo.Check.Warning.UnusedKeywordOperation, []},
{Credo.Check.Warning.UnusedListOperation, []},
{Credo.Check.Warning.UnusedPathOperation, []},
{Credo.Check.Warning.UnusedRegexOperation, []},
{Credo.Check.Warning.UnusedStringOperation, []},
{Credo.Check.Warning.UnusedTupleOperation, []},
{Credo.Check.Warning.UnsafeExec, []},
# Test-specific checks
# Important for test isolation
{Credo.Check.Warning.LeakyEnvironment, []},
# Custom checks for test patterns
{
Credo.Check.Refactor.PipeChainStart,
# Factory functions
excluded_functions: ["build", "create", "insert"],
excluded_argument_types: [:atom, :number]
}
],
# Disable these checks for test files
disabled: [
# Tests don't need module docs
{Credo.Check.Readability.ModuleDoc, []},
# Tests don't need specs
{Credo.Check.Readability.Specs, []},
# Common in test setup
{Credo.Check.Refactor.VariableRebinding, []}
]
}
]
}

39
.devcontainer/setup.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -e
echo "→ fetching & compiling deps"
mix deps.get
mix compile
# only run Ecto if the project actually has those tasks
if mix help | grep -q "ecto.create"; then
echo "→ waiting for database to be ready..."
# Wait for database to be ready
DB_HOST=${DB_HOST:-db}
timeout=60
while ! nc -z $DB_HOST 5432 2>/dev/null; do
if [ $timeout -eq 0 ]; then
echo "❌ Database connection timeout"
exit 1
fi
echo "Waiting for database... ($timeout seconds remaining)"
sleep 1
timeout=$((timeout - 1))
done
# Give the database a bit more time to fully initialize
echo "→ giving database 2 more seconds to fully initialize..."
sleep 2
echo "→ database is ready, running ecto.create && ecto.migrate"
mix ecto.create --quiet
mix ecto.migrate
fi
cd assets
echo "→ installing JS & CSS dependencies"
yarn install --frozen-lockfile
echo "→ building assets"
echo "✅ setup complete"

View File

@@ -8,4 +8,9 @@ export GIT_SHA="1111"
export WANDERER_INVITES="false"
export WANDERER_PUBLIC_API_DISABLED="false"
export WANDERER_CHARACTER_API_DISABLED="false"
export WANDERER_ZKILL_PRELOAD_DISABLED="false"
export WANDERER_KILLS_SERVICE_ENABLED="true"
export WANDERER_KILLS_BASE_URL="ws://host.docker.internal:4004"
export WANDERER_SSE_ENABLED="true"
export WANDERER_WEBHOOKS_ENABLED="true"
export WANDERER_SSE_MAX_CONNECTIONS="1000"
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"

109
.github/workflows/advanced-test.yml vendored Normal file
View File

@@ -0,0 +1,109 @@
name: Build Test
on:
push:
branches:
- develop
env:
MIX_ENV: prod
GH_TOKEN: ${{ github.token }}
REGISTRY_IMAGE: wandererltd/community-edition
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
deploy-test:
name: 🚀 Deploy to test env (fly.io)
runs-on: ubuntu-latest
if: ${{ github.base_ref == 'develop' || (github.ref == 'refs/heads/develop' && github.event_name == 'push') }}
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: 👀 Read app name
uses: SebRollen/toml-action@v1.0.0
id: app_name
with:
file: "fly.toml"
field: "app"
- name: 🚀 Deploy Test
run: flyctl deploy --remote-only --wait-timeout=300 --ha=false
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
build:
name: 🛠 Build
runs-on: ubuntu-22.04
if: ${{ (github.ref == 'refs/heads/develop') && github.event_name == 'push' }}
permissions:
checks: write
contents: write
packages: write
attestations: write
id-token: write
pull-requests: write
repository-projects: write
strategy:
matrix:
otp: ["27"]
elixir: ["1.17"]
node-version: ["18.x"]
outputs:
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash }}
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Setup Elixir
uses: erlef/setup-beam@v1
with:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}
# nix build would also work here because `todos` is the default package
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 😅 Cache deps
id: cache-deps
uses: actions/cache@v4
env:
cache-name: cache-elixir-deps
with:
path: |
deps
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-
- name: 😅 Cache compiled build
id: cache-build
uses: actions/cache@v4
env:
cache-name: cache-compiled-build
with:
path: |
_build
key: ${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-${{ hashFiles( '**/lib/**/*.{ex,eex}', '**/config/*.exs', '**/mix.exs' ) }}
restore-keys: |
${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-
${{ runner.os }}-build-
# Step: Download project dependencies. If unchanged, uses
# the cached version.
- name: 🌐 Install dependencies
run: mix deps.get --only "prod"
# Step: Compile the project treating any warnings as errors.
# Customize this step if a different behavior is desired.
- name: 🛠 Compiles without warnings
if: steps.cache-build.outputs.cache-hit != 'true'
run: mix compile

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- main
- develop
- "releases/*"
env:
MIX_ENV: prod
@@ -21,7 +22,7 @@ jobs:
build:
name: 🛠 Build
runs-on: ubuntu-22.04
if: ${{ (github.ref == 'refs/heads/main') && github.event_name == 'push' }}
if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && github.event_name == 'push' }}
permissions:
checks: write
contents: write
@@ -36,7 +37,7 @@ jobs:
elixir: ["1.17"]
node-version: ["18.x"]
outputs:
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash }}
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash || steps.set-commit-develop.outputs.commit_hash }}
steps:
- name: Prepare
run: |
@@ -89,12 +90,19 @@ jobs:
- name: Generate Changelog & Update Tag Version
id: generate-changelog
if: github.ref == 'refs/heads/main'
run: |
git config --global user.name 'CI'
git config --global user.email 'ci@users.noreply.github.com'
mix git_ops.release --force-patch --yes
git push --follow-tags
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Set commit hash for develop
id: set-commit-develop
if: github.ref == 'refs/heads/develop'
run: |
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
docker:
name: 🛠 Build Docker Images
@@ -130,12 +138,14 @@ jobs:
fetch-depth: 0
- name: Prepare Changelog
if: github.ref == 'refs/heads/main'
run: |
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
- name: Get Release Tag
id: get-latest-tag
if: github.ref == 'refs/heads/main'
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: 1.0.0
@@ -190,12 +200,14 @@ jobs:
- uses: markpatterson27/markdown-to-output@v1
id: extract-changelog
if: github.ref == 'refs/heads/main'
with:
filepath: CHANGELOG.md
- name: Get content
uses: 2428392/gh-truncate-string-action@v1.3.0
id: get-content
if: github.ref == 'refs/heads/main'
with:
stringToTruncate: |
📣 Wanderer new release available 🎉
@@ -236,9 +248,11 @@ jobs:
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
type=semver,pattern={{version}},enable=${{ github.ref == 'refs/heads/main' }}
type=semver,pattern={{major}}.{{minor}},enable=${{ github.ref == 'refs/heads/main' }}
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }},enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }}
type=raw,value=develop-{{sha}},enable=${{ github.ref == 'refs/heads/develop' }}
- name: Create manifest list and push
working-directory: /tmp/digests
@@ -278,6 +292,7 @@ jobs:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0
if: github.ref == 'refs/heads/main'
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ needs.docker.outputs.release-notes }}

View File

@@ -0,0 +1,300 @@
name: Flaky Test Detection
on:
schedule:
# Run nightly at 2 AM UTC
- cron: '0 2 * * *'
workflow_dispatch:
inputs:
test_file:
description: 'Specific test file to check (optional)'
required: false
type: string
iterations:
description: 'Number of test iterations'
required: false
default: '10'
type: string
env:
MIX_ENV: test
ELIXIR_VERSION: "1.17"
OTP_VERSION: "27"
jobs:
detect-flaky-tests:
name: 🔍 Detect Flaky Tests
runs-on: ubuntu-22.04
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: wanderer_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: ⬇️ Checkout repository
uses: actions/checkout@v4
- name: 🏗️ Setup Elixir & Erlang
uses: erlef/setup-beam@v1
with:
elixir-version: ${{ env.ELIXIR_VERSION }}
otp-version: ${{ env.OTP_VERSION }}
- name: 📦 Restore dependencies cache
uses: actions/cache@v4
id: deps-cache
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-
- name: 📦 Install dependencies
if: steps.deps-cache.outputs.cache-hit != 'true'
run: |
mix deps.get
mix deps.compile
- name: 🏗️ Compile project
run: mix compile --warnings-as-errors
- name: 🏗️ Setup test database
run: |
mix ecto.create
mix ecto.migrate
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/wanderer_test
- name: 🔍 Run flaky test detection
id: flaky-detection
run: |
# Determine test target
TEST_FILE="${{ github.event.inputs.test_file }}"
ITERATIONS="${{ github.event.inputs.iterations || '10' }}"
if [ -n "$TEST_FILE" ]; then
echo "Checking specific file: $TEST_FILE"
mix test.stability --runs $ITERATIONS --file "$TEST_FILE" --detect --report flaky_report.json
else
echo "Checking all tests"
mix test.stability --runs $ITERATIONS --detect --report flaky_report.json
fi
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/wanderer_test
continue-on-error: true
- name: 📊 Upload flaky test report
if: always()
uses: actions/upload-artifact@v4
with:
name: flaky-test-report
path: flaky_report.json
retention-days: 30
- name: 💬 Comment on flaky tests
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Read the report
let report;
try {
const reportContent = fs.readFileSync('flaky_report.json', 'utf8');
report = JSON.parse(reportContent);
} catch (error) {
console.log('No flaky test report found');
return;
}
if (!report.flaky_tests || report.flaky_tests.length === 0) {
console.log('No flaky tests detected!');
return;
}
// Create issue body
const issueBody = `## 🔍 Flaky Tests Detected
The automated flaky test detection found ${report.flaky_tests.length} potentially flaky test(s).
### Summary
- **Total test runs**: ${report.summary.total_runs}
- **Success rate**: ${(report.summary.success_rate * 100).toFixed(1)}%
- **Average duration**: ${(report.summary.avg_duration_ms / 1000).toFixed(2)}s
### Flaky Tests
| Test | Failure Rate | Details |
|------|--------------|---------|
${report.flaky_tests.map(test =>
`| ${test.test} | ${(test.failure_rate * 100).toFixed(1)}% | Failed ${test.failures}/${report.summary.total_runs} runs |`
).join('\n')}
### Recommended Actions
1. Review the identified tests for race conditions
2. Check for timing dependencies or async issues
3. Ensure proper test isolation and cleanup
4. Consider adding explicit waits or synchronization
5. Use \`async: false\` if tests share resources
---
*This issue was automatically created by the flaky test detection workflow.*
*Run time: ${new Date().toISOString()}*
`;
try {
// Check if there's already an open issue
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'flaky-test',
state: 'open'
});
if (issues.data.length > 0) {
// Update existing issue
const issue = issues.data[0];
try {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: issueBody
});
console.log(`Updated existing issue #${issue.number}`);
} catch (commentError) {
console.error('Failed to create comment:', commentError.message);
throw commentError;
}
} else {
// Create new issue
try {
const newIssue = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: '🔍 Flaky Tests Detected',
body: issueBody,
labels: ['flaky-test', 'test-quality', 'automated']
});
console.log(`Created new issue #${newIssue.data.number}`);
} catch (createError) {
console.error('Failed to create issue:', createError.message);
throw createError;
}
}
} catch (listError) {
console.error('Failed to list issues:', listError.message);
console.error('API error details:', listError.response?.data || 'No response data');
throw listError;
}
- name: 📈 Update metrics
if: always()
run: |
# Parse and store metrics for tracking
if [ -f flaky_report.json ]; then
FLAKY_COUNT=$(jq '.flaky_tests | length' flaky_report.json)
SUCCESS_RATE=$(jq '.summary.success_rate' flaky_report.json)
echo "FLAKY_TEST_COUNT=$FLAKY_COUNT" >> $GITHUB_ENV
echo "TEST_SUCCESS_RATE=$SUCCESS_RATE" >> $GITHUB_ENV
# Log metrics (could be sent to monitoring service)
echo "::notice title=Flaky Test Metrics::Found $FLAKY_COUNT flaky tests with ${SUCCESS_RATE}% success rate"
fi
analyze-test-history:
name: 📊 Analyze Test History
runs-on: ubuntu-22.04
needs: detect-flaky-tests
if: always()
steps:
- name: ⬇️ Checkout repository
uses: actions/checkout@v4
- name: 📥 Download previous reports
uses: dawidd6/action-download-artifact@v3
with:
workflow: flaky-test-detection.yml
workflow_conclusion: completed
name: flaky-test-report
path: historical-reports
if_no_artifact_found: warn
- name: 📊 Generate trend analysis
run: |
# Analyze historical trends
python3 <<'EOF'
import json
import os
from datetime import datetime
import glob
reports = []
for report_file in glob.glob('historical-reports/*/flaky_report.json'):
try:
with open(report_file, 'r') as f:
data = json.load(f)
reports.append(data)
except:
pass
if not reports:
print("No historical data found")
exit(0)
# Sort by timestamp
reports.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
# Analyze trends
print("## Test Stability Trend Analysis")
print(f"\nAnalyzed {len(reports)} historical reports")
print("\n### Flaky Test Counts Over Time")
for report in reports[:10]: # Last 10 reports
timestamp = report.get('timestamp', 'Unknown')
flaky_count = len(report.get('flaky_tests', []))
success_rate = report.get('summary', {}).get('success_rate', 0) * 100
print(f"- {timestamp[:10]}: {flaky_count} flaky tests ({success_rate:.1f}% success rate)")
# Identify persistently flaky tests
all_flaky = {}
for report in reports:
for test in report.get('flaky_tests', []):
test_name = test.get('test', '')
if test_name not in all_flaky:
all_flaky[test_name] = 0
all_flaky[test_name] += 1
if all_flaky:
print("\n### Persistently Flaky Tests")
sorted_flaky = sorted(all_flaky.items(), key=lambda x: x[1], reverse=True)
for test_name, count in sorted_flaky[:5]:
percentage = (count / len(reports)) * 100
print(f"- {test_name}: Flaky in {count}/{len(reports)} runs ({percentage:.1f}%)")
EOF
- name: 💾 Save analysis
uses: actions/upload-artifact@v4
with:
name: test-stability-analysis
path: |
flaky_report.json
historical-reports/
retention-days: 90

333
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,333 @@
name: 🧪 Test Suite
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
permissions:
contents: read
pull-requests: write
issues: write
env:
MIX_ENV: test
ELIXIR_VERSION: '1.16'
OTP_VERSION: '26'
NODE_VERSION: '18'
jobs:
test:
name: Test Suite
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: wanderer_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Elixir/OTP
uses: erlef/setup-beam@v1
with:
elixir-version: ${{ env.ELIXIR_VERSION }}
otp-version: ${{ env.OTP_VERSION }}
- name: Cache Elixir dependencies
uses: actions/cache@v3
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Install Elixir dependencies
run: |
mix deps.get
mix deps.compile
- name: Check code formatting
id: format
run: |
if mix format --check-formatted; then
echo "status=✅ Passed" >> $GITHUB_OUTPUT
echo "count=0" >> $GITHUB_OUTPUT
else
echo "status=❌ Failed" >> $GITHUB_OUTPUT
echo "count=1" >> $GITHUB_OUTPUT
fi
continue-on-error: true
- name: Compile code and capture warnings
id: compile
run: |
# Capture compilation output
output=$(mix compile 2>&1 || true)
echo "$output" > compile_output.txt
# Count warnings
warning_count=$(echo "$output" | grep -c "warning:" || echo "0")
# Check if compilation succeeded
if mix compile > /dev/null 2>&1; then
echo "status=✅ Success" >> $GITHUB_OUTPUT
else
echo "status=❌ Failed" >> $GITHUB_OUTPUT
fi
echo "warnings=$warning_count" >> $GITHUB_OUTPUT
echo "output<<EOF" >> $GITHUB_OUTPUT
echo "$output" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Setup database
run: |
mix ecto.create
mix ecto.migrate
- name: Run tests with coverage
id: tests
run: |
# Run tests with coverage
output=$(mix test --cover 2>&1 || true)
echo "$output" > test_output.txt
# Parse test results
if echo "$output" | grep -q "0 failures"; then
echo "status=✅ All Passed" >> $GITHUB_OUTPUT
test_status="success"
else
echo "status=❌ Some Failed" >> $GITHUB_OUTPUT
test_status="failed"
fi
# Extract test counts
test_line=$(echo "$output" | grep -E "[0-9]+ tests?, [0-9]+ failures?" | head -1 || echo "0 tests, 0 failures")
total_tests=$(echo "$test_line" | grep -o '[0-9]\+ tests\?' | grep -o '[0-9]\+' | head -1 || echo "0")
failures=$(echo "$test_line" | grep -o '[0-9]\+ failures\?' | grep -o '[0-9]\+' | head -1 || echo "0")
echo "total=$total_tests" >> $GITHUB_OUTPUT
echo "failures=$failures" >> $GITHUB_OUTPUT
echo "passed=$((total_tests - failures))" >> $GITHUB_OUTPUT
# Calculate success rate
if [ "$total_tests" -gt 0 ]; then
success_rate=$(echo "scale=1; ($total_tests - $failures) * 100 / $total_tests" | bc)
else
success_rate="0"
fi
echo "success_rate=$success_rate" >> $GITHUB_OUTPUT
exit_code=$?
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Generate coverage report
id: coverage
run: |
# Generate coverage report with GitHub format
output=$(mix coveralls.github 2>&1 || true)
echo "$output" > coverage_output.txt
# Extract coverage percentage
coverage=$(echo "$output" | grep -o '[0-9]\+\.[0-9]\+%' | head -1 | sed 's/%//' || echo "0")
if [ -z "$coverage" ]; then
coverage="0"
fi
echo "percentage=$coverage" >> $GITHUB_OUTPUT
# Determine status
if (( $(echo "$coverage >= 80" | bc -l) )); then
echo "status=✅ Excellent" >> $GITHUB_OUTPUT
elif (( $(echo "$coverage >= 60" | bc -l) )); then
echo "status=⚠️ Good" >> $GITHUB_OUTPUT
else
echo "status=❌ Needs Improvement" >> $GITHUB_OUTPUT
fi
continue-on-error: true
- name: Run Credo analysis
id: credo
run: |
# Run Credo and capture output
output=$(mix credo --strict --format=json 2>&1 || true)
echo "$output" > credo_output.txt
# Try to parse JSON output
if echo "$output" | jq . > /dev/null 2>&1; then
issues=$(echo "$output" | jq '.issues | length' 2>/dev/null || echo "0")
high_issues=$(echo "$output" | jq '.issues | map(select(.priority == "high")) | length' 2>/dev/null || echo "0")
normal_issues=$(echo "$output" | jq '.issues | map(select(.priority == "normal")) | length' 2>/dev/null || echo "0")
low_issues=$(echo "$output" | jq '.issues | map(select(.priority == "low")) | length' 2>/dev/null || echo "0")
else
# Fallback: try to count issues from regular output
regular_output=$(mix credo --strict 2>&1 || true)
issues=$(echo "$regular_output" | grep -c "┃" || echo "0")
high_issues="0"
normal_issues="0"
low_issues="0"
fi
echo "total_issues=$issues" >> $GITHUB_OUTPUT
echo "high_issues=$high_issues" >> $GITHUB_OUTPUT
echo "normal_issues=$normal_issues" >> $GITHUB_OUTPUT
echo "low_issues=$low_issues" >> $GITHUB_OUTPUT
# Determine status
if [ "$issues" -eq 0 ]; then
echo "status=✅ Clean" >> $GITHUB_OUTPUT
elif [ "$issues" -lt 10 ]; then
echo "status=⚠️ Minor Issues" >> $GITHUB_OUTPUT
else
echo "status=❌ Needs Attention" >> $GITHUB_OUTPUT
fi
continue-on-error: true
- name: Run Dialyzer analysis
id: dialyzer
run: |
# Ensure PLT is built
mix dialyzer --plt
# Run Dialyzer and capture output
output=$(mix dialyzer --format=github 2>&1 || true)
echo "$output" > dialyzer_output.txt
# Count warnings and errors
warnings=$(echo "$output" | grep -c "warning:" || echo "0")
errors=$(echo "$output" | grep -c "error:" || echo "0")
echo "warnings=$warnings" >> $GITHUB_OUTPUT
echo "errors=$errors" >> $GITHUB_OUTPUT
# Determine status
if [ "$errors" -eq 0 ] && [ "$warnings" -eq 0 ]; then
echo "status=✅ Clean" >> $GITHUB_OUTPUT
elif [ "$errors" -eq 0 ]; then
echo "status=⚠️ Warnings Only" >> $GITHUB_OUTPUT
else
echo "status=❌ Has Errors" >> $GITHUB_OUTPUT
fi
continue-on-error: true
- name: Create test results summary
id: summary
run: |
# Calculate overall score
format_score=${{ steps.format.outputs.count == '0' && '100' || '0' }}
compile_score=${{ steps.compile.outputs.warnings == '0' && '100' || '80' }}
test_score=${{ steps.tests.outputs.success_rate }}
coverage_score=${{ steps.coverage.outputs.percentage }}
credo_score=$(echo "scale=0; (100 - ${{ steps.credo.outputs.total_issues }} * 2)" | bc | sed 's/^-.*$/0/')
dialyzer_score=$(echo "scale=0; (100 - ${{ steps.dialyzer.outputs.warnings }} * 2 - ${{ steps.dialyzer.outputs.errors }} * 10)" | bc | sed 's/^-.*$/0/')
overall_score=$(echo "scale=1; ($format_score + $compile_score + $test_score + $coverage_score + $credo_score + $dialyzer_score) / 6" | bc)
echo "overall_score=$overall_score" >> $GITHUB_OUTPUT
# Determine overall status
if (( $(echo "$overall_score >= 90" | bc -l) )); then
echo "overall_status=🌟 Excellent" >> $GITHUB_OUTPUT
elif (( $(echo "$overall_score >= 80" | bc -l) )); then
echo "overall_status=✅ Good" >> $GITHUB_OUTPUT
elif (( $(echo "$overall_score >= 70" | bc -l) )); then
echo "overall_status=⚠️ Needs Improvement" >> $GITHUB_OUTPUT
else
echo "overall_status=❌ Poor" >> $GITHUB_OUTPUT
fi
continue-on-error: true
- name: Find existing PR comment
if: github.event_name == 'pull_request'
id: find_comment
uses: peter-evans/find-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: '## 🧪 Test Results Summary'
- name: Create or update PR comment
if: github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.find_comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
edit-mode: replace
body: |
## 🧪 Test Results Summary
**Overall Quality Score: ${{ steps.summary.outputs.overall_score }}%** ${{ steps.summary.outputs.overall_status }}
### 📊 Metrics Dashboard
| Category | Status | Count | Details |
|----------|---------|-------|---------|
| 📝 **Code Formatting** | ${{ steps.format.outputs.status }} | ${{ steps.format.outputs.count }} issues | `mix format --check-formatted` |
| 🔨 **Compilation** | ${{ steps.compile.outputs.status }} | ${{ steps.compile.outputs.warnings }} warnings | `mix compile` |
| 🧪 **Tests** | ${{ steps.tests.outputs.status }} | ${{ steps.tests.outputs.failures }}/${{ steps.tests.outputs.total }} failed | Success rate: ${{ steps.tests.outputs.success_rate }}% |
| 📊 **Coverage** | ${{ steps.coverage.outputs.status }} | ${{ steps.coverage.outputs.percentage }}% | `mix coveralls` |
| 🎯 **Credo** | ${{ steps.credo.outputs.status }} | ${{ steps.credo.outputs.total_issues }} issues | High: ${{ steps.credo.outputs.high_issues }}, Normal: ${{ steps.credo.outputs.normal_issues }}, Low: ${{ steps.credo.outputs.low_issues }} |
| 🔍 **Dialyzer** | ${{ steps.dialyzer.outputs.status }} | ${{ steps.dialyzer.outputs.errors }} errors, ${{ steps.dialyzer.outputs.warnings }} warnings | `mix dialyzer` |
### 🎯 Quality Gates
Based on the project's quality thresholds:
- **Compilation Warnings**: ${{ steps.compile.outputs.warnings }}/148 (limit: 148)
- **Credo Issues**: ${{ steps.credo.outputs.total_issues }}/87 (limit: 87)
- **Dialyzer Warnings**: ${{ steps.dialyzer.outputs.warnings }}/161 (limit: 161)
- **Test Coverage**: ${{ steps.coverage.outputs.percentage }}%/50% (minimum: 50%)
- **Test Failures**: ${{ steps.tests.outputs.failures }}/0 (limit: 0)
<details>
<summary>📈 Progress Toward Goals</summary>
Target goals for the project:
- ✨ **Zero compilation warnings** (currently: ${{ steps.compile.outputs.warnings }})
- ✨ **≤10 Credo issues** (currently: ${{ steps.credo.outputs.total_issues }})
- ✨ **Zero Dialyzer warnings** (currently: ${{ steps.dialyzer.outputs.warnings }})
- ✨ **≥85% test coverage** (currently: ${{ steps.coverage.outputs.percentage }}%)
- ✅ **Zero test failures** (currently: ${{ steps.tests.outputs.failures }})
</details>
<details>
<summary>🔧 Quick Actions</summary>
To improve code quality:
```bash
# Fix formatting issues
mix format
# View detailed Credo analysis
mix credo --strict
# Check Dialyzer warnings
mix dialyzer
# Generate detailed coverage report
mix coveralls.html
```
</details>
---
🤖 *Auto-generated by GitHub Actions* • Updated: ${{ github.event.head_commit.timestamp }}
> **Note**: This comment will be updated automatically when new commits are pushed to this PR.

6
.gitignore vendored
View File

@@ -4,7 +4,8 @@
*.iml
*.key
.repomixignore
repomix*
/.idea/
/node_modules/
/assets/node_modules/
@@ -17,6 +18,9 @@
/priv/static/*.js
/priv/static/*.css
# Dialyzer PLT files
/priv/plts/
.DS_Store
**/.DS_Store

View File

@@ -2,6 +2,500 @@
<!-- changelog -->
## [v1.75.1](https://github.com/wanderer-industries/wanderer/compare/v1.75.0...v1.75.1) (2025-07-30)
### Bug Fixes:
* unable to cancel ping from right click context menu
## [v1.75.0](https://github.com/wanderer-industries/wanderer/compare/v1.74.13...v1.75.0) (2025-07-29)
### Features:
* autoset connection size for c4->null and c13
* apiv1 and tests
* support webhook and sse
* disable webhook/websocket by default
* add websocket and webhooks for events
* Add Jest testing for getState util
### Bug Fixes:
* remove bug with lazy delete
* update broken length and remove verbose logging
* removed old documents
* removed unneeded api, and fixed data comparision bug
* ci comments
* test updates
* properly send sse events
* add test coverage for api
* add more logging around character online and tracking
* clean up SSE warnings
* update env variable usage for sse
* sse cleanup
* remove misleading error
* update killactivity color on nodes
## [v1.74.13](https://github.com/wanderer-industries/wanderer/compare/v1.74.12...v1.74.13) (2025-07-29)
### Bug Fixes:
* Core: Fixed issue with callback url
## [v1.74.12](https://github.com/wanderer-industries/wanderer/compare/v1.74.11...v1.74.12) (2025-07-22)
## [v1.74.11](https://github.com/wanderer-industries/wanderer/compare/v1.74.10...v1.74.11) (2025-07-18)
### Bug Fixes:
* Map: Fixed remove pings for removed systems
## [v1.74.10](https://github.com/wanderer-industries/wanderer/compare/v1.74.9...v1.74.10) (2025-07-15)
## [v1.74.9](https://github.com/wanderer-industries/wanderer/compare/v1.74.8...v1.74.9) (2025-07-13)
### Bug Fixes:
* Map: Trying to fix problem with fast forwarding after page are inactive some time.
## [v1.74.8](https://github.com/wanderer-industries/wanderer/compare/v1.74.7...v1.74.8) (2025-07-11)
### Bug Fixes:
* Map: removed comments
* Map: Fixed conflict
* Map: Unified settings. Second part: Import/Export
* Map: Unified settings. First part: add one place for storing settings
## [v1.74.7](https://github.com/wanderer-industries/wanderer/compare/v1.74.6...v1.74.7) (2025-07-09)
## [v1.74.6](https://github.com/wanderer-industries/wanderer/compare/v1.74.5...v1.74.6) (2025-07-09)
## [v1.74.5](https://github.com/wanderer-industries/wanderer/compare/v1.74.4...v1.74.5) (2025-07-09)
### Bug Fixes:
* Map: Add background for Pochven's systems. Changed from Region name to constellation name for pochven systems. Changed connection style for gates (display like common connection). Changed behaviour of connections.
## [v1.74.4](https://github.com/wanderer-industries/wanderer/compare/v1.74.3...v1.74.4) (2025-07-07)
### Bug Fixes:
* Core: Fixed issue with update system positions
## [v1.74.3](https://github.com/wanderer-industries/wanderer/compare/v1.74.2...v1.74.3) (2025-07-06)
### Bug Fixes:
* Core: Fixed issues with map subscription component
## [v1.74.2](https://github.com/wanderer-industries/wanderer/compare/v1.74.1...v1.74.2) (2025-06-30)
### Bug Fixes:
* Core: Fixed map loading for not existing maps
## [v1.74.1](https://github.com/wanderer-industries/wanderer/compare/v1.74.0...v1.74.1) (2025-06-28)
### Bug Fixes:
* Core: Mark connections between Pochven systems as known.
## [v1.74.0](https://github.com/wanderer-industries/wanderer/compare/v1.73.0...v1.74.0) (2025-06-25)
### Features:
* Core: Reverted showing linked signature ID as part of temporary names
## [v1.73.0](https://github.com/wanderer-industries/wanderer/compare/v1.72.1...v1.73.0) (2025-06-25)
### Features:
* Core: Allowed system temp names up to 12 characters. Deprecated showing linked signature ID as part of temporary name.
## [v1.72.1](https://github.com/wanderer-industries/wanderer/compare/v1.72.0...v1.72.1) (2025-06-23)
### Bug Fixes:
* issue with tracking signature activity
## [v1.72.0](https://github.com/wanderer-industries/wanderer/compare/v1.71.3...v1.72.0) (2025-06-21)
### Features:
* Core: Added an ability to see & topup map balance and map subscription info (on public)
## [v1.71.3](https://github.com/wanderer-industries/wanderer/compare/v1.71.2...v1.71.3) (2025-06-21)
### Bug Fixes:
* Map: Fix incorrect placing of labels
## [v1.71.2](https://github.com/wanderer-industries/wanderer/compare/v1.71.1...v1.71.2) (2025-06-20)
### Bug Fixes:
* fix issue with kill service disconnect
## [v1.71.1](https://github.com/wanderer-industries/wanderer/compare/v1.71.0...v1.71.1) (2025-06-19)
### Bug Fixes:
* update system kills widget timing
## [v1.71.0](https://github.com/wanderer-industries/wanderer/compare/v1.70.7...v1.71.0) (2025-06-19)
### Features:
* use external services for kill data
### Bug Fixes:
* remove duplicate kills connections
* Fixed kills clinet init & map start/update logic
* avoid duplicate subs, and remove subs on inactive maps
## [v1.70.7](https://github.com/wanderer-industries/wanderer/compare/v1.70.6...v1.70.7) (2025-06-18)
### Bug Fixes:
* Subscriptions: Added option to topup using ALL user balance available
## [v1.70.6](https://github.com/wanderer-industries/wanderer/compare/v1.70.5...v1.70.6) (2025-06-18)
## [v1.70.5](https://github.com/wanderer-industries/wanderer/compare/v1.70.4...v1.70.5) (2025-06-17)
### Bug Fixes:
* Core: Fixed character caching issues
## [v1.70.4](https://github.com/wanderer-industries/wanderer/compare/v1.70.3...v1.70.4) (2025-06-16)
### Bug Fixes:
* Core: Distribute tracking to minimal pool first
## [v1.70.3](https://github.com/wanderer-industries/wanderer/compare/v1.70.2...v1.70.3) (2025-06-16)
### Bug Fixes:
* Core: Don't pause tracking for new pools
## [v1.70.2](https://github.com/wanderer-industries/wanderer/compare/v1.70.1...v1.70.2) (2025-06-15)
### Bug Fixes:
* Core: Invalidate character copr and ally data on map server start
## [v1.70.1](https://github.com/wanderer-industries/wanderer/compare/v1.70.0...v1.70.1) (2025-06-14)
### Bug Fixes:
* resolve api issue with custom name
## [v1.70.0](https://github.com/wanderer-industries/wanderer/compare/v1.69.1...v1.70.0) (2025-06-11)
### Features:
* Core: Fix admin page error
## [v1.69.1](https://github.com/wanderer-industries/wanderer/compare/v1.69.0...v1.69.1) (2025-06-11)
## [v1.69.0](https://github.com/wanderer-industries/wanderer/compare/v1.68.6...v1.69.0) (2025-06-11)
### Features:
* Core: Added multiple tracking pools support
## [v1.68.6](https://github.com/wanderer-industries/wanderer/compare/v1.68.5...v1.68.6) (2025-06-10)
## [v1.68.5](https://github.com/wanderer-industries/wanderer/compare/v1.68.4...v1.68.5) (2025-06-10)
### Bug Fixes:
* Core: Fixed updating map options
## [v1.68.4](https://github.com/wanderer-industries/wanderer/compare/v1.68.3...v1.68.4) (2025-06-10)
## [v1.68.3](https://github.com/wanderer-industries/wanderer/compare/v1.68.2...v1.68.3) (2025-06-09)
## [v1.68.2](https://github.com/wanderer-industries/wanderer/compare/v1.68.1...v1.68.2) (2025-06-09)
### Bug Fixes:
* Core: Fixed character auth with wallet (on characters page)
## [v1.68.1](https://github.com/wanderer-industries/wanderer/compare/v1.68.0...v1.68.1) (2025-06-09)
### Bug Fixes:
* Core: Fixed auth from welcome page if invites disabled
## [v1.68.0](https://github.com/wanderer-industries/wanderer/compare/v1.67.5...v1.68.0) (2025-06-09)
### Features:
* Core: Added invites store support
## [v1.67.5](https://github.com/wanderer-industries/wanderer/compare/v1.67.4...v1.67.5) (2025-06-08)
### Bug Fixes:
* Core: Added back ARM docker image build
## [v1.67.4](https://github.com/wanderer-industries/wanderer/compare/v1.67.3...v1.67.4) (2025-06-08)
### Bug Fixes:
* Core: Fixed issue with system splash updates
## [v1.67.3](https://github.com/wanderer-industries/wanderer/compare/v1.67.2...v1.67.3) (2025-06-08)
### Bug Fixes:
* Core: Fixed issue with system splash updates
## [v1.67.2](https://github.com/wanderer-industries/wanderer/compare/v1.67.1...v1.67.2) (2025-06-08)
## [v1.67.1](https://github.com/wanderer-industries/wanderer/compare/v1.67.0...v1.67.1) (2025-06-08)
## [v1.67.0](https://github.com/wanderer-industries/wanderer/compare/v1.66.25...v1.67.0) (2025-06-08)
### Features:
* Core: Added support for WANDERER_CHARACTER_TRACKING_PAUSE_DISABLED env variable to pause inactive character trackers
## [v1.66.25](https://github.com/wanderer-industries/wanderer/compare/v1.66.24...v1.66.25) (2025-06-08)
### Bug Fixes:
* Core: Disabled kills fetching based on env settings
## [v1.66.24](https://github.com/wanderer-industries/wanderer/compare/v1.66.23...v1.66.24) (2025-06-08)
## [v1.66.23](https://github.com/wanderer-industries/wanderer/compare/v1.66.22...v1.66.23) (2025-06-08)
## [v1.66.22](https://github.com/wanderer-industries/wanderer/compare/v1.66.21...v1.66.22) (2025-06-07)
## [v1.66.21](https://github.com/wanderer-industries/wanderer/compare/v1.66.20...v1.66.21) (2025-06-07)
### Bug Fixes:
* Core: Fixed kills fetching based on env settings
## [v1.66.20](https://github.com/wanderer-industries/wanderer/compare/v1.66.19...v1.66.20) (2025-06-07)
## [v1.66.19](https://github.com/wanderer-industries/wanderer/compare/v1.66.18...v1.66.19) (2025-06-07)
### Bug Fixes:
* Core: Added check for offline characters timeouts
## [v1.66.18](https://github.com/wanderer-industries/wanderer/compare/v1.66.17...v1.66.18) (2025-06-07)
## [v1.66.17](https://github.com/wanderer-industries/wanderer/compare/v1.66.16...v1.66.17) (2025-06-07)
### Bug Fixes:
* Core: Increased tracking pause timeout for offline characters up to 10 hours
## [v1.66.16](https://github.com/wanderer-industries/wanderer/compare/v1.66.15...v1.66.16) (2025-06-07)
### Bug Fixes:
* Core: Increased tracking pause timeout for offline characters up to 10 hours
## [v1.66.15](https://github.com/wanderer-industries/wanderer/compare/v1.66.14...v1.66.15) (2025-06-07)

14
assets/jest.config.js Normal file
View File

@@ -0,0 +1,14 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>'],
moduleDirectories: ['node_modules', 'js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/js/$1',
'\.scss$': 'identity-obj-proxy', // Mock SCSS files
},
transform: {
'^.+\.(ts|tsx)$': 'ts-jest',
'^.+\.(js|jsx)$': 'babel-jest', // Add babel-jest for JS/JSX files if needed
},
};

View File

@@ -212,3 +212,75 @@
.p-inputtext:enabled:hover {
border-color: #335c7e;
}
// --------------- TOAST
.p-toast .p-toast-message {
background-color: #1a1a1a;
color: #e0e0e0;
border-left: 4px solid transparent;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
}
.p-toast .p-toast-message .p-toast-summary {
color: #ffffff;
font-weight: 600;
}
.p-toast .p-toast-message .p-toast-detail {
color: #c0c0c0;
font-size: 13px;
}
.p-toast .p-toast-icon-close {
color: #ffaa00;
transition: background 0.2s;
}
.p-toast .p-toast-icon-close:hover {
background: #333;
color: #fff;
}
.p-toast-message-success {
border-left-color: #f1c40f;
}
.p-toast-message-error {
border-left-color: #e74c3c;
}
.p-toast-message-info {
border-left-color: #3498db;
}
.p-toast-message-warn {
border-left-color: #e67e22;
}
.p-toast-message-success .p-toast-message-icon {
color: #f1c40f;
}
.p-toast-message-error .p-toast-message-icon {
color: #e74c3c;
}
.p-toast-message-info .p-toast-message-icon {
color: #3498db;
}
.p-toast-message-warn .p-toast-message-icon {
color: #e67e22;
}
.p-toast-message-success .p-toast-message-content {
border-left-color: #f1c40f;
}
.p-toast-message-error .p-toast-message-content {
border-left-color: #e74c3c;
}
.p-toast-message-info .p-toast-message-content {
border-left-color: #3498db;
}
.p-toast-message-warn .p-toast-message-content {
border-left-color: #e67e22;
}

View File

@@ -64,9 +64,9 @@ body .p-dialog {
}
.p-dialog-footer {
padding: 1rem;
border-top: 1px solid #ddd;
background: #f4f4f4;
padding: .75rem 1rem;
border-top: none !important;
//background: #f4f4f4;
}
.p-dialog-header-close {

View File

@@ -57,8 +57,8 @@ export const Characters = ({ data }: CharactersProps) => {
<>
<span
className={clsx(
'absolute top-[2px] left-[2px] w-[9px] h-[9px]',
'text-yellow-500 text-[9px] rounded-[1px] z-10 hover:hidden',
'absolute flex flex-col p-[2px] top-[0px] left-[0px] w-[35px] h-[35px]',
'text-yellow-500 text-[9px] z-10 bg-gray-800/40',
'pi',
PrimeIcons.PAUSE,
)}

View File

@@ -0,0 +1 @@
export * from './parseMapUserSettings.ts';

View File

@@ -0,0 +1,67 @@
import { MapUserSettings, SettingsWithVersion } from '@/hooks/Mapper/mapRootProvider/types.ts';
const REQUIRED_KEYS = [
'widgets',
'interface',
'onTheMap',
'routes',
'localWidget',
'signaturesWidget',
'killsWidget',
] as const;
type RequiredKeys = (typeof REQUIRED_KEYS)[number];
/** Custom error for any parsing / validation issue */
export class MapUserSettingsParseError extends Error {
constructor(msg: string) {
super(`MapUserSettings parse error: ${msg}`);
}
}
const isNumber = (v: unknown): v is number => typeof v === 'number' && !Number.isNaN(v);
/** Minimal check that an object matches SettingsWithVersion<*> */
const isSettingsWithVersion = (v: unknown): v is SettingsWithVersion<unknown> =>
typeof v === 'object' && v !== null && isNumber((v as any).version) && 'settings' in (v as any);
/** Ensure every required key is present */
const hasAllRequiredKeys = (v: unknown): v is Record<RequiredKeys, unknown> =>
typeof v === 'object' && v !== null && REQUIRED_KEYS.every(k => k in v);
/* ------------------------------ Main parser ------------------------------- */
/**
* Parses and validates a JSON string as `MapUserSettings`.
*
* @throws `MapUserSettingsParseError` если строка не JSON или нарушена структура
*/
export const parseMapUserSettings = (json: unknown): MapUserSettings => {
if (typeof json !== 'string') throw new MapUserSettingsParseError('Input must be a JSON string');
let data: unknown;
try {
data = JSON.parse(json);
} catch (e) {
throw new MapUserSettingsParseError(`Invalid JSON: ${(e as Error).message}`);
}
if (!hasAllRequiredKeys(data)) {
const missing = REQUIRED_KEYS.filter(k => !(k in (data as any)));
throw new MapUserSettingsParseError(`Missing top-level field(s): ${missing.join(', ')}`);
}
for (const key of REQUIRED_KEYS) {
if (!isSettingsWithVersion((data as any)[key])) {
throw new MapUserSettingsParseError(`"${key}" must match SettingsWithVersion<T>`);
}
}
// Everything passes, so cast is safe
return data as MapUserSettings;
};
/* ------------------------------ Usage example ----------------------------- */
// const raw = fetchFromServer(); // string
// const settings = parseMapUserSettings(raw);

View File

@@ -98,6 +98,7 @@ interface MapCompProps {
theme?: string;
pings: PingData[];
minimapPlacement?: PanelPosition;
localShowShipName?: boolean;
}
const MapComp = ({
@@ -117,6 +118,7 @@ const MapComp = ({
onAddSystem,
pings,
minimapPlacement = 'bottom-right',
localShowShipName = false,
}: MapCompProps) => {
const { getNodes } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
@@ -212,8 +214,9 @@ const MapComp = ({
showKSpaceBG: showKSpaceBG,
isThickConnections: isThickConnections,
pings,
localShowShipName,
}));
}, [showKSpaceBG, isThickConnections, pings, update]);
}, [showKSpaceBG, isThickConnections, pings, update, localShowShipName]);
return (
<>

View File

@@ -10,6 +10,7 @@ export type MapData = MapUnionTypes & {
showKSpaceBG: boolean;
isThickConnections: boolean;
linkedSigEveId: string;
localShowShipName: boolean;
};
interface MapProviderProps {
@@ -42,6 +43,7 @@ const INITIAL_DATA: MapData = {
followingCharacterEveId: null,
userHubs: [],
pings: [],
localShowShipName: false,
};
export interface MapContextProps {

View File

@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { useKillsCounter } from '../../hooks/useKillsCounter';
import { useKillsCounter } from '../../hooks/useKillsCounter.ts';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
import {
KILLS_ROW_HEIGHT,
SystemKillsList,
@@ -49,7 +49,7 @@ export const KillsCounter = ({
content={
<div className="overflow-hidden flex w-[450px] flex-col" style={{ height: `${tooltipHeight}px` }}>
<div className="flex-1 h-full">
<SystemKillsList kills={limitedKills} onlyOneSystem />
<SystemKillsList kills={limitedKills} onlyOneSystem timeRange={1} />
</div>
</div>
}

View File

@@ -0,0 +1 @@
export * from './KillsCounter.tsx';

View File

@@ -3,11 +3,11 @@ import clsx from 'clsx';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components';
import { useLocalCharactersItemTemplate } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters';
import { useLocalCharacterWidgetSettings } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalWidgetSettings';
import classes from './SolarSystemLocalCounter.module.scss';
import { useTheme } from '@/hooks/Mapper/hooks/useTheme.ts';
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider/types.ts';
import classes from './LocalCounter.module.scss';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useLocalCharactersItemTemplate } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters.tsx';
interface LocalCounterProps {
localCounterCharacters: Array<CharItemProps>;
@@ -16,8 +16,10 @@ interface LocalCounterProps {
}
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
const [settings] = useLocalCharacterWidgetSettings();
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
const {
data: { localShowShipName },
} = useMapState();
const itemTemplate = useLocalCharactersItemTemplate(localShowShipName);
const theme = useTheme();
const pilotTooltipContent = useMemo(() => {

View File

@@ -0,0 +1 @@
export * from './LocalCounter';

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import classes from './SolarSystemEdge.module.scss';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, getSmoothStepPath, Position, useStore } from 'reactflow';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, Position, useStore } from 'reactflow';
import { getEdgeParams } from '@/hooks/Mapper/components/map/utils.ts';
import clsx from 'clsx';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
@@ -51,11 +51,11 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
const [hovered, setHovered] = useState(false);
const [path, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos] = useMemo(() => {
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode!, targetNode!);
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
const method = isWormhole ? getBezierPath : getSmoothStepPath;
const method = isWormhole ? getBezierPath : getBezierPath;
const [edgePath, labelX, labelY] = method({
sourceX: sx - offset.x,

View File

@@ -40,6 +40,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
z-index: 3;
overflow: hidden;
&.Pochven,
&.Mataria,
&.Amarria,
&.Gallente,
@@ -95,6 +96,15 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
}
}
&.Pochven {
&::after {
opacity: 0.8;
background-image: url('/images/pochven.webp');
background-position-x: 0;
background-position-y: -13px;
}
}
&.selected {
border-color: $pastel-pink;
box-shadow: 0 0 10px #9a1af1c2;

View File

@@ -12,17 +12,19 @@ import {
} from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { Tag } from 'primereact/tag';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
// let render = 0;
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(
nodeVars.solarSystemId,
);
// console.log('JOipP', `render ${nodeVars.id}`, render++);
@@ -38,17 +40,17 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</div>
)}
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && (
{localKillsCount != null && localKillsCount > 0 && nodeVars.solarSystemId && localKillsActivityType && (
<KillsCounter
killsCount={localKillsCount}
systemId={nodeVars.solarSystemId}
size={TooltipSize.lg}
killsActivityType={nodeVars.killsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
killsActivityType={localKillsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[localKillsActivityType])}
>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
<span className={clsx(classes.text)}>{localKillsCount}</span>
</div>
</KillsCounter>
)}

View File

@@ -12,16 +12,16 @@ import {
} from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
// let render = 0;
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(nodeVars.solarSystemId);
// console.log('JOipP', `render ${nodeVars.id}`, render++);
@@ -37,17 +37,17 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
</div>
)}
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && (
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && localKillsActivityType && (
<KillsCounter
killsCount={localKillsCount}
systemId={nodeVars.solarSystemId}
size={TooltipSize.lg}
killsActivityType={nodeVars.killsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
killsActivityType={localKillsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[localKillsActivityType])}
>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
<span className={clsx(classes.text)}>{localKillsCount}</span>
</div>
</KillsCounter>
)}

View File

@@ -22,6 +22,7 @@ export function useKillsCounter({ realSystemId }: UseKillsCounterProps) {
systemId: realSystemId,
outCommand,
showAllVisible: false,
sinceHours: 1,
});
const filteredKills = useMemo(() => {

View File

@@ -1,6 +1,7 @@
import { useEffect, useState, useCallback } from 'react';
import { useEffect, useState, useCallback, useMemo } from 'react';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
interface Kill {
solar_system_id: number | string;
@@ -9,32 +10,78 @@ interface Kill {
interface MapEvent {
name: Commands;
data?: any;
data?: unknown;
payload?: Kill[];
}
export function useNodeKillsCount(systemId: number | string, initialKillsCount: number | null): number | null {
function getActivityType(count: number): string {
if (count <= 5) return 'activityNormal';
if (count <= 30) return 'activityWarn';
return 'activityDanger';
}
export function useNodeKillsCount(systemId: number | string, initialKillsCount: number | null = null): { killsCount: number | null; killsActivityType: string | null } {
const [killsCount, setKillsCount] = useState<number | null>(initialKillsCount);
const { data: mapData } = useMapRootState();
const { detailedKills = {} } = mapData;
// Calculate 1-hour kill count from detailed kills
const oneHourKillCount = useMemo(() => {
const systemKills = detailedKills[systemId] || [];
// If we have detailed kills data (even if empty), use it for counting
if (Object.prototype.hasOwnProperty.call(detailedKills, systemId)) {
const oneHourAgo = Date.now() - 60 * 60 * 1000; // 1 hour in milliseconds
const recentKills = systemKills.filter(kill => {
if (!kill.kill_time) return false;
const killTime = new Date(kill.kill_time).getTime();
if (isNaN(killTime)) return false;
return killTime >= oneHourAgo;
});
return recentKills.length; // Return 0 if no recent kills, not null
}
// Return null only if we don't have detailed kills data for this system
return null;
}, [detailedKills, systemId]);
useEffect(() => {
setKillsCount(initialKillsCount);
}, [initialKillsCount]);
// Always prefer the calculated 1-hour count over initial count
// This ensures we properly expire old kills
if (oneHourKillCount !== null) {
setKillsCount(oneHourKillCount);
} else if (detailedKills[systemId] && detailedKills[systemId].length === 0) {
// If we have detailed kills data but it's empty, set to 0
setKillsCount(0);
} else {
// Only fall back to initial count if we have no detailed kills data at all
setKillsCount(initialKillsCount);
}
}, [oneHourKillCount, initialKillsCount, detailedKills, systemId]);
const handleEvent = useCallback(
(event: MapEvent): boolean => {
if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) {
const killForSystem = event.payload.find(kill => kill.solar_system_id.toString() === systemId.toString());
if (killForSystem && typeof killForSystem.kills === 'number') {
setKillsCount(killForSystem.kills);
// Only update if we don't have detailed kills data
if (!detailedKills[systemId] || detailedKills[systemId].length === 0) {
setKillsCount(killForSystem.kills);
}
}
return true;
}
return false;
},
[systemId],
[systemId, detailedKills],
);
useMapEventListener(handleEvent);
return killsCount;
const killsActivityType = useMemo(() => {
return killsCount !== null && killsCount > 0 ? getActivityType(killsCount) : null;
}, [killsCount]);
return { killsCount, killsActivityType };
}

View File

@@ -5,7 +5,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick';
import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { Regions, REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
@@ -15,20 +15,12 @@ import { useSystemName } from './useSystemName';
import { LabelInfo, useLabelsInfo } from './useLabelsInfo';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
function getActivityType(count: number): string {
if (count <= 5) return 'activityNormal';
if (count <= 30) return 'activityWarn';
return 'activityDanger';
}
export interface SolarSystemNodeVars {
id: string;
selected: boolean;
visible: boolean;
isWormhole: boolean;
classTitleColor: string | null;
killsCount: number | null;
killsActivityType: string | null;
hasUserCharacters: boolean;
showHandlers: boolean;
regionClass: string | null;
@@ -65,6 +57,7 @@ const SpaceToClass: Record<string, string> = {
[Spaces.Matar]: 'Mataria',
[Spaces.Amarr]: 'Amarria',
[Spaces.Gallente]: 'Gallente',
[Spaces.Pochven]: 'Pochven',
};
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
@@ -112,6 +105,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
region_id,
is_shattered,
solar_system_name,
constellation_name,
} = systemStaticInfo;
const { isShowUnsplashedSignatures } = interfaceSettings;
@@ -124,7 +118,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
characters,
wormholesData,
hubs,
kills,
userCharacters,
isConnecting,
hoverNodeId,
@@ -161,9 +154,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
isShowLinkedSigId,
});
const killsCount = useMemo(() => kills[parseInt(solar_system_id)] ?? null, [kills, solar_system_id]);
const killsActivityType = killsCount ? getActivityType(killsCount) : null;
const hasUserCharacters = useMemo(
() => charactersInSystem.some(x => userCharacters.includes(x.eve_id)),
[charactersInSystem, userCharacters],
@@ -195,18 +185,24 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]);
const isRally = useMemo(
() => pings.find(x => x.solar_system_id === solar_system_id && x.type === PingType.Rally),
() => !!pings.find(x => x.solar_system_id === solar_system_id && x.type === PingType.Rally),
[pings, solar_system_id],
);
const regionName = useMemo(() => {
if (region_id === Regions.Pochven) {
return constellation_name;
}
return region_name;
}, [constellation_name, region_id, region_name]);
const nodeVars: SolarSystemNodeVars = {
id,
selected,
visible,
isWormhole,
classTitleColor,
killsCount,
killsActivityType,
hasUserCharacters,
userCharacters,
showHandlers,
@@ -233,7 +229,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
isThickConnections,
classTitle: class_title,
temporaryName: computedTemporaryName,
regionName: region_name,
regionName,
solarSystemName: solar_system_name,
isRally,
};

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
import { useMemo } from 'react';
interface UseSystemNameParams {
isTempSystemNameEnabled: boolean;
@@ -26,7 +26,7 @@ export const useSystemName = ({
}
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
return temporary_name ? `${linkedSigPrefix}${temporary_name}` : `${linkedSigPrefix}${solar_system_name}`;
return temporary_name ? `${linkedSigPrefix}:${temporary_name}` : `${linkedSigPrefix}:${solar_system_name}`;
}
return temporary_name ?? '';

View File

@@ -1,37 +1,48 @@
import { Position, internalsSymbol } from 'reactflow';
import { Position, internalsSymbol, Node } from 'reactflow';
// returns the position (top,right,bottom or right) passed node compared to
function getParams(nodeA, nodeB) {
type Coords = [number, number];
type CoordsWithPosition = [number, number, Position];
function segmentsIntersect(a1: number, a2: number, b1: number, b2: number): boolean {
const [minA, maxA] = a1 < a2 ? [a1, a2] : [a2, a1];
const [minB, maxB] = b1 < b2 ? [b1, b2] : [b2, b1];
return maxA >= minB && maxB >= minA;
}
function getParams(nodeA: Node, nodeB: Node): CoordsWithPosition {
const centerA = getNodeCenter(nodeA);
const centerB = getNodeCenter(nodeB);
const horizontalDiff = Math.abs(centerA.x - centerB.x);
const verticalDiff = Math.abs(centerA.y - centerB.y);
let position: Position;
// when the horizontal difference between the nodes is bigger, we use Position.Left or Position.Right for the handle
if (horizontalDiff > verticalDiff) {
position = centerA.x > centerB.x ? Position.Left : Position.Right;
} else {
// here the vertical difference between the nodes is bigger, so we use Position.Top or Position.Bottom for the handle
if (
segmentsIntersect(
nodeA.positionAbsolute!.x - 10,
nodeA.positionAbsolute!.x - 10 + nodeA.width! + 20,
nodeB.positionAbsolute!.x,
nodeB.positionAbsolute!.x + nodeB.width!,
)
) {
position = centerA.y > centerB.y ? Position.Top : Position.Bottom;
} else {
position = centerA.x > centerB.x ? Position.Left : Position.Right;
}
const [x, y] = getHandleCoordsByPosition(nodeA, position);
return [x, y, position];
}
function getHandleCoordsByPosition(node, handlePosition) {
// all handles are from type source, that's why we use handleBounds.source here
const handle = node[internalsSymbol].handleBounds.source.find(h => h.position === handlePosition);
function getHandleCoordsByPosition(node: Node, handlePosition: Position): Coords {
const handle = node[internalsSymbol]!.handleBounds!.source!.find(h => h.position === handlePosition);
if (!handle) {
throw new Error(`Handle with position ${handlePosition} not found on node ${node.id}`);
}
let offsetX = handle.width / 2;
let offsetY = handle.height / 2;
// this is a tiny detail to make the markerEnd of an edge visible.
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
switch (handlePosition) {
case Position.Left:
offsetX = 0;
@@ -47,21 +58,20 @@ function getHandleCoordsByPosition(node, handlePosition) {
break;
}
const x = node.positionAbsolute.x + handle.x + offsetX;
const y = node.positionAbsolute.y + handle.y + offsetY;
const x = node.positionAbsolute!.x + handle.x + offsetX;
const y = node.positionAbsolute!.y + handle.y + offsetY;
return [x, y];
}
function getNodeCenter(node) {
function getNodeCenter(node: Node): { x: number; y: number } {
return {
x: node.positionAbsolute.x + node.width / 2,
y: node.positionAbsolute.y + node.height / 2,
x: node.positionAbsolute!.x + node.width! / 2,
y: node.positionAbsolute!.y + node.height! / 2,
};
}
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
export function getEdgeParams(source, target) {
export function getEdgeParams(source: Node, target: Node) {
const [sx, sy, sourcePos] = getParams(source, target);
const [tx, ty, targetPos] = getParams(target, source);

View File

@@ -1,9 +1,4 @@
import { Button } from 'primereact/button';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import clsx from 'clsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { Commands, OutCommand, PingType } from '@/hooks/Mapper/types';
import { PingRoute } from '@/hooks/Mapper/components/mapInterface/components/PingsInterface/PingRoute.tsx';
import {
CharacterCardById,
SystemView,
@@ -12,12 +7,17 @@ import {
WdImgButton,
WdImgButtonTooltip,
} from '@/hooks/Mapper/components/ui-kit';
import useRefState from 'react-usestateref';
import { PrimeIcons } from 'primereact/api';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { PingRoute } from '@/hooks/Mapper/components/mapInterface/components/PingsInterface/PingRoute.tsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { Commands, OutCommand, PingType } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { Button } from 'primereact/button';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { Toast } from 'primereact/toast';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import useRefState from 'react-usestateref';
const PING_PLACEMENT_MAP = {
[PingsPlacement.rightTop]: 'top-right',
@@ -119,7 +119,7 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
await outCommand({
type: OutCommand.cancelPing,
data: { type: ping.type, solar_system_id: ping.solar_system_id },
data: { type: ping.type, id: ping.id },
});
}, [outCommand, ping]);

View File

@@ -7,10 +7,6 @@ import {
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
} from '@/hooks/Mapper/components/map/constants.ts';
import {
SETTINGS_KEYS,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
@@ -18,6 +14,7 @@ import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureC
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName;

View File

@@ -206,7 +206,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
aria-describedby="temporaryName"
autoComplete="off"
value={temporaryName}
maxLength={10}
maxLength={12}
onChange={e => setTemporaryName(e.target.value)}
/>
</IconField>

View File

@@ -6,7 +6,6 @@ import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootP
import { UserPermission } from '@/hooks/Mapper/types/permissions';
import { LocalCharactersList } from './components/LocalCharactersList';
import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters';
import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings';
import { LocalCharactersHeader } from './components/LocalCharactersHeader';
import classes from './LocalCharacters.module.scss';
import clsx from 'clsx';
@@ -14,9 +13,9 @@ import clsx from 'clsx';
export const LocalCharacters = () => {
const {
data: { characters, userCharacters, selectedSystems },
storedSettings: { settingsLocal, settingsLocalUpdate },
} = useMapRootState();
const [settings, setSettings] = useLocalCharacterWidgetSettings();
const [systemId] = selectedSystems;
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
@@ -31,12 +30,12 @@ export const LocalCharacters = () => {
.map(x => ({
...x,
isOwn: userCharacters.includes(x.eve_id),
compact: settings.compact,
showShipName: settings.showShipName,
compact: settingsLocal.compact,
showShipName: settingsLocal.showShipName,
}))
.sort(sortCharacters);
if (!showOffline || !settings.showOffline) {
if (!showOffline || !settingsLocal.showOffline) {
return filtered.filter(c => c.online);
}
return filtered;
@@ -44,9 +43,9 @@ export const LocalCharacters = () => {
characters,
systemId,
userCharacters,
settings.compact,
settings.showOffline,
settings.showShipName,
settingsLocal.compact,
settingsLocal.showOffline,
settingsLocal.showShipName,
showOffline,
]);
@@ -54,7 +53,7 @@ export const LocalCharacters = () => {
const isNotSelectedSystem = selectedSystems.length !== 1;
const showList = sorted.length > 0 && selectedSystems.length === 1;
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
const itemTemplate = useLocalCharactersItemTemplate(settingsLocal.showShipName);
return (
<Widget
@@ -63,8 +62,8 @@ export const LocalCharacters = () => {
sortedCount={sorted.length}
showList={showList}
showOffline={showOffline}
settings={settings}
setSettings={setSettings}
settings={settingsLocal}
setSettings={settingsLocalUpdate}
/>
}
>
@@ -81,7 +80,7 @@ export const LocalCharacters = () => {
{showList && (
<LocalCharactersList
items={sorted}
itemSize={settings.compact ? 26 : 41}
itemSize={settingsLocal.compact ? 26 : 41}
itemTemplate={itemTemplate}
containerClassName={clsx(
'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none',

View File

@@ -1,21 +0,0 @@
import useLocalStorageState from 'use-local-storage-state';
export interface LocalCharacterWidgetSettings {
compact: boolean;
showOffline: boolean;
version: number;
showShipName: boolean;
}
export const LOCAL_CHARACTER_WIDGET_DEFAULT: LocalCharacterWidgetSettings = {
compact: true,
showOffline: false,
version: 0,
showShipName: false,
};
export function useLocalCharacterWidgetSettings() {
return useLocalStorageState<LocalCharacterWidgetSettings>('kills:widget:settings', {
defaultValue: LOCAL_CHARACTER_WIDGET_DEFAULT,
});
}

View File

@@ -8,8 +8,8 @@ import {
Setting,
SettingsTypes,
SIGNATURE_SETTINGS,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
interface SystemSignatureSettingsDialogProps {
settings: SignatureSettingsType;

View File

@@ -1,21 +1,14 @@
import { useCallback, useState, useEffect, useRef, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemSignaturesContent } from './SystemSignaturesContent';
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { SystemSignaturesHeader } from './SystemSignatureHeader';
import useLocalStorageState from 'use-local-storage-state';
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
import {
SETTINGS_KEYS,
SETTINGS_VALUES,
SIGNATURE_SETTING_STORE_KEY,
SIGNATURE_WINDOW_ID,
SignatureSettingsType,
getDeletionTimeoutMs,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
/**
* Custom hook for managing pending signature deletions and undo countdown.
@@ -69,8 +62,11 @@ function useSignatureUndo(
// determine timeout from settings
const timeoutMs = getDeletionTimeoutMs(settings);
// Ensure a minimum of 1 second for immediate deletion so the UI shows
const effectiveTimeoutMs = timeoutMs === 0 ? 1000 : timeoutMs;
setCountdown(Math.ceil(timeoutMs / 1000));
setCountdown(Math.ceil(effectiveTimeoutMs / 1000));
// start new interval
intervalRef.current = window.setInterval(() => {
@@ -126,20 +122,14 @@ export const SystemSignatures = () => {
const {
data: { selectedSystems },
outCommand,
storedSettings: { settingsSignatures, settingsSignaturesUpdate },
} = useMapRootState();
const [currentSettings, setCurrentSettings] = useLocalStorageState<SignatureSettingsType>(
SIGNATURE_SETTING_STORE_KEY,
{
defaultValue: SETTINGS_VALUES,
},
);
const [systemId] = selectedSystems;
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
const { pendingIds, countdown, deletedSignatures, addDeleted, handleUndo } = useSignatureUndo(
systemId,
currentSettings,
settingsSignatures,
outCommand,
);
@@ -157,20 +147,20 @@ export const SystemSignatures = () => {
const handleSettingsSave = useCallback(
(newSettings: SignatureSettingsType) => {
setCurrentSettings(newSettings);
settingsSignaturesUpdate(newSettings);
setVisible(false);
},
[setCurrentSettings],
[settingsSignaturesUpdate],
);
const handleLazyDeleteToggle = useCallback(
(value: boolean) => {
setCurrentSettings(prev => ({
settingsSignaturesUpdate(prev => ({
...prev,
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value,
}));
},
[setCurrentSettings],
[settingsSignaturesUpdate],
);
const openSettings = useCallback(() => setVisible(true), []);
@@ -180,7 +170,7 @@ export const SystemSignatures = () => {
label={
<SystemSignaturesHeader
sigCount={sigCount}
lazyDeleteValue={currentSettings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
lazyDeleteValue={settingsSignatures[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
pendingCount={pendingIds.size}
undoCountdown={countdown}
onLazyDeleteChange={handleLazyDeleteToggle}
@@ -197,7 +187,7 @@ export const SystemSignatures = () => {
) : (
<SystemSignaturesContent
systemId={systemId}
settings={currentSettings}
settings={settingsSignatures}
deletedSignatures={deletedSignatures}
onLazyDeleteChange={handleLazyDeleteToggle}
onCountChange={handleCountChange}
@@ -207,7 +197,7 @@ export const SystemSignatures = () => {
{visible && (
<SystemSignatureSettingsDialog
settings={currentSettings}
settings={settingsSignatures}
onCancel={() => setVisible(false)}
onSave={handleSettingsSave}
/>

View File

@@ -8,7 +8,6 @@ import {
SortOrder,
} from 'primereact/datatable';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import useLocalStorageState from 'use-local-storage-state';
import { SignatureView } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView';
import {
@@ -17,9 +16,6 @@ import {
GROUPS_LIST,
MEDIUM_MAX_WIDTH,
OTHER_COLUMNS_WIDTH,
SETTINGS_KEYS,
SIGNATURE_WINDOW_ID,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
import { TooltipPosition, WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
@@ -36,19 +32,11 @@ import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { getSignatureRowClass } from '../helpers/rowStyles';
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
type SystemSignaturesSortSettings = {
sortField: string;
sortOrder: SortOrder;
};
const SORT_DEFAULT_VALUES: SystemSignaturesSortSettings = {
sortField: 'inserted_at',
sortOrder: -1,
};
interface SystemSignaturesContentProps {
systemId: string;
settings: SignatureSettingsType;
@@ -79,6 +67,10 @@ export const SystemSignaturesContent = ({
const [nameColumnWidth, setNameColumnWidth] = useState('auto');
const [hoveredSignature, setHoveredSignature] = useState<SystemSignature | null>(null);
const {
storedSettings: { settingsSignatures, settingsSignaturesUpdate },
} = useMapRootState();
const tableRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<WdTooltipHandlers>(null);
@@ -87,11 +79,6 @@ export const SystemSignaturesContent = ({
const { clipboardContent, setClipboardContent } = useClipboard();
const [sortSettings, setSortSettings] = useLocalStorageState<{ sortField: string; sortOrder: SortOrder }>(
'window:signatures:sort',
{ defaultValue: SORT_DEFAULT_VALUES },
);
const {
signatures,
selectedSignatures,
@@ -246,8 +233,8 @@ export const SystemSignaturesContent = ({
tooltipRef.current?.hide();
}, []);
const refVars = useRef({ settings, selectedSignatures, setSortSettings });
refVars.current = { settings, selectedSignatures, setSortSettings };
const refVars = useRef({ settings, selectedSignatures, settingsSignatures, settingsSignaturesUpdate });
refVars.current = { settings, selectedSignatures, settingsSignatures, settingsSignaturesUpdate };
// @ts-ignore
const getRowClassName = useCallback(rowData => {
@@ -263,7 +250,12 @@ export const SystemSignaturesContent = ({
}, []);
const handleSortSettings = useCallback(
(e: DataTableStateEvent) => refVars.current.setSortSettings({ sortField: e.sortField, sortOrder: e.sortOrder }),
(e: DataTableStateEvent) =>
refVars.current.settingsSignaturesUpdate({
...refVars.current.settingsSignatures,
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
}),
[],
);
@@ -295,8 +287,8 @@ export const SystemSignaturesContent = ({
rowHover
selectAll
onRowDoubleClick={handleRowClick}
sortField={sortSettings.sortField}
sortOrder={sortSettings.sortOrder}
sortField={settingsSignatures[SETTINGS_KEYS.SORT_FIELD] as string}
sortOrder={settingsSignatures[SETTINGS_KEYS.SORT_ORDER] as SortOrder}
onSort={handleSortSettings}
onRowMouseEnter={onRowMouseEnter}
onRowMouseLeave={onRowMouseLeave}

View File

@@ -11,6 +11,7 @@ import {
SignatureKindFR,
SignatureKindRU,
} from '@/hooks/Mapper/types';
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
export const TIME_ONE_MINUTE = 1000 * 60;
export const TIME_TEN_MINUTES = TIME_ONE_MINUTE * 10;
@@ -96,44 +97,11 @@ export const getGroupIdByRawGroup = (val: string): SignatureGroup | undefined =>
return MAPPING_GROUP_TO_ENG[val] || undefined;
};
export const SIGNATURE_WINDOW_ID = 'system_signatures_window';
export const SIGNATURE_SETTING_STORE_KEY = 'wanderer_system_signature_settings_v6_5';
export enum SETTINGS_KEYS {
SHOW_DESCRIPTION_COLUMN = 'show_description_column',
SHOW_UPDATED_COLUMN = 'show_updated_column',
SHOW_CHARACTER_COLUMN = 'show_character_column',
LAZY_DELETE_SIGNATURES = 'lazy_delete_signatures',
KEEP_LAZY_DELETE = 'keep_lazy_delete_enabled',
DELETION_TIMING = 'deletion_timing',
COLOR_BY_TYPE = 'color_by_type',
SHOW_CHARACTER_PORTRAIT = 'show_character_portrait',
// From SignatureKind
COSMIC_ANOMALY = SignatureKind.CosmicAnomaly,
COSMIC_SIGNATURE = SignatureKind.CosmicSignature,
DEPLOYABLE = SignatureKind.Deployable,
STRUCTURE = SignatureKind.Structure,
STARBASE = SignatureKind.Starbase,
SHIP = SignatureKind.Ship,
DRONE = SignatureKind.Drone,
// From SignatureGroup
WORMHOLE = SignatureGroup.Wormhole,
RELIC_SITE = SignatureGroup.RelicSite,
DATA_SITE = SignatureGroup.DataSite,
ORE_SITE = SignatureGroup.OreSite,
GAS_SITE = SignatureGroup.GasSite,
COMBAT_SITE = SignatureGroup.CombatSite,
}
export enum SettingsTypes {
flag,
dropdown,
}
export type SignatureSettingsType = { [key in SETTINGS_KEYS]?: unknown };
export type Setting = {
key: SETTINGS_KEYS;
name: string;
@@ -142,12 +110,6 @@ export type Setting = {
options?: { label: string; value: number | string | boolean }[];
};
export enum SIGNATURES_DELETION_TIMING {
IMMEDIATE,
DEFAULT,
EXTENDED,
}
// Now use a stricter type: every timing key maps to a number
export type SignatureDeletionTimingType = Record<SIGNATURES_DELETION_TIMING, number>;
@@ -194,32 +156,6 @@ export const SIGNATURE_SETTINGS = {
],
};
export const SETTINGS_VALUES: SignatureSettingsType = {
[SETTINGS_KEYS.SHOW_UPDATED_COLUMN]: true,
[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN]: true,
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: true,
[SETTINGS_KEYS.KEEP_LAZY_DELETE]: false,
[SETTINGS_KEYS.DELETION_TIMING]: SIGNATURES_DELETION_TIMING.DEFAULT,
[SETTINGS_KEYS.COLOR_BY_TYPE]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_PORTRAIT]: true,
[SETTINGS_KEYS.COSMIC_ANOMALY]: true,
[SETTINGS_KEYS.COSMIC_SIGNATURE]: true,
[SETTINGS_KEYS.DEPLOYABLE]: true,
[SETTINGS_KEYS.STRUCTURE]: true,
[SETTINGS_KEYS.STARBASE]: true,
[SETTINGS_KEYS.SHIP]: true,
[SETTINGS_KEYS.DRONE]: true,
[SETTINGS_KEYS.WORMHOLE]: true,
[SETTINGS_KEYS.RELIC_SITE]: true,
[SETTINGS_KEYS.DATA_SITE]: true,
[SETTINGS_KEYS.ORE_SITE]: true,
[SETTINGS_KEYS.GAS_SITE]: true,
[SETTINGS_KEYS.COMBAT_SITE]: true,
};
// Now this map is strongly typed as “number” for each timing enum
export const SIGNATURE_DELETION_TIMEOUTS: SignatureDeletionTimingType = {
[SIGNATURES_DELETION_TIMING.IMMEDIATE]: 0,

View File

@@ -0,0 +1,52 @@
import { getState } from './getState';
import { UNKNOWN_SIGNATURE_NAME } from '@/hooks/Mapper/helpers';
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
describe('getState', () => {
const mockSignaturesMatch: string[] = []; // This parameter is not used in the function
it('should return 0 if group is undefined', () => {
const newSig: SystemSignature = { id: '1', name: 'Test Sig', group: undefined } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(0);
});
it('should return 0 if group is CosmicSignature', () => {
const newSig: SystemSignature = { id: '1', name: 'Test Sig', group: SignatureGroup.CosmicSignature } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(0);
});
it('should return 1 if group is not CosmicSignature and name is undefined', () => {
const newSig: SystemSignature = { id: '1', name: undefined, group: SignatureGroup.Wormhole } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(1);
});
it('should return 1 if group is not CosmicSignature and name is empty', () => {
const newSig: SystemSignature = { id: '1', name: '', group: SignatureGroup.Wormhole } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(1);
});
it('should return 1 if group is not CosmicSignature and name is UNKNOWN_SIGNATURE_NAME', () => {
const newSig: SystemSignature = { id: '1', name: UNKNOWN_SIGNATURE_NAME, group: SignatureGroup.Wormhole } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(1);
});
it('should return 2 if group is not CosmicSignature and name is a non-empty string', () => {
const newSig: SystemSignature = { id: '1', name: 'Custom Name', group: SignatureGroup.Wormhole } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(2);
});
// According to the current implementation, state = -1 is unreachable
// because the conditions for 0, 1, and 2 cover all possibilities for the given inputs.
// If the logic of getState were to change to make -1 possible, a test case should be added here.
// For now, we can test a scenario that should lead to one of the valid states,
// for example, if group is something other than CosmicSignature and name is valid.
it('should handle other valid signature groups correctly, leading to state 2 with a valid name', () => {
const newSig: SystemSignature = { id: '1', name: 'Combat Site', group: SignatureGroup.CombatSite } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(2);
});
it('should handle other valid signature groups correctly, leading to state 1 with an empty name', () => {
const newSig: SystemSignature = { id: '1', name: '', group: SignatureGroup.DataSite } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(1);
});
});

View File

@@ -1,5 +1,5 @@
import { SignatureSettingsType } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
export interface UseSystemSignaturesDataProps {
systemId: string;

View File

@@ -5,15 +5,13 @@ import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useEffect, useState } from 'react';
import useRefState from 'react-usestateref';
import {
SETTINGS_KEYS,
getDeletionTimeoutMs,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getActualSigs } from '../helpers';
import { UseSystemSignaturesDataProps } from './types';
import { usePendingDeletions } from './usePendingDeletions';
import { useSignatureFetching } from './useSignatureFetching';
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures.ts';
export const useSystemSignaturesData = ({
systemId,
@@ -78,15 +76,10 @@ export const useSystemSignaturesData = ({
if (removed.length > 0) {
await processRemovedSignatures(removed, added, updated);
// Only show pending deletions if:
// 1. Lazy deletion is enabled AND
// 2. Deletion timing is not immediate (> 0)
// Show pending deletions if lazy deletion is enabled
// The deletion timing controls how long the countdown lasts, not whether lazy delete is active
if (onSignatureDeleted && lazyDeleteValue) {
const timeoutMs = getDeletionTimeoutMs(settings);
if (timeoutMs > 0) {
onSignatureDeleted(removed);
}
onSignatureDeleted(removed);
}
}

View File

@@ -3,7 +3,6 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemKillsList } from './SystemKillsList';
import { KillsHeader } from './components/SystemKillsHeader';
import { useKillsWidgetSettings } from './hooks/useKillsWidgetSettings';
import { useSystemKills } from './hooks/useSystemKills';
import { KillsSettingsDialog } from './components/SystemKillsSettingsDialog';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
@@ -13,27 +12,25 @@ const SystemKillsContent = () => {
const {
data: { selectedSystems, isSubscriptionActive },
outCommand,
storedSettings: { settingsKills },
} = useMapRootState();
const [systemId] = selectedSystems || [];
const systemStaticInfo = getSystemStaticInfo(systemId)!;
const [settings] = useKillsWidgetSettings();
const visible = settings.showAll;
const { kills, isLoading, error } = useSystemKills({
systemId,
outCommand,
showAllVisible: visible,
sinceHours: settings.timeRange,
showAllVisible: settingsKills.showAll,
sinceHours: settingsKills.timeRange,
});
const isNothingSelected = !systemId && !visible;
const isNothingSelected = !systemId && !settingsKills.showAll;
const showLoading = isLoading && kills.length === 0;
const filteredKills = useMemo(() => {
if (!settings.whOnly || !visible) return kills;
if (!settingsKills.whOnly || !settingsKills.showAll) return kills;
return kills.filter(kill => {
if (!systemStaticInfo) {
console.warn(`System with id ${kill.solar_system_id} not found.`);
@@ -41,7 +38,7 @@ const SystemKillsContent = () => {
}
return isWormholeSpace(systemStaticInfo.system_class);
});
}, [kills, settings.whOnly, systemStaticInfo, visible]);
}, [kills, settingsKills.whOnly, systemStaticInfo, settingsKills.showAll]);
if (!isSubscriptionActive) {
return (
@@ -87,7 +84,9 @@ const SystemKillsContent = () => {
);
}
return <SystemKillsList kills={filteredKills} onlyOneSystem={!visible} timeRange={settings.timeRange} />;
return (
<SystemKillsList kills={filteredKills} onlyOneSystem={!settingsKills.showAll} timeRange={settingsKills.timeRange} />
);
};
export const WSystemKills = () => {

View File

@@ -17,67 +17,91 @@ import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { WithClassName } from '@/hooks/Mapper/types/common.ts';
export type CompactKillRowProps = {
killDetails: DetailedKill;
killDetails?: DetailedKill | null;
systemName: string;
onlyOneSystem: boolean;
} & WithClassName;
export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, className }: CompactKillRowProps) => {
const {
killmail_id = 0,
killmail_id,
// Victim data
victim_char_name = 'Unknown Pilot',
victim_alliance_ticker = '',
victim_corp_ticker = '',
victim_ship_name = 'Unknown Ship',
victim_corp_name = '',
victim_alliance_name = '',
victim_char_id = 0,
victim_corp_id = 0,
victim_alliance_id = 0,
victim_ship_type_id = 0,
// Attacker data
final_blow_char_id = 0,
final_blow_char_name = '',
final_blow_alliance_ticker = '',
final_blow_alliance_name = '',
final_blow_alliance_id = 0,
final_blow_corp_ticker = '',
final_blow_corp_id = 0,
final_blow_corp_name = '',
final_blow_ship_type_id = 0,
kill_time = '',
total_value = 0,
} = killDetails || {};
const attackerIsNpc = final_blow_char_id === 0;
// Define victim affiliation ticker.
const victimAffiliationTicker = victim_alliance_ticker || victim_corp_ticker || 'No Ticker';
const killValueFormatted = total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
const attackerSubscript = getAttackerSubscript(killDetails);
const { victimCorpLogoUrl, victimAllianceLogoUrl, victimShipUrl } = buildVictimImageUrls({
victim_char_name,
victim_alliance_ticker,
victim_corp_ticker,
victim_ship_name,
victim_corp_name,
victim_alliance_name,
victim_char_id,
victim_ship_type_id,
victim_corp_id,
victim_alliance_id,
victim_ship_type_id,
// Attacker data
final_blow_char_id,
final_blow_char_name,
final_blow_alliance_ticker,
final_blow_alliance_name,
final_blow_alliance_id,
final_blow_corp_ticker,
final_blow_corp_id,
final_blow_corp_name,
final_blow_ship_type_id,
kill_time,
total_value,
} = killDetails || {};
// Apply fallback values using nullish coalescing to handle both null and undefined
const safeKillmailId = killmail_id ?? 0;
const safeVictimCharName = victim_char_name ?? 'Unknown Pilot';
const safeVictimAllianceTicker = victim_alliance_ticker ?? '';
const safeVictimCorpTicker = victim_corp_ticker ?? '';
const safeVictimShipName = victim_ship_name ?? 'Unknown Ship';
const safeVictimCorpName = victim_corp_name ?? '';
const safeVictimAllianceName = victim_alliance_name ?? '';
const safeVictimCharId = victim_char_id ?? 0;
const safeVictimCorpId = victim_corp_id ?? 0;
const safeVictimAllianceId = victim_alliance_id ?? 0;
const safeVictimShipTypeId = victim_ship_type_id ?? 0;
const safeFinalBlowCharId = final_blow_char_id ?? 0;
const safeFinalBlowCharName = final_blow_char_name ?? '';
const safeFinalBlowAllianceTicker = final_blow_alliance_ticker ?? '';
const safeFinalBlowAllianceName = final_blow_alliance_name ?? '';
const safeFinalBlowAllianceId = final_blow_alliance_id ?? 0;
const safeFinalBlowCorpTicker = final_blow_corp_ticker ?? '';
const safeFinalBlowCorpId = final_blow_corp_id ?? 0;
const safeFinalBlowCorpName = final_blow_corp_name ?? '';
const safeFinalBlowShipTypeId = final_blow_ship_type_id ?? 0;
const safeKillTime = kill_time ?? '';
const safeTotalValue = total_value ?? 0;
const attackerIsNpc = safeFinalBlowCharId === 0;
// Define victim affiliation ticker.
const victimAffiliationTicker = safeVictimAllianceTicker || safeVictimCorpTicker || 'No Ticker';
const killValueFormatted = safeTotalValue != null && safeTotalValue > 0 ? `${formatISK(safeTotalValue)} ISK` : null;
const killTimeAgo = safeKillTime ? formatTimeMixed(safeKillTime) : '0h ago';
const attackerSubscript = killDetails ? getAttackerSubscript(killDetails) : undefined;
const { victimCorpLogoUrl, victimAllianceLogoUrl, victimShipUrl } = buildVictimImageUrls({
victim_char_id: safeVictimCharId,
victim_ship_type_id: safeVictimShipTypeId,
victim_corp_id: safeVictimCorpId,
victim_alliance_id: safeVictimAllianceId,
});
const { attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
final_blow_char_id,
final_blow_corp_id,
final_blow_alliance_id,
final_blow_char_id: safeFinalBlowCharId,
final_blow_corp_id: safeFinalBlowCorpId,
final_blow_alliance_id: safeFinalBlowAllianceId,
});
const { url: victimPrimaryLogoUrl, tooltip: victimPrimaryTooltip } = getPrimaryLogoAndTooltip(
victimAllianceLogoUrl,
victimCorpLogoUrl,
victim_alliance_name,
victim_corp_name,
safeVictimAllianceName,
safeVictimCorpName,
'Victim',
);
@@ -87,25 +111,25 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
attackerIsNpc,
attackerAllianceLogoUrl,
attackerCorpLogoUrl,
final_blow_alliance_name,
final_blow_corp_name,
final_blow_ship_type_id,
safeFinalBlowAllianceName,
safeFinalBlowCorpName,
safeFinalBlowShipTypeId,
),
[
attackerAllianceLogoUrl,
attackerCorpLogoUrl,
attackerIsNpc,
final_blow_alliance_name,
final_blow_corp_name,
final_blow_ship_type_id,
safeFinalBlowAllianceName,
safeFinalBlowCorpName,
safeFinalBlowShipTypeId,
],
);
// Define attackerTicker to use the alliance ticker if available, otherwise the corp ticker.
const attackerTicker = attackerIsNpc ? '' : final_blow_alliance_ticker || final_blow_corp_ticker || '';
const attackerTicker = attackerIsNpc ? '' : safeFinalBlowAllianceTicker || safeFinalBlowCorpTicker || '';
// For the attacker image link: if the attacker is not an NPC, link to the character page; otherwise, link to the kill page.
const attackerLink = attackerIsNpc ? zkillLink('kill', killmail_id) : zkillLink('character', final_blow_char_id);
const attackerLink = attackerIsNpc ? zkillLink('kill', safeKillmailId) : zkillLink('character', safeFinalBlowCharId);
return (
<div
@@ -121,7 +145,7 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
{victimShipUrl && (
<div className="relative shrink-0 w-8 h-8 overflow-hidden">
<a
href={zkillLink('kill', killmail_id)}
href={zkillLink('kill', safeKillmailId)}
target="_blank"
rel="noopener noreferrer"
className="block w-full h-full"
@@ -137,7 +161,7 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
{victimPrimaryLogoUrl && (
<WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}>
<a
href={zkillLink('kill', killmail_id)}
href={zkillLink('kill', safeKillmailId)}
target="_blank"
rel="noopener noreferrer"
className="relative block shrink-0 w-8 h-8 overflow-hidden"
@@ -153,12 +177,12 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
</div>
<div className="flex flex-col ml-2 flex-1 min-w-0 overflow-hidden leading-[1rem]">
<div className="truncate text-stone-200">
{victim_char_name}
{safeVictimCharName}
<span className="text-stone-400"> / {victimAffiliationTicker}</span>
</div>
<div className="truncate text-stone-300 flex items-center gap-1">
<span className="text-stone-400 overflow-hidden text-ellipsis whitespace-nowrap max-w-[140px]">
{victim_ship_name}
{safeVictimShipName}
</span>
{killValueFormatted && (
<>
@@ -170,9 +194,9 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
</div>
<div className="flex items-center ml-auto gap-2">
<div className="flex flex-col items-end flex-1 min-w-0 overflow-hidden text-right leading-[1rem]">
{!attackerIsNpc && (final_blow_char_name || attackerTicker) && (
{!attackerIsNpc && (safeFinalBlowCharName || attackerTicker) && (
<div className="truncate text-stone-200">
{final_blow_char_name}
{safeFinalBlowCharName}
{!attackerIsNpc && attackerTicker && <span className="ml-1 text-stone-400">/ {attackerTicker}</span>}
</div>
)}

View File

@@ -7,9 +7,9 @@ import {
WdImgButton,
WdTooltipWrapper,
} from '@/hooks/Mapper/components/ui-kit';
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
import { PrimeIcons } from 'primereact/api';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
interface KillsHeaderProps {
systemId?: string;
@@ -17,11 +17,14 @@ interface KillsHeaderProps {
}
export const KillsHeader: React.FC<KillsHeaderProps> = ({ systemId, onOpenSettings }) => {
const [settings, setSettings] = useKillsWidgetSettings();
const { showAll } = settings;
const {
storedSettings: { settingsKills, settingsKillsUpdate },
} = useMapRootState();
const { showAll } = settingsKills;
const onToggleShowAllVisible = () => {
setSettings(prev => ({ ...prev, showAll: !prev.showAll }));
settingsKillsUpdate(prev => ({ ...prev, showAll: !prev.showAll }));
};
const headerRef = useRef<HTMLDivElement>(null);

View File

@@ -3,12 +3,12 @@ import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { PrimeIcons } from 'primereact/api';
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import { SystemView, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
interface KillsSettingsDialogProps {
visible: boolean;
@@ -16,12 +16,15 @@ interface KillsSettingsDialogProps {
}
export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visible, setVisible }) => {
const [globalSettings, setGlobalSettings] = useKillsWidgetSettings();
const {
storedSettings: { settingsKills, settingsKillsUpdate },
} = useMapRootState();
const localRef = useRef({
showAll: globalSettings.showAll,
whOnly: globalSettings.whOnly,
excludedSystems: globalSettings.excludedSystems || [],
timeRange: globalSettings.timeRange,
showAll: settingsKills.showAll,
whOnly: settingsKills.whOnly,
excludedSystems: settingsKills.excludedSystems || [],
timeRange: settingsKills.timeRange,
});
const [, forceRender] = useState(0);
@@ -30,14 +33,14 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
useEffect(() => {
if (visible) {
localRef.current = {
showAll: globalSettings.showAll,
whOnly: globalSettings.whOnly,
excludedSystems: globalSettings.excludedSystems || [],
timeRange: globalSettings.timeRange,
showAll: settingsKills.showAll,
whOnly: settingsKills.whOnly,
excludedSystems: settingsKills.excludedSystems || [],
timeRange: settingsKills.timeRange,
};
forceRender(n => n + 1);
}
}, [visible, globalSettings]);
}, [visible, settingsKills]);
const handleWHChange = useCallback((checked: boolean) => {
localRef.current = {
@@ -75,12 +78,12 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
}, []);
const handleApply = useCallback(() => {
setGlobalSettings(prev => ({
settingsKillsUpdate(prev => ({
...prev,
...localRef.current,
}));
setVisible(false);
}, [setGlobalSettings, setVisible]);
}, [settingsKillsUpdate, setVisible]);
const handleHide = useCallback(() => {
setVisible(false);

View File

@@ -33,7 +33,10 @@ export function formatISK(value: number): string {
return Math.round(value).toString();
}
export function getAttackerSubscript(kill: DetailedKill) {
export function getAttackerSubscript(kill: DetailedKill | undefined) {
if (!kill) {
return null;
}
if (kill.npc) {
return { label: 'npc', cssClass: 'text-purple-400' };
}

View File

@@ -1,53 +0,0 @@
import { useMemo, useCallback } from 'react';
import useLocalStorageState from 'use-local-storage-state';
export interface KillsWidgetSettings {
showAll: boolean;
whOnly: boolean;
excludedSystems: number[];
version: number;
timeRange: number;
}
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
showAll: false,
whOnly: true,
excludedSystems: [],
version: 2,
timeRange: 4,
};
function mergeWithDefaults(settings?: Partial<KillsWidgetSettings>): KillsWidgetSettings {
if (!settings) {
return DEFAULT_KILLS_WIDGET_SETTINGS;
}
return {
...DEFAULT_KILLS_WIDGET_SETTINGS,
...settings,
excludedSystems: Array.isArray(settings.excludedSystems) ? settings.excludedSystems : [],
};
}
export function useKillsWidgetSettings() {
const [rawValue, setRawValue] = useLocalStorageState<KillsWidgetSettings | undefined>('kills:widget:settings');
const value = useMemo<KillsWidgetSettings>(() => {
return mergeWithDefaults(rawValue);
}, [rawValue]);
const setValue = useCallback(
(newVal: KillsWidgetSettings | ((prev: KillsWidgetSettings) => KillsWidgetSettings)) => {
setRawValue(prev => {
const mergedPrev = mergeWithDefaults(prev);
const nextUnmerged = typeof newVal === 'function' ? newVal(mergedPrev) : newVal;
return mergeWithDefaults(nextUnmerged);
});
},
[setRawValue],
);
return [value, setValue] as const;
}

View File

@@ -3,7 +3,6 @@ import debounce from 'lodash.debounce';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useKillsWidgetSettings } from './useKillsWidgetSettings';
interface UseSystemKillsProps {
systemId?: string;
@@ -13,26 +12,25 @@ interface UseSystemKillsProps {
sinceHours?: number;
}
function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceHours: number): DetailedKill[] {
const cutoff = Date.now() - sinceHours * 60 * 60 * 1000;
function combineKills(existing: DetailedKill[], incoming: DetailedKill[]): DetailedKill[] {
// Don't filter by time when storing - let components filter when displaying
const byId: Record<string, DetailedKill> = {};
for (const kill of [...existing, ...incoming]) {
if (!kill.kill_time) continue;
const killTimeMs = new Date(kill.kill_time).valueOf();
if (killTimeMs >= cutoff) {
byId[kill.killmail_id] = kill;
}
byId[kill.killmail_id] = kill;
}
return Object.values(byId);
}
export function useSystemKills({ systemId, outCommand, showAllVisible = false, sinceHours = 24 }: UseSystemKillsProps) {
const { data, update } = useMapRootState();
const { detailedKills = {}, systems = [] } = data;
const [settings] = useKillsWidgetSettings();
const excludedSystems = settings.excludedSystems;
const {
data: { detailedKills = {}, systems = [] },
update,
storedSettings: { settingsKills },
} = useMapRootState();
const { excludedSystems } = settingsKills;
const effectiveSinceHours = sinceHours;
@@ -55,14 +53,14 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
for (const [sid, newKills] of Object.entries(killsMap)) {
const existing = updated[sid] ?? [];
const combined = combineKills(existing, newKills, effectiveSinceHours);
const combined = combineKills(existing, newKills);
updated[sid] = combined;
}
return { ...prev, detailedKills: updated };
});
},
[update, effectiveSinceHours],
[update],
);
const fetchKills = useCallback(

View File

@@ -14,13 +14,14 @@ import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/compone
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types';
import { PingsInterface } from '@/hooks/Mapper/components/mapInterface/components';
import { OldSettingsDialog } from '@/hooks/Mapper/components/mapRootContent/components/OldSettingsDialog.tsx';
export interface MapRootContentProps {}
// eslint-disable-next-line no-empty-pattern
export const MapRootContent = ({}: MapRootContentProps) => {
const {
storedSettings: { interfaceSettings },
storedSettings: { interfaceSettings, isReady, hasOldSettings },
data,
} = useMapRootState();
const { isShowMenu } = interfaceSettings;
@@ -34,7 +35,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
const [showTrackingDialog, setShowTrackingDialog] = useState(false);
/* Important Notice - this solution needs for use one instance of MapInterface */
const mapInterface = <MapInterface />;
const mapInterface = isReady ? <MapInterface /> : null;
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []);
@@ -90,6 +91,8 @@ export const MapRootContent = ({}: MapRootContentProps) => {
{showTrackingDialog && (
<TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} />
)}
{hasOldSettings && <OldSettingsDialog />}
</Layout>
</div>
);

View File

@@ -12,6 +12,7 @@ import {
import { WidgetsSettings } from './components/WidgetsSettings';
import { CommonSettings } from './components/CommonSettings';
import { SettingsListItem } from './types.ts';
import { ImportExport } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components/ImportExport.tsx';
export interface MapSettingsProps {
visible: boolean;
@@ -87,6 +88,10 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
<TabPanel header="Widgets" className="h-full" headerClassName={styles.verticalTabHeader}>
<WidgetsSettings />
</TabPanel>
<TabPanel header="Import/Export" className="h-full" headerClassName={styles.verticalTabHeader}>
<ImportExport />
</TabPanel>
</TabView>
</div>
</div>

View File

@@ -22,6 +22,7 @@ import { OutCommand } from '@/hooks/Mapper/types';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { Dropdown } from 'primereact/dropdown';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
type MapSettingsContextType = {
renderSettingItem: (item: SettingsListItem) => ReactNode;
@@ -30,7 +31,7 @@ type MapSettingsContextType = {
const MapSettingsContext = createContext<MapSettingsContextType | undefined>(undefined);
export const MapSettingsProvider = ({ children }: { children: ReactNode }) => {
export const MapSettingsProvider = ({ children }: WithChildren) => {
const {
outCommand,
storedSettings: { interfaceSettings, setInterfaceSettings },

View File

@@ -0,0 +1,202 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useMemo, useRef } from 'react';
import { Toast } from 'primereact/toast';
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { saveTextFile } from '@/hooks/Mapper/utils/saveToFile.ts';
import { SplitButton } from 'primereact/splitbutton';
import { loadTextFile } from '@/hooks/Mapper/utils';
export const ImportExport = () => {
const {
storedSettings: { getSettingsForExport, applySettings },
data: { map_slug },
} = useMapRootState();
const toast = useRef<Toast | null>(null);
const handleImportFromClipboard = useCallback(async () => {
const text = await navigator.clipboard.readText();
if (text == null || text == '') {
return;
}
try {
const parsed = parseMapUserSettings(text);
if (applySettings(parsed)) {
toast.current?.show({
severity: 'success',
summary: 'Import',
detail: 'Map settings was imported successfully.',
life: 3000,
});
setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 100);
return;
}
toast.current?.show({
severity: 'warn',
summary: 'Warning',
detail: 'Settings already imported. Or something went wrong.',
life: 3000,
});
} catch (error) {
console.error(`Import from clipboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on import from Clipboard, check console log.',
life: 3000,
});
}
}, [applySettings]);
const handleImportFromFile = useCallback(async () => {
try {
const text = await loadTextFile();
const parsed = parseMapUserSettings(text);
if (applySettings(parsed)) {
toast.current?.show({
severity: 'success',
summary: 'Import',
detail: 'Map settings was imported successfully.',
life: 3000,
});
return;
}
toast.current?.show({
severity: 'warn',
summary: 'Warning',
detail: 'Settings already imported. Or something went wrong.',
life: 3000,
});
} catch (error) {
console.error(`Import from file Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on import from File, check console log.',
life: 3000,
});
}
}, [applySettings]);
const handleExportToClipboard = useCallback(async () => {
const settings = getSettingsForExport();
if (!settings) {
return;
}
try {
await navigator.clipboard.writeText(settings);
toast.current?.show({
severity: 'success',
summary: 'Export',
detail: 'Map settings copied into clipboard',
life: 3000,
});
} catch (error) {
console.error(`Export to clipboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on copying to clipboard, check console log.',
life: 3000,
});
}
}, [getSettingsForExport]);
const handleExportToFile = useCallback(async () => {
const settings = getSettingsForExport();
if (!settings) {
return;
}
try {
saveTextFile(`map_settings_${map_slug}.json`, settings);
toast.current?.show({
severity: 'success',
summary: 'Export to File',
detail: 'Map settings successfully saved to file',
life: 3000,
});
} catch (error) {
console.error(`Export to cliboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on saving to file, check console log.',
life: 3000,
});
}
}, [getSettingsForExport, map_slug]);
const importItems = useMemo(
() => [
{
label: 'Import from File',
icon: 'pi pi-file-import',
command: handleImportFromFile,
},
],
[handleImportFromFile],
);
const exportItems = useMemo(
() => [
{
label: 'Export as File',
icon: 'pi pi-file-export',
command: handleExportToFile,
},
],
[handleExportToFile],
);
return (
<div className="w-full h-full flex flex-col gap-5">
<div className="flex flex-col gap-1">
<div>
<SplitButton
onClick={handleImportFromClipboard}
icon="pi pi-download"
size="small"
severity="warning"
label="Import from Clipboard"
className="py-[4px]"
model={importItems}
/>
</div>
<span className="text-stone-500 text-[12px]">
*Will read map settings from clipboard. Be careful it could overwrite current settings.
</span>
</div>
<div className="flex flex-col gap-1">
<div>
<SplitButton
onClick={handleExportToClipboard}
icon="pi pi-upload"
size="small"
label="Export to Clipboard"
className="py-[4px]"
model={exportItems}
/>
</div>
<span className="text-stone-500 text-[12px]">*Will save map settings to clipboard.</span>
</div>
<Toast ref={toast} />
</div>
);
};

View File

@@ -0,0 +1,206 @@
import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useCallback, useRef, useState } from 'react';
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
import { Toast } from 'primereact/toast';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { saveTextFile } from '@/hooks/Mapper/utils';
const createSettings = function <T>(lsSettings: string | null, defaultValues: T) {
return {
version: -1,
settings: lsSettings ? JSON.parse(lsSettings) : defaultValues,
};
};
export const OldSettingsDialog = () => {
const cpRemoveBtnRef = useRef<HTMLElement>();
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
const toast = useRef<Toast | null>(null);
const {
storedSettings: { checkOldSettings },
data: { map_slug },
} = useMapRootState();
const handleExport = useCallback(
async (asFile?: boolean) => {
const interfaceSettings = localStorage.getItem('window:interface:settings');
const widgetRoutes = localStorage.getItem('window:interface:routes');
const widgetLocal = localStorage.getItem('window:interface:local');
const widgetKills = localStorage.getItem('kills:widget:settings');
const onTheMapOld = localStorage.getItem('window:onTheMap:settings');
const widgetsOld = localStorage.getItem('windows:settings:v2');
const signatures = localStorage.getItem('wanderer_system_signature_settings_v6_5');
const out: MapUserSettings = {
killsWidget: createSettings(widgetKills, DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createSettings(widgetLocal, DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createSettings(widgetsOld, getDefaultWidgetProps()),
routes: createSettings(widgetRoutes, DEFAULT_ROUTES_SETTINGS),
onTheMap: createSettings(onTheMapOld, DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createSettings(signatures, DEFAULT_SIGNATURE_SETTINGS),
interface: createSettings(interfaceSettings, STORED_INTERFACE_DEFAULT_VALUES),
};
if (asFile) {
if (!out) {
return;
}
try {
saveTextFile(`map_settings_${map_slug}.json`, JSON.stringify(out));
toast.current?.show({
severity: 'success',
summary: 'Export to File',
detail: 'Map settings successfully saved to file',
life: 3000,
});
} catch (error) {
console.error(`Export to cliboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on saving to file, check console log.',
life: 3000,
});
return;
}
return;
}
try {
await navigator.clipboard.writeText(JSON.stringify(out));
toast.current?.show({
severity: 'success',
summary: 'Export to clipboard',
detail: 'Map settings was export successfully.',
life: 3000,
});
} catch (error) {
console.error(`Export to clipboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on copying to clipboard, check console log.',
life: 3000,
});
}
},
[map_slug],
);
const handleExportClipboard = useCallback(async () => {
await handleExport();
}, [handleExport]);
const handleExportAsFile = useCallback(async () => {
await handleExport(true);
}, [handleExport]);
const handleProceed = useCallback(() => {
localStorage.removeItem('window:interface:settings');
localStorage.removeItem('window:interface:routes');
localStorage.removeItem('window:interface:local');
localStorage.removeItem('kills:widget:settings');
localStorage.removeItem('window:onTheMap:settings');
localStorage.removeItem('windows:settings:v2');
localStorage.removeItem('wanderer_system_signature_settings_v6_5');
checkOldSettings();
}, [checkOldSettings]);
return (
<>
<Dialog
header={
<div className="dialog-header">
<span className="pointer-events-none">Old settings detected!</span>
</div>
}
draggable={false}
resizable={false}
closable={false}
visible
onHide={() => null}
className="w-[640px] h-[400px] text-text-color min-h-0"
footer={
<div className="flex items-center justify-end">
<Button
// @ts-ignore
ref={cpRemoveBtnRef}
onClick={handleShowCP}
icon="pi pi-exclamation-triangle"
size="small"
severity="warning"
label="Proceed"
/>
</div>
}
>
<div className="w-full h-full flex flex-col gap-1 items-center justify-center text-stone-400 text-[15px]">
<span>
We detected <span className="text-orange-400">deprecated</span> settings saved in your browser.
</span>
<span>
Now we will give you ability to make <span className="text-orange-400">export</span> your old settings.
</span>
<span>
After click: all settings will saved in your <span className="text-orange-400">clipboard</span>.
</span>
<span>
Then you need to go into <span className="text-orange-400">Map Settings</span> and click{' '}
<span className="text-orange-400">Import from clipboard</span>
</span>
<div className="h-[30px]"></div>
<div className="flex items-center gap-3">
<Button
onClick={handleExportClipboard}
icon="pi pi-copy"
size="small"
severity="info"
label="Export to Clipboard"
/>
<Button
onClick={handleExportAsFile}
icon="pi pi-download"
size="small"
severity="info"
label="Export as File"
/>
</div>
<span className="text-stone-600 text-[12px]">*You will see this dialog until click Export.</span>
</div>
</Dialog>
<ConfirmPopup
target={cpRemoveBtnRef.current}
visible={cpRemoveVisible}
onHide={handleHideCP}
message="After click dialog will disappear. Ready?"
icon="pi pi-exclamation-triangle"
accept={handleProceed}
/>
<Toast ref={toast} />
</>
);
};

View File

@@ -7,24 +7,11 @@ import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virt
import clsx from 'clsx';
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
import { CharacterCard, TooltipPosition, WdCheckbox, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import useLocalStorageState from 'use-local-storage-state';
import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
type WindowLocalSettingsType = {
compact: boolean;
hideOffline: boolean;
version: number;
};
const STORED_DEFAULT_VALUES: WindowLocalSettingsType = {
compact: true,
hideOffline: false,
version: 0,
};
const itemTemplate = (item: CharacterTypeRaw & WithIsOwnCharacter, options: VirtualScrollerTemplateOptions) => {
return (
<div
@@ -48,14 +35,11 @@ export interface OnTheMapProps {
export const OnTheMap = ({ show, onHide }: OnTheMapProps) => {
const {
data: { characters, userCharacters },
storedSettings: { settingsOnTheMap, settingsOnTheMapUpdate },
} = useMapRootState();
const [searchVal, setSearchVal] = useState('');
const [settings, setSettings] = useLocalStorageState<WindowLocalSettingsType>('window:onTheMap:settings', {
defaultValue: STORED_DEFAULT_VALUES,
});
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
@@ -107,12 +91,12 @@ export const OnTheMap = ({ show, onHide }: OnTheMapProps) => {
});
}
if (showOffline && !settings.hideOffline) {
if (showOffline && !settingsOnTheMap.hideOffline) {
return out;
}
return out.filter(x => x.online);
}, [showOffline, searchVal, characters, settings.hideOffline, userCharacters]);
}, [showOffline, searchVal, characters, settingsOnTheMap.hideOffline, userCharacters]);
return (
<Sidebar
@@ -153,9 +137,11 @@ export const OnTheMap = ({ show, onHide }: OnTheMapProps) => {
size="m"
labelSide="left"
label={'Hide offline'}
value={settings.hideOffline}
value={settingsOnTheMap.hideOffline}
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
onChange={() => setSettings(() => ({ ...settings, hideOffline: !settings.hideOffline }))}
onChange={() =>
settingsOnTheMapUpdate(() => ({ ...settingsOnTheMap, hideOffline: !settingsOnTheMap.hideOffline }))
}
/>
)}
</div>

View File

@@ -0,0 +1,49 @@
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import useLocalStorageState from 'use-local-storage-state';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const DebugComponent = () => {
const { outCommand } = useMapRootState();
const [record, setRecord] = useLocalStorageState<boolean>('record', {
defaultValue: false,
});
// @ts-ignore
const [recordsList] = useLocalStorageState<{ type; data }[]>('recordsList', {
defaultValue: [],
});
const handleRunSavedEvents = () => {
recordsList.forEach(record => outCommand(record));
};
return (
<>
<WdTooltipWrapper content="Run saved events" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={handleRunSavedEvents}
disabled={recordsList.length === 0 || record}
>
<i className="pi pi-forward"></i>
</button>
</WdTooltipWrapper>
<WdTooltipWrapper content="Record" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={() => setRecord(x => !x)}
>
{!record ? (
<i className="pi pi-play-circle text-green-500"></i>
) : (
<i className="pi pi-stop-circle text-red-500"></i>
)}
</button>
</WdTooltipWrapper>
</>
);
};

View File

@@ -7,6 +7,7 @@ import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
// import { DebugComponent } from '@/hooks/Mapper/components/mapRootContent/components/RightBar/DebugComponent.tsx';
interface RightBarProps {
onShowOnTheMap?: () => void;
@@ -79,6 +80,9 @@ export const RightBar = ({
</div>
<div className="flex flex-col items-center mb-2 gap-1">
{/* TODO - do not delete this code needs for debug */}
{/*<DebugComponent />*/}
<WdTooltipWrapper content="Map user settings" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"

View File

@@ -48,7 +48,7 @@ export const MapWrapper = () => {
linkSignatureToSystem,
systemSignatures,
},
storedSettings: { interfaceSettings },
storedSettings: { interfaceSettings, settingsLocal },
} = useMapRootState();
const {
@@ -183,15 +183,22 @@ export const MapWrapper = () => {
const handleTogglePing = useCallback(async (type: PingType, solar_system_id: string, hasPing: boolean) => {
if (hasPing) {
// Find the ping for this solar system to get its ID
const ping = pings.find(p => p.solar_system_id === solar_system_id);
if (!ping) {
console.error('Cannot find ping for solar system:', solar_system_id);
return;
}
await outCommand({
type: OutCommand.cancelPing,
data: { type, solar_system_id: solar_system_id },
data: { type, id: ping.id },
});
return;
}
setOpenPing({ type, solar_system_id });
}, []);
}, [pings, outCommand]);
const handleCustomLabelDialog = useCallback(() => {
const { systemContextProps } = ref.current;
@@ -254,6 +261,7 @@ export const MapWrapper = () => {
pings={pings}
onAddSystem={onAddSystem}
minimapPlacement={minimapPosition}
localShowShipName={settingsLocal.showShipName}
/>
{openSettings != null && (

View File

@@ -33,6 +33,7 @@ export enum Regions {
Solitude = 10000044,
TashMurkon = 10000020,
VergeVendor = 10000068,
Pochven = 10000070,
}
export enum Spaces {
@@ -40,6 +41,7 @@ export enum Spaces {
'Gallente' = 'Gallente',
'Matar' = 'Matar',
'Amarr' = 'Amarr',
'Pochven' = 'Pochven',
}
export const REGIONS_MAP: Record<number, Spaces> = {
@@ -66,6 +68,7 @@ export const REGIONS_MAP: Record<number, Spaces> = {
[Regions.Solitude]: Spaces.Gallente,
[Regions.TashMurkon]: Spaces.Amarr,
[Regions.VergeVendor]: Spaces.Gallente,
[Regions.Pochven]: Spaces.Pochven,
};
export type K162Type = {

View File

@@ -0,0 +1,71 @@
import { SignatureGroup, SignatureKind } from '@/hooks/Mapper/types';
export const SIGNATURE_WINDOW_ID = 'system_signatures_window';
export enum SIGNATURES_DELETION_TIMING {
IMMEDIATE,
DEFAULT,
EXTENDED,
}
export enum SETTINGS_KEYS {
SORT_FIELD = 'sortField',
SORT_ORDER = 'sortOrder',
SHOW_DESCRIPTION_COLUMN = 'show_description_column',
SHOW_UPDATED_COLUMN = 'show_updated_column',
SHOW_CHARACTER_COLUMN = 'show_character_column',
LAZY_DELETE_SIGNATURES = 'lazy_delete_signatures',
KEEP_LAZY_DELETE = 'keep_lazy_delete_enabled',
DELETION_TIMING = 'deletion_timing',
COLOR_BY_TYPE = 'color_by_type',
SHOW_CHARACTER_PORTRAIT = 'show_character_portrait',
// From SignatureKind
COSMIC_ANOMALY = SignatureKind.CosmicAnomaly,
COSMIC_SIGNATURE = SignatureKind.CosmicSignature,
DEPLOYABLE = SignatureKind.Deployable,
STRUCTURE = SignatureKind.Structure,
STARBASE = SignatureKind.Starbase,
SHIP = SignatureKind.Ship,
DRONE = SignatureKind.Drone,
// From SignatureGroup
WORMHOLE = SignatureGroup.Wormhole,
RELIC_SITE = SignatureGroup.RelicSite,
DATA_SITE = SignatureGroup.DataSite,
ORE_SITE = SignatureGroup.OreSite,
GAS_SITE = SignatureGroup.GasSite,
COMBAT_SITE = SignatureGroup.CombatSite,
}
export type SignatureSettingsType = { [key in SETTINGS_KEYS]?: unknown };
export const DEFAULT_SIGNATURE_SETTINGS: SignatureSettingsType = {
[SETTINGS_KEYS.SORT_FIELD]: 'inserted_at',
[SETTINGS_KEYS.SORT_ORDER]: -1,
[SETTINGS_KEYS.SHOW_UPDATED_COLUMN]: true,
[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN]: true,
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: true,
[SETTINGS_KEYS.KEEP_LAZY_DELETE]: false,
[SETTINGS_KEYS.DELETION_TIMING]: SIGNATURES_DELETION_TIMING.DEFAULT,
[SETTINGS_KEYS.COLOR_BY_TYPE]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_PORTRAIT]: true,
[SETTINGS_KEYS.COSMIC_ANOMALY]: true,
[SETTINGS_KEYS.COSMIC_SIGNATURE]: true,
[SETTINGS_KEYS.DEPLOYABLE]: true,
[SETTINGS_KEYS.STRUCTURE]: true,
[SETTINGS_KEYS.STARBASE]: true,
[SETTINGS_KEYS.SHIP]: true,
[SETTINGS_KEYS.DRONE]: true,
[SETTINGS_KEYS.WORMHOLE]: true,
[SETTINGS_KEYS.RELIC_SITE]: true,
[SETTINGS_KEYS.DATA_SITE]: true,
[SETTINGS_KEYS.ORE_SITE]: true,
[SETTINGS_KEYS.GAS_SITE]: true,
[SETTINGS_KEYS.COMBAT_SITE]: true,
};

View File

@@ -0,0 +1,11 @@
export function getFormattedTime() {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const ms = String(now.getMilliseconds() + 1000).slice(1);
return `${hours}:${minutes}:${seconds} ${ms}`;
}

View File

@@ -1,4 +1,3 @@
export * from './useActualizeSettings';
export * from './useClipboard';
export * from './useHotkey';
export * from './usePageVisibility';

View File

@@ -1,23 +0,0 @@
import { useEffect } from 'react';
type Settings = Record<string, unknown>;
export const useActualizeSettings = <T extends Settings>(defaultVals: T, vals: T, setVals: (newVals: T) => void) => {
useEffect(() => {
let foundNew = false;
const newVals = Object.keys(defaultVals).reduce((acc, x) => {
if (Object.keys(acc).includes(x)) {
return acc;
}
foundNew = true;
// @ts-ignore
return { ...acc, [x]: defaultVals[x] };
}, vals);
if (foundNew) {
setVals(newVals);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};

View File

@@ -1,11 +1,12 @@
import { useState, useEffect } from 'react';
function usePageVisibility() {
const [isVisible, setIsVisible] = useState(!document.hidden);
const getIsVisible = () => !document.hidden;
const [isVisible, setIsVisible] = useState(getIsVisible());
useEffect(() => {
const handleVisibilityChange = () => {
setIsVisible(!document.hidden);
setIsVisible(getIsVisible());
};
document.addEventListener('visibilitychange', handleVisibilityChange);

View File

@@ -19,10 +19,24 @@ import {
} from '@/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts';
import { WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
import { DetailedKill } from '../types/kills';
import { InterfaceStoredSettings, RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { DEFAULT_ROUTES_SETTINGS, STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/constants.ts';
import {
InterfaceStoredSettings,
KillsWidgetSettings,
LocalWidgetSettings,
MapUserSettings,
OnTheMapSettingsType,
RoutesType,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { useMapUserSettings } from '@/hooks/Mapper/mapRootProvider/hooks/useMapUserSettings.ts';
import { useGlobalHooks } from '@/hooks/Mapper/mapRootProvider/hooks/useGlobalHooks.ts';
import { DEFAULT_SIGNATURE_SETTINGS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
export type MapRootData = MapUnionTypes & {
selectedSystems: string[];
@@ -36,6 +50,7 @@ export type MapRootData = MapUnionTypes & {
};
trackingCharactersData: TrackingCharacter[];
loadingPublicRoutes: boolean;
map_slug: string | null;
};
const INITIAL_DATA: MapRootData = {
@@ -70,6 +85,7 @@ const INITIAL_DATA: MapRootData = {
followingCharacterEveId: null,
pings: [],
loadingPublicRoutes: false,
map_slug: null,
};
export enum InterfaceStoredSettingsProps {
@@ -103,6 +119,19 @@ export interface MapRootContextProps {
setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>;
settingsRoutes: RoutesType;
settingsRoutesUpdate: Dispatch<SetStateAction<RoutesType>>;
settingsLocal: LocalWidgetSettings;
settingsLocalUpdate: Dispatch<SetStateAction<LocalWidgetSettings>>;
settingsSignatures: SignatureSettingsType;
settingsSignaturesUpdate: Dispatch<SetStateAction<SignatureSettingsType>>;
settingsOnTheMap: OnTheMapSettingsType;
settingsOnTheMapUpdate: Dispatch<SetStateAction<OnTheMapSettingsType>>;
settingsKills: KillsWidgetSettings;
settingsKillsUpdate: Dispatch<SetStateAction<KillsWidgetSettings>>;
isReady: boolean;
hasOldSettings: boolean;
getSettingsForExport(): string | undefined;
applySettings(settings: MapUserSettings): boolean;
checkOldSettings(): void;
};
}
@@ -134,6 +163,19 @@ const MapRootContext = createContext<MapRootContextProps>({
setInterfaceSettings: () => null,
settingsRoutes: DEFAULT_ROUTES_SETTINGS,
settingsRoutesUpdate: () => null,
settingsLocal: DEFAULT_WIDGET_LOCAL_SETTINGS,
settingsLocalUpdate: () => null,
settingsSignatures: DEFAULT_SIGNATURE_SETTINGS,
settingsSignaturesUpdate: () => null,
settingsOnTheMap: DEFAULT_ON_THE_MAP_SETTINGS,
settingsOnTheMapUpdate: () => null,
settingsKills: DEFAULT_KILLS_WIDGET_SETTINGS,
settingsKillsUpdate: () => null,
isReady: false,
hasOldSettings: false,
getSettingsForExport: () => '',
applySettings: () => false,
checkOldSettings: () => null,
},
});
@@ -154,9 +196,11 @@ const MapRootHandlers = forwardRef(({ children }: WithChildren, fwdRef: Forwarde
export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProviderProps) => {
const { update, ref } = useContextStore<MapRootData>({ ...INITIAL_DATA });
const storedSettings = useMapUserSettings();
const storedSettings = useMapUserSettings(ref);
const { windowsSettings, toggleWidgetVisibility, updateWidgetSettings, resetWidgets } =
useStoreWidgets(storedSettings);
const { windowsSettings, toggleWidgetVisibility, updateWidgetSettings, resetWidgets } = useStoreWidgets();
const comments = useComments({ outCommand });
const charactersCache = useCharactersCache({ outCommand });

View File

@@ -1,10 +1,18 @@
import {
AvailableThemes,
InterfaceStoredSettings,
KillsWidgetSettings,
LocalWidgetSettings,
MiniMapPlacement,
OnTheMapSettingsType,
PingsPlacement,
RoutesType,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
CURRENT_WINDOWS_VERSION,
DEFAULT_WIDGETS,
STORED_VISIBLE_WIDGETS_DEFAULT,
} from '@/hooks/Mapper/components/mapInterface/constants.tsx';
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
isShowMenu: false,
@@ -31,3 +39,29 @@ export const DEFAULT_ROUTES_SETTINGS: RoutesType = {
avoid_triglavian: false,
avoid: [],
};
export const DEFAULT_WIDGET_LOCAL_SETTINGS: LocalWidgetSettings = {
compact: true,
showOffline: false,
version: 0,
showShipName: false,
};
export const DEFAULT_ON_THE_MAP_SETTINGS: OnTheMapSettingsType = {
hideOffline: false,
version: 0,
};
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
showAll: false,
whOnly: true,
excludedSystems: [],
version: 2,
timeRange: 4,
};
export const getDefaultWidgetProps = () => ({
version: CURRENT_WINDOWS_VERSION,
visible: STORED_VISIBLE_WIDGETS_DEFAULT,
windows: DEFAULT_WIDGETS,
});

View File

@@ -0,0 +1,22 @@
type Settings = Record<string, unknown>;
export const actualizeSettings = <T extends Settings>(defaultVals: T, vals: T, setVals: (newVals: T) => void) => {
let foundNew = false;
const newVals = Object.keys(defaultVals).reduce((acc, key) => {
if (key in acc) {
return acc;
}
foundNew = true;
return {
...acc,
[key]: defaultVals[key],
};
}, vals);
if (foundNew) {
setVals(newVals);
}
};

View File

@@ -0,0 +1 @@
export * from './actualizeSettings';

View File

@@ -14,8 +14,8 @@ export const useCommandPings = () => {
ref.current.update({ pings });
}, []);
const pingCancelled = useCallback(({ type, solar_system_id }: CommandPingCancelled) => {
const newPings = ref.current.pings.filter(x => x.solar_system_id !== solar_system_id && x.type !== type);
const pingCancelled = useCallback(({ type, id }: CommandPingCancelled) => {
const newPings = ref.current.pings.filter(x => x.id !== id && x.type !== type);
ref.current.update({ pings: newPings });
}, []);

View File

@@ -27,6 +27,7 @@ export const useMapInit = () => {
main_character_eve_id,
following_character_eve_id,
user_hubs,
map_slug,
} = props;
const updateData: Partial<MapRootData> = {};
@@ -98,6 +99,10 @@ export const useMapInit = () => {
updateData.followingCharacterEveId = following_character_eve_id;
}
if ('map_slug' in props) {
updateData.map_slug = map_slug;
}
update(updateData);
},
[update, addSystemStatic],

View File

@@ -1,39 +1,222 @@
import useLocalStorageState from 'use-local-storage-state';
import { InterfaceStoredSettings, RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { DEFAULT_ROUTES_SETTINGS, STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { useActualizeSettings } from '@/hooks/Mapper/hooks';
import { useEffect } from 'react';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { MapUserSettings, MapUserSettingsStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { useCallback, useEffect, useRef, useState } from 'react';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures';
import { MapRootData } from '@/hooks/Mapper/mapRootProvider';
import { useSettingsValueAndSetter } from '@/hooks/Mapper/mapRootProvider/hooks/useSettingsValueAndSetter.ts';
import fastDeepEqual from 'fast-deep-equal';
export const useMigrationRoutesSettingsV1 = (update: (upd: RoutesType) => void) => {
//TODO if current Date is more than 01.01.2026 - remove this hook.
// import { actualizeSettings } from '@/hooks/Mapper/mapRootProvider/helpers';
useEffect(() => {
const items = localStorage.getItem(SESSION_KEY.routes);
if (items) {
update(JSON.parse(items));
localStorage.removeItem(SESSION_KEY.routes);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// TODO - we need provide and compare version
const createWidgetSettingsWithVersion = <T>(settings: T) => {
return {
version: 0,
settings,
};
};
export const useMapUserSettings = () => {
const [interfaceSettings, setInterfaceSettings] = useLocalStorageState<InterfaceStoredSettings>(
'window:interface:settings',
{
defaultValue: STORED_INTERFACE_DEFAULT_VALUES,
},
);
const createDefaultWidgetSettings = (): MapUserSettings => {
return {
killsWidget: createWidgetSettingsWithVersion(DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createWidgetSettingsWithVersion(DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createWidgetSettingsWithVersion(getDefaultWidgetProps()),
routes: createWidgetSettingsWithVersion(DEFAULT_ROUTES_SETTINGS),
onTheMap: createWidgetSettingsWithVersion(DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createWidgetSettingsWithVersion(DEFAULT_SIGNATURE_SETTINGS),
interface: createWidgetSettingsWithVersion(STORED_INTERFACE_DEFAULT_VALUES),
};
};
const [settingsRoutes, settingsRoutesUpdate] = useLocalStorageState<RoutesType>('window:interface:routes', {
defaultValue: DEFAULT_ROUTES_SETTINGS,
const EMPTY_OBJ = {};
export const useMapUserSettings = ({ map_slug }: MapRootData) => {
const [isReady, setIsReady] = useState(false);
const [hasOldSettings, setHasOldSettings] = useState(false);
const [mapUserSettings, setMapUserSettings] = useLocalStorageState<MapUserSettingsStructure>('map-user-settings', {
defaultValue: EMPTY_OBJ,
});
useActualizeSettings(STORED_INTERFACE_DEFAULT_VALUES, interfaceSettings, setInterfaceSettings);
useActualizeSettings(DEFAULT_ROUTES_SETTINGS, settingsRoutes, settingsRoutesUpdate);
const ref = useRef({ mapUserSettings, setMapUserSettings, map_slug });
ref.current = { mapUserSettings, setMapUserSettings, map_slug };
useMigrationRoutesSettingsV1(settingsRoutesUpdate);
useEffect(() => {
const { mapUserSettings, setMapUserSettings } = ref.current;
if (map_slug === null) {
return;
}
return { interfaceSettings, setInterfaceSettings, settingsRoutes, settingsRoutesUpdate };
if (!(map_slug in mapUserSettings)) {
setMapUserSettings({
...mapUserSettings,
[map_slug]: createDefaultWidgetSettings(),
});
}
}, [map_slug]);
const [interfaceSettings, setInterfaceSettings] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'interface',
);
const [settingsRoutes, settingsRoutesUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'routes',
);
const [settingsLocal, settingsLocalUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'localWidget',
);
const [settingsSignatures, settingsSignaturesUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'signaturesWidget',
);
const [settingsOnTheMap, settingsOnTheMapUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'onTheMap',
);
const [settingsKills, settingsKillsUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'killsWidget',
);
const [windowsSettings, setWindowsSettings] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'widgets',
);
// HERE we MUST work with migrations
useEffect(() => {
if (isReady) {
return;
}
if (map_slug === null) {
return;
}
if (mapUserSettings[map_slug] == null) {
return;
}
// TODO !!!! FROM this date 06.07.2025 - we must work only with migrations
// actualizeSettings(STORED_INTERFACE_DEFAULT_VALUES, interfaceSettings, setInterfaceSettings);
// actualizeSettings(DEFAULT_ROUTES_SETTINGS, settingsRoutes, settingsRoutesUpdate);
// actualizeSettings(DEFAULT_WIDGET_LOCAL_SETTINGS, settingsLocal, settingsLocalUpdate);
// actualizeSettings(DEFAULT_SIGNATURE_SETTINGS, settingsSignatures, settingsSignaturesUpdate);
// actualizeSettings(DEFAULT_ON_THE_MAP_SETTINGS, settingsOnTheMap, settingsOnTheMapUpdate);
// actualizeSettings(DEFAULT_KILLS_WIDGET_SETTINGS, settingsKills, settingsKillsUpdate);
setIsReady(true);
}, [
map_slug,
mapUserSettings,
interfaceSettings,
setInterfaceSettings,
settingsRoutes,
settingsRoutesUpdate,
settingsLocal,
settingsLocalUpdate,
settingsSignatures,
settingsSignaturesUpdate,
settingsOnTheMap,
settingsOnTheMapUpdate,
settingsKills,
settingsKillsUpdate,
isReady,
]);
const checkOldSettings = useCallback(() => {
const interfaceSettings = localStorage.getItem('window:interface:settings');
const widgetRoutes = localStorage.getItem('window:interface:routes');
const widgetLocal = localStorage.getItem('window:interface:local');
const widgetKills = localStorage.getItem('kills:widget:settings');
const onTheMapOld = localStorage.getItem('window:onTheMap:settings');
const widgetsOld = localStorage.getItem('windows:settings:v2');
setHasOldSettings(!!(widgetsOld || interfaceSettings || widgetRoutes || widgetLocal || widgetKills || onTheMapOld));
}, []);
useEffect(() => {
checkOldSettings();
}, [checkOldSettings]);
const getSettingsForExport = useCallback(() => {
const { map_slug } = ref.current;
if (map_slug == null) {
return;
}
return JSON.stringify(ref.current.mapUserSettings[map_slug]);
}, []);
const applySettings = useCallback((settings: MapUserSettings) => {
const { map_slug, mapUserSettings, setMapUserSettings } = ref.current;
if (map_slug == null) {
return false;
}
if (fastDeepEqual(settings, mapUserSettings[map_slug])) {
return false;
}
setMapUserSettings(old => ({
...old,
[map_slug]: settings,
}));
return true;
}, []);
return {
isReady,
hasOldSettings,
interfaceSettings,
setInterfaceSettings,
settingsRoutes,
settingsRoutesUpdate,
settingsLocal,
settingsLocalUpdate,
settingsSignatures,
settingsSignaturesUpdate,
settingsOnTheMap,
settingsOnTheMapUpdate,
settingsKills,
settingsKillsUpdate,
windowsSettings,
setWindowsSettings,
getSettingsForExport,
applySettings,
checkOldSettings,
};
};

View File

@@ -0,0 +1,60 @@
import { Dispatch, SetStateAction, useCallback, useMemo, useRef } from 'react';
import {
MapUserSettings,
MapUserSettingsStructure,
SettingsWithVersion,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
type ExtractSettings<S extends keyof MapUserSettings> =
MapUserSettings[S] extends SettingsWithVersion<infer U> ? U : never;
type Setter<S extends keyof MapUserSettings> = (
value: Partial<ExtractSettings<S>> | ((prev: ExtractSettings<S>) => Partial<ExtractSettings<S>>),
) => void;
type GenerateSettingsReturn<S extends keyof MapUserSettings> = [ExtractSettings<S>, Setter<S>];
export const useSettingsValueAndSetter = <S extends keyof MapUserSettings>(
settings: MapUserSettingsStructure,
setSettings: Dispatch<SetStateAction<MapUserSettingsStructure>>,
mapId: string | null,
setting: S,
): GenerateSettingsReturn<S> => {
const data = useMemo<ExtractSettings<S>>(() => {
if (!mapId) return {} as ExtractSettings<S>;
const mapSettings = settings[mapId];
return (mapSettings?.[setting]?.settings ?? ({} as ExtractSettings<S>)) as ExtractSettings<S>;
}, [mapId, setting, settings]);
const refData = useRef({ mapId, setting, setSettings });
refData.current = { mapId, setting, setSettings };
const setter = useCallback<Setter<S>>(value => {
const { mapId, setting, setSettings } = refData.current;
if (!mapId) return;
setSettings(all => {
const currentMap = all[mapId];
const prev = currentMap[setting].settings as ExtractSettings<S>;
const version = currentMap[setting].version;
const patch =
typeof value === 'function' ? (value as (p: ExtractSettings<S>) => Partial<ExtractSettings<S>>)(prev) : value;
return {
...all,
[mapId]: {
...currentMap,
[setting]: {
version,
settings: { ...(prev as any), ...patch } as ExtractSettings<S>,
},
},
};
});
}, []);
return [data, setter];
};

View File

@@ -1,14 +1,8 @@
import useLocalStorageState from 'use-local-storage-state';
import {
CURRENT_WINDOWS_VERSION,
DEFAULT_WIDGETS,
STORED_VISIBLE_WIDGETS_DEFAULT,
WidgetsIds,
WINDOWS_LOCAL_STORE_KEY,
} from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import { DEFAULT_WIDGETS, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
import { useCallback, useEffect, useRef } from 'react';
import { /*SNAP_GAP,*/ WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
import { Dispatch, SetStateAction, useCallback, useRef } from 'react';
import { WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
import { getDefaultWidgetProps } from '@/hooks/Mapper/mapRootProvider/constants.ts';
export type StoredWindowProps = Omit<WindowProps, 'content'>;
export type WindowStoreInfo = {
@@ -20,17 +14,12 @@ export type WindowStoreInfo = {
// export type UpdateWidgetSettingsFunc = (widgets: WindowProps[]) => void;
export type ToggleWidgetVisibility = (widgetId: WidgetsIds) => void;
export const getDefaultWidgetProps = () => ({
version: CURRENT_WINDOWS_VERSION,
visible: STORED_VISIBLE_WIDGETS_DEFAULT,
windows: DEFAULT_WIDGETS,
});
export const useStoreWidgets = () => {
const [windowsSettings, setWindowsSettings] = useLocalStorageState<WindowStoreInfo>(WINDOWS_LOCAL_STORE_KEY, {
defaultValue: getDefaultWidgetProps(),
});
interface UseStoreWidgetsProps {
windowsSettings: WindowStoreInfo;
setWindowsSettings: Dispatch<SetStateAction<WindowStoreInfo>>;
}
export const useStoreWidgets = ({ windowsSettings, setWindowsSettings }: UseStoreWidgetsProps) => {
const ref = useRef({ windowsSettings, setWindowsSettings });
ref.current = { windowsSettings, setWindowsSettings };
@@ -83,33 +72,6 @@ export const useStoreWidgets = () => {
});
}, []);
useEffect(() => {
const { setWindowsSettings } = ref.current;
const raw = localStorage.getItem(WINDOWS_LOCAL_STORE_KEY);
if (!raw) {
console.warn('No windows found in local storage!!');
setWindowsSettings(getDefaultWidgetProps());
return;
}
const { version, windows, visible, viewPort } = JSON.parse(raw) as WindowStoreInfo;
if (!version || CURRENT_WINDOWS_VERSION > version) {
setWindowsSettings(getDefaultWidgetProps());
}
// eslint-disable-next-line no-debugger
const out = windows.filter(x => DEFAULT_WIDGETS.find(def => def.id === x.id));
setWindowsSettings({
version: CURRENT_WINDOWS_VERSION,
windows: out as WindowProps[],
visible,
viewPort,
});
}, []);
const resetWidgets = useCallback(() => ref.current.setWindowsSettings(getDefaultWidgetProps()), []);
return {

View File

@@ -1,3 +1,6 @@
import { WindowStoreInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
export enum AvailableThemes {
default = 'default',
pathfinder = 'pathfinder',
@@ -43,3 +46,42 @@ export type RoutesType = {
avoid_triglavian: boolean;
avoid: number[];
};
export type LocalWidgetSettings = {
compact: boolean;
showOffline: boolean;
version: number;
showShipName: boolean;
};
export type OnTheMapSettingsType = {
hideOffline: boolean;
version: number;
};
export type KillsWidgetSettings = {
showAll: boolean;
whOnly: boolean;
excludedSystems: number[];
version: number;
timeRange: number;
};
export type SettingsWithVersion<T> = {
version: number;
settings: T;
};
export type MapUserSettings = {
widgets: SettingsWithVersion<WindowStoreInfo>;
interface: SettingsWithVersion<InterfaceStoredSettings>;
onTheMap: SettingsWithVersion<OnTheMapSettingsType>;
routes: SettingsWithVersion<RoutesType>;
localWidget: SettingsWithVersion<LocalWidgetSettings>;
signaturesWidget: SettingsWithVersion<SignatureSettingsType>;
killsWidget: SettingsWithVersion<KillsWidgetSettings>;
};
export type MapUserSettingsStructure = {
[mapId: string]: MapUserSettings;
};

View File

@@ -97,6 +97,7 @@ export type CommandInit = {
is_subscription_active?: boolean;
main_character_eve_id?: string | null;
following_character_eve_id?: string | null;
map_slug?: string;
};
export type CommandAddSystems = SolarSystemRawType[];
@@ -150,7 +151,7 @@ export type CommandUpdateTracking = {
follow: boolean;
};
export type CommandPingAdded = PingData[];
export type CommandPingCancelled = Pick<PingData, 'type' | 'solar_system_id'>;
export type CommandPingCancelled = Pick<PingData, 'type' | 'id'>;
export interface UserSettings {
primaryCharacterId?: string;

View File

@@ -4,6 +4,7 @@ export enum PingType {
}
export type PingData = {
id: string;
inserted_at: number;
character_eve_id: string;
solar_system_id: string;

View File

@@ -1,27 +1,121 @@
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
import { RefObject, useCallback } from 'react';
import { RefObject, useCallback, useEffect, useRef } from 'react';
import debounce from 'lodash.debounce';
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
// const inIndex = 0;
// const prevEventTime = +new Date();
const LAST_VERSION_KEY = 'wandererLastVersion';
// @ts-ignore
export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRef: RefObject<any>) => {
const visible = usePageVisibility();
const wasHiddenOnce = useRef(false);
const visibleRef = useRef(visible);
visibleRef.current = visible;
// TODO - do not delete THIS code it needs for debug
// const [record, setRecord] = useLocalStorageState<boolean>('record', {
// defaultValue: false,
// });
// const [recordsList, setRecordsList] = useLocalStorageState<{ type; data }[]>('recordsList', {
// defaultValue: [],
// });
//
// const ref = useRef({ record, setRecord, recordsList, setRecordsList });
// ref.current = { record, setRecord, recordsList, setRecordsList };
//
// const recordBufferRef = useRef<{ type; data }[]>([]);
// useEffect(() => {
// if (record || recordBufferRef.current.length === 0) {
// return;
// }
//
// ref.current.setRecordsList([...recordBufferRef.current]);
// recordBufferRef.current = [];
// }, [record]);
const handleCommand = useCallback(
// @ts-ignore
async ({ type, data }) => {
if (!hooksRef.current) {
return;
}
// TODO - do not delete THIS code it needs for debug
// console.log('JOipP', `OUT`, ref.current.record, { type, data });
// if (ref.current.record) {
// recordBufferRef.current.push({ type, data });
// }
// 'ui_loaded'
return await hooksRef.current.pushEventAsync(type, data);
},
[hooksRef.current],
);
const handleMapEvent = useCallback(({ type, body }) => {
handlerRefs.forEach(ref => {
if (!ref.current) {
// @ts-ignore
const eventsBufferRef = useRef<{ type; body }[]>([]);
const eventTick = useCallback(
debounce(() => {
if (eventsBufferRef.current.length === 0) {
return;
}
ref.current?.command(type, body);
});
const { type, body } = eventsBufferRef.current.shift()!;
handlerRefs.forEach(ref => {
if (!ref.current) {
return;
}
ref.current?.command(type, body);
});
// TODO - do not delete THIS code it needs for debug
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
if (eventsBufferRef.current.length > 0) {
eventTick();
}
}, 10),
[],
);
const eventTickRef = useRef(eventTick);
eventTickRef.current = eventTick;
// @ts-ignore
const handleMapEvent = useCallback(({ type, body }) => {
// TODO - do not delete THIS code it needs for debug
// const currentTime = +new Date();
// const timeDiff = currentTime - prevEventTime;
// prevEventTime = currentTime;
// console.log('JOipP', `IN [${inIndex++}] [${timeDiff}] ${getFormattedTime()}`, { type, body });
if (!eventTickRef.current || !visibleRef.current) {
return;
}
eventsBufferRef.current.push({ type, body });
eventTickRef.current();
}, []);
useEffect(() => {
if (!visible && !wasHiddenOnce.current) {
wasHiddenOnce.current = true;
return;
}
if (!wasHiddenOnce.current) {
return;
}
if (!visible) {
return;
}
hooksRef.current.pushEventAsync('ui_loaded', { version: localStorage.getItem(LAST_VERSION_KEY) });
}, [hooksRef.current, visible]);
return { handleCommand, handleMapEvent };
};

View File

@@ -1,2 +1,4 @@
export * from './contextStore';
export * from './getQueryVariable';
export * from './loadTextFile';
export * from './saveToFile';

View File

@@ -0,0 +1,27 @@
export function loadTextFile(): Promise<string> {
return new Promise((resolve, reject) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json,.json';
input.onchange = () => {
const file = input.files?.[0];
if (!file) {
reject(new Error('No file selected'));
return;
}
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = () => {
reject(reader.error);
};
reader.readAsText(file);
};
input.click();
});
}

View File

@@ -0,0 +1,33 @@
export function saveTextFile(filename: string, content: string) {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
export async function saveTextFileInteractive(filename: string, content: string) {
if (!('showSaveFilePicker' in window)) {
throw new Error('File System Access API is not supported in this browser.');
}
const handle = await (window as any).showSaveFilePicker({
suggestedName: filename,
types: [
{
description: 'Text Files',
accept: { 'text/plain': ['.txt', '.json'] },
},
],
});
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
}

View File

@@ -6,7 +6,7 @@
"scripts": {
"build": "vite build --emptyOutDir false",
"watch": "vite build --watch --minify false --emptyOutDir false --clearScreen true --mode development",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "jest"
},
"engines": {
"node": ">= 18.0.0"
@@ -50,6 +50,7 @@
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.13",
"@types/jest": "^29.5.12",
"@types/lodash.debounce": "^4.0.9",
"@types/lodash.isequal": "^4.5.8",
"@types/react": "^18.3.12",
@@ -59,6 +60,7 @@
"@vitejs/plugin-react": "^4.3.3",
"@vitejs/plugin-react-refresh": "^1.3.6",
"autoprefixer": "^10.4.19",
"babel-jest": "^29.7.0",
"child_process": "^1.0.2",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
@@ -67,6 +69,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"heroicons": "^2.0.18",
"jest": "^29.7.0",
"merge-options": "^3.0.4",
"postcss": "^8.4.38",
"postcss-cli": "^11.0.0",
@@ -74,6 +77,7 @@
"prettier": "^3.2.5",
"sass": "^1.77.2",
"sass-loader": "^14.2.1",
"ts-jest": "^29.1.2",
"typescript": "^5.2.2",
"vite": "^5.0.5",
"vite-plugin-cdn-import": "^1.0.1"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@@ -27,7 +27,11 @@ config :wanderer_app,
generators: [timestamp_type: :utc_datetime],
ddrt: DDRT,
logger: Logger,
pubsub_client: Phoenix.PubSub
pubsub_client: Phoenix.PubSub,
wanderer_kills_base_url:
System.get_env("WANDERER_KILLS_BASE_URL", "ws://host.docker.internal:4004"),
wanderer_kills_service_enabled:
System.get_env("WANDERER_KILLS_SERVICE_ENABLED", "false") == "true"
config :wanderer_app, WandererAppWeb.Endpoint,
adapter: Bandit.PhoenixAdapter,

View File

@@ -84,3 +84,12 @@ config :swoosh, :api_client, false
config :logger, :console,
level: :info,
format: "$time $metadata[$level] $message\n"
# WandererKills service configuration (WebSocket-based)
config :wanderer_app,
# Enable WandererKills service integration
wanderer_kills_service_enabled: true,
# WebSocket connection URL for WandererKills service
wanderer_kills_base_url:
System.get_env("WANDERER_KILLS_BASE_URL", "ws://host.docker.internal:4004")

275
config/quality_gates.exs Normal file
View File

@@ -0,0 +1,275 @@
# Quality Gates Configuration
#
# This file defines the error budget thresholds for the project.
# These are intentionally set high initially to avoid blocking development
# while we work on improving code quality.
defmodule WandererApp.QualityGates do
@moduledoc """
Central configuration for all quality gate thresholds.
## Error Budget Philosophy
We use error budgets to:
1. Allow gradual improvement of code quality
2. Avoid blocking development on legacy issues
3. Provide clear targets for improvement
4. Track progress over time
## Threshold Levels
- **Current**: What we enforce today (relaxed)
- **Target**: Where we want to be (strict)
- **Timeline**: When we plan to tighten the thresholds
"""
@doc """
Returns the current error budget configuration.
"""
def current_thresholds do
%{
# Compilation warnings
compilation: %{
# Increased from 100 to accommodate current state
max_warnings: 500,
target: 0,
# Extended timeline
timeline: "Q3 2025",
description: "Allow existing warnings while we fix them gradually"
},
# Credo code quality issues
credo: %{
# Increased from 50 to accommodate current state
max_issues: 200,
# Increased from 10
max_high_priority: 50,
target_issues: 10,
target_high_priority: 0,
# Extended timeline
timeline: "Q2 2025",
description: "Focus on high-priority issues first"
},
# Dialyzer static analysis
dialyzer: %{
# Allow some errors for now (was 0)
max_errors: 20,
max_warnings: :unlimited,
target_errors: 0,
target_warnings: 0,
# Extended timeline
timeline: "Q4 2025",
description: "Temporarily allow some errors during codebase improvement"
},
# Test coverage
coverage: %{
# Reduced from 70% to accommodate current state
minimum: 50,
target: 90,
# Extended timeline
timeline: "Q3 2025",
description: "Start with 50% coverage, gradually improve to 90%"
},
# Test execution
tests: %{
# Increased from 10 to accommodate current state
max_failures: 50,
# 10% flaky tests allowed (increased)
max_flaky_rate: 0.10,
# 10 minutes (increased from 5)
max_duration_seconds: 600,
target_failures: 0,
# 5 minutes
target_duration_seconds: 300,
# Extended timeline
timeline: "Q2 2025",
description: "Allow more test failures during stabilization phase"
},
# Code formatting
formatting: %{
enforced: true,
auto_fix_in_ci: false,
description: "Strict formatting enforcement from day one"
},
# Documentation
documentation: %{
# 50% of modules documented
min_module_doc_coverage: 0.5,
# 30% of public functions documented
min_function_doc_coverage: 0.3,
target_module_coverage: 0.9,
target_function_coverage: 0.8,
timeline: "Q3 2025",
description: "Gradually improve documentation coverage"
},
# Security
security: %{
sobelow_enabled: false,
max_high_risk: 0,
max_medium_risk: 5,
target_enabled: true,
timeline: "Q2 2025",
description: "Security scanning to be enabled after initial cleanup"
},
# Dependencies
dependencies: %{
max_outdated_major: 10,
max_outdated_minor: 20,
max_vulnerable: 0,
audit_enabled: true,
description: "Keep dependencies reasonably up to date"
},
# Performance
performance: %{
max_slow_tests_seconds: 5,
max_memory_usage_mb: 500,
profiling_enabled: false,
timeline: "Q4 2025",
description: "Performance monitoring to be added later"
}
}
end
@doc """
Returns the configuration for GitHub Actions.
"""
def github_actions_config do
thresholds = current_thresholds()
%{
compilation_warnings: thresholds.compilation.max_warnings,
credo_issues: thresholds.credo.max_issues,
dialyzer_errors: thresholds.dialyzer.max_errors,
coverage_minimum: thresholds.coverage.minimum,
test_max_failures: thresholds.tests.max_failures,
test_timeout_minutes: div(thresholds.tests.max_duration_seconds, 60)
}
end
@doc """
Returns the configuration for mix check.
"""
def mix_check_config do
thresholds = current_thresholds()
[
# Compiler with warnings allowed
{:compiler, "mix compile --warnings-as-errors false"},
# Credo with issue budget
{:credo, "mix credo --strict --max-issues #{thresholds.credo.max_issues}"},
# Dialyzer without halt on warnings
{:dialyzer, "mix dialyzer", exit_status: 0},
# Tests with failure allowance
{:ex_unit, "mix test --max-failures #{thresholds.tests.max_failures}"},
# Formatting is strict
{:formatter, "mix format --check-formatted"},
# Coverage check
{:coverage, "mix coveralls --minimum-coverage #{thresholds.coverage.minimum}"},
# Documentation coverage (optional for now)
{:docs_coverage, false},
# Security scanning (disabled for now)
{:sobelow, false},
# Dependency audit
{:audit, "mix deps.audit", exit_status: 0},
# Doctor check (disabled)
{:doctor, false}
]
end
@doc """
Generates a quality report showing current vs target thresholds.
"""
def quality_report do
thresholds = current_thresholds()
"""
# WandererApp Quality Gates Report
Generated: #{DateTime.utc_now() |> DateTime.to_string()}
## Current Error Budgets vs Targets
| Category | Current Budget | Target Goal | Timeline | Status |
|----------|----------------|-------------|----------|--------|
| Compilation Warnings | ≤#{thresholds.compilation.max_warnings} | #{thresholds.compilation.target} | #{thresholds.compilation.timeline} | 🟡 Relaxed |
| Credo Issues | ≤#{thresholds.credo.max_issues} | #{thresholds.credo.target_issues} | #{thresholds.credo.timeline} | 🟡 Relaxed |
| Dialyzer Errors | ≤#{thresholds.dialyzer.max_errors} | #{thresholds.dialyzer.target_errors} | #{thresholds.dialyzer.timeline} | 🟡 Relaxed |
| Test Coverage | ≥#{thresholds.coverage.minimum}% | #{thresholds.coverage.target}% | #{thresholds.coverage.timeline} | 🟡 Relaxed |
| Test Failures | ≤#{thresholds.tests.max_failures} | #{thresholds.tests.target_failures} | #{thresholds.tests.timeline} | 🟡 Relaxed |
| Code Formatting | Required | Required | - | ✅ Strict |
## Improvement Roadmap
### Q1 2025
- Reduce Credo issues from #{thresholds.credo.max_issues} to #{thresholds.credo.target_issues}
- Achieve zero test failures
- Reduce test execution time to under 3 minutes
### Q2 2025
- Eliminate all compilation warnings
- Increase test coverage to #{thresholds.coverage.target}%
- Enable security scanning with Sobelow
### Q3 2025
- Clean up all Dialyzer warnings
- Achieve 90% documentation coverage
### Q4 2025
- Implement performance monitoring
- Add memory usage tracking
## Quick Commands
```bash
# Check current quality status
mix check
# Run with auto-fix where possible
mix check --fix
# Generate detailed quality report
mix quality.report
# Check specific category
mix credo --strict
mix test --cover
mix dialyzer
```
"""
end
@doc """
Checks if a metric passes the current threshold.
"""
def passes_threshold?(category, metric, value) do
thresholds = current_thresholds()
case {category, metric} do
{:compilation, :warnings} -> value <= thresholds.compilation.max_warnings
{:credo, :issues} -> value <= thresholds.credo.max_issues
{:credo, :high_priority} -> value <= thresholds.credo.max_high_priority
{:dialyzer, :errors} -> value <= thresholds.dialyzer.max_errors
{:coverage, :percentage} -> value >= thresholds.coverage.minimum
{:tests, :failures} -> value <= thresholds.tests.max_failures
{:tests, :duration} -> value <= thresholds.tests.max_duration_seconds
_ -> true
end
end
end

View File

@@ -53,6 +53,20 @@ public_api_disabled =
|> get_var_from_path_or_env("WANDERER_PUBLIC_API_DISABLED", "false")
|> String.to_existing_atom()
character_api_disabled =
config_dir
|> get_var_from_path_or_env("WANDERER_CHARACTER_API_DISABLED", "true")
|> String.to_existing_atom()
wanderer_kills_service_enabled =
config_dir
|> get_var_from_path_or_env("WANDERER_KILLS_SERVICE_ENABLED", "false")
|> String.to_existing_atom()
wanderer_kills_base_url =
config_dir
|> get_var_from_path_or_env("WANDERER_KILLS_BASE_URL", "ws://wanderer-kills:4004")
map_subscriptions_enabled =
config_dir
|> get_var_from_path_or_env("WANDERER_MAP_SUBSCRIPTIONS_ENABLED", "false")
@@ -70,9 +84,9 @@ map_subscription_base_price =
config_dir
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_BASE_PRICE", 100_000_000)
map_subscription_extra_characters_100_price =
map_subscription_extra_characters_50_price =
config_dir
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_CHARACTERS_100_PRICE", 50_000_000)
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_CHARACTERS_50_PRICE", 50_000_000)
map_subscription_extra_hubs_10_price =
config_dir
@@ -117,11 +131,17 @@ config :wanderer_app,
admins: admins,
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
corp_wallet_eve_id: System.get_env("WANDERER_CORP_WALLET_EVE_ID", "-1"),
public_api_disabled: public_api_disabled,
character_api_disabled:
System.get_env("WANDERER_CHARACTER_API_DISABLED", "true") |> String.to_existing_atom(),
zkill_preload_disabled:
System.get_env("WANDERER_ZKILL_PRELOAD_DISABLED", "false") |> String.to_existing_atom(),
active_tracking_pool: System.get_env("WANDERER_ACTIVE_TRACKING_POOL", "default"),
tracking_pool_max_size:
System.get_env("WANDERER_TRACKING_POOL_MAX_SIZE", "300") |> String.to_integer(),
character_tracking_pause_disabled:
System.get_env("WANDERER_CHARACTER_TRACKING_PAUSE_DISABLED", "true")
|> String.to_existing_atom(),
character_api_disabled: character_api_disabled,
wanderer_kills_service_enabled: wanderer_kills_service_enabled,
wanderer_kills_base_url: wanderer_kills_base_url,
map_subscriptions_enabled: map_subscriptions_enabled,
map_connection_auto_expire_hours: map_connection_auto_expire_hours,
map_connection_auto_eol_hours: map_connection_auto_eol_hours,
@@ -147,7 +167,7 @@ config :wanderer_app,
month_12_discount: 0.5
}
],
extra_characters_100: map_subscription_extra_characters_100_price,
extra_characters_50: map_subscription_extra_characters_50_price,
extra_hubs_10: map_subscription_extra_hubs_10_price
}
@@ -170,11 +190,31 @@ config :ueberauth, WandererApp.Ueberauth.Strategy.Eve.OAuth,
client_id: {WandererApp.Ueberauth, :client_id},
client_secret: {WandererApp.Ueberauth, :client_secret},
client_id_default: System.get_env("EVE_CLIENT_ID", "<EVE_CLIENT_ID>"),
client_id_1: System.get_env("EVE_CLIENT_ID_1", ""),
client_id_2: System.get_env("EVE_CLIENT_ID_2", ""),
client_id_3: System.get_env("EVE_CLIENT_ID_3", ""),
client_id_4: System.get_env("EVE_CLIENT_ID_4", ""),
client_id_5: System.get_env("EVE_CLIENT_ID_5", ""),
client_id_6: System.get_env("EVE_CLIENT_ID_6", ""),
client_id_7: System.get_env("EVE_CLIENT_ID_7", ""),
client_id_8: System.get_env("EVE_CLIENT_ID_8", ""),
client_id_9: System.get_env("EVE_CLIENT_ID_9", ""),
client_id_10: System.get_env("EVE_CLIENT_ID_10", ""),
client_id_with_wallet:
System.get_env("EVE_CLIENT_WITH_WALLET_ID", "<EVE_CLIENT_WITH_WALLET_ID>"),
client_id_with_corp_wallet:
System.get_env("EVE_CLIENT_WITH_CORP_WALLET_ID", "<EVE_CLIENT_WITH_CORP_WALLET_ID>"),
client_secret_default: System.get_env("EVE_CLIENT_SECRET", "<EVE_CLIENT_SECRET>"),
client_secret_1: System.get_env("EVE_CLIENT_SECRET_1", ""),
client_secret_2: System.get_env("EVE_CLIENT_SECRET_2", ""),
client_secret_3: System.get_env("EVE_CLIENT_SECRET_3", ""),
client_secret_4: System.get_env("EVE_CLIENT_SECRET_4", ""),
client_secret_5: System.get_env("EVE_CLIENT_SECRET_5", ""),
client_secret_6: System.get_env("EVE_CLIENT_SECRET_6", ""),
client_secret_7: System.get_env("EVE_CLIENT_SECRET_7", ""),
client_secret_8: System.get_env("EVE_CLIENT_SECRET_8", ""),
client_secret_9: System.get_env("EVE_CLIENT_SECRET_9", ""),
client_secret_10: System.get_env("EVE_CLIENT_SECRET_10", ""),
client_secret_with_wallet:
System.get_env("EVE_CLIENT_WITH_WALLET_SECRET", "<EVE_CLIENT_WITH_WALLET_SECRET>"),
client_secret_with_corp_wallet:
@@ -350,3 +390,26 @@ end
config :wanderer_app, :license_manager,
api_url: System.get_env("LM_API_URL", "http://localhost:4000"),
auth_key: System.get_env("LM_AUTH_KEY")
# SSE Configuration
config :wanderer_app, :sse,
enabled:
config_dir
|> get_var_from_path_or_env("WANDERER_SSE_ENABLED", "true")
|> String.to_existing_atom(),
max_connections_total:
config_dir |> get_int_from_path_or_env("WANDERER_SSE_MAX_CONNECTIONS", 1000),
max_connections_per_map:
config_dir |> get_int_from_path_or_env("SSE_MAX_CONNECTIONS_PER_MAP", 50),
max_connections_per_api_key:
config_dir |> get_int_from_path_or_env("SSE_MAX_CONNECTIONS_PER_API_KEY", 10),
keepalive_interval: config_dir |> get_int_from_path_or_env("SSE_KEEPALIVE_INTERVAL", 30000),
connection_timeout: config_dir |> get_int_from_path_or_env("SSE_CONNECTION_TIMEOUT", 300_000)
# External Events Configuration
config :wanderer_app, :external_events,
webhooks_enabled:
config_dir
|> get_var_from_path_or_env("WANDERER_WEBHOOKS_ENABLED", "true")
|> String.to_existing_atom(),
webhook_timeout_ms: config_dir |> get_int_from_path_or_env("WANDERER_WEBHOOK_TIMEOUT_MS", 15000)

View File

@@ -8,15 +8,23 @@ import Config
config :wanderer_app, WandererApp.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
hostname: System.get_env("DB_HOST", "localhost"),
database: "wanderer_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10
pool_size: 20,
ownership_timeout: 60_000,
timeout: 60_000
# Set environment variable before config runs to ensure character API is enabled in tests
System.put_env("WANDERER_CHARACTER_API_DISABLED", "false")
config :wanderer_app,
ddrt: Test.DDRTMock,
logger: Test.LoggerMock,
pubsub_client: Test.PubSubMock
pubsub_client: Test.PubSubMock,
cached_info: WandererApp.CachedInfo.Mock,
character_api_disabled: false,
environment: :test
# We don't run a server during test. If one is required,
# you can enable the server option below.
@@ -36,3 +44,8 @@ config :logger, level: :warning
# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime
# Configure MIME types for testing, including XML for error response contract tests
config :mime, :types, %{
"application/xml" => ["xml"]
}

25
coveralls.json Normal file
View File

@@ -0,0 +1,25 @@
{
"coverage_options": {
"treat_no_relevant_lines_as_covered": true,
"output_dir": "cover/",
"template_path": "cover/coverage.html.eex",
"minimum_coverage": 70
},
"terminal_options": {
"file_column_width": 40
},
"html_options": {
"output_dir": "cover/"
},
"skip_files": [
"test/",
"lib/wanderer_app_web.ex",
"lib/wanderer_app.ex",
"lib/wanderer_app/application.ex",
"lib/wanderer_app/release.ex",
"lib/wanderer_app_web/endpoint.ex",
"lib/wanderer_app_web/telemetry.ex",
"lib/wanderer_app_web/gettext.ex",
"priv/"
]
}

126
lib/mix/tasks/test.setup.ex Normal file
View File

@@ -0,0 +1,126 @@
defmodule Mix.Tasks.Test.Setup do
@moduledoc """
Sets up the test database environment.
This task will:
- Create the test database if it doesn't exist
- Run all migrations
- Verify the setup is correct
## Usage
mix test.setup
## Options
--force Drop the existing test database and recreate it
--quiet Reduce output verbosity
--seed Seed the database with test fixtures after setup
## Examples
mix test.setup
mix test.setup --force
mix test.setup --seed
mix test.setup --force --seed --quiet
"""
use Mix.Task
alias WandererApp.DatabaseSetup
@shortdoc "Sets up the test database environment"
@impl Mix.Task
def run(args) do
# Parse options
{opts, _} =
OptionParser.parse!(args,
strict: [force: :boolean, quiet: :boolean, seed: :boolean],
aliases: [f: :force, q: :quiet, s: :seed]
)
# Configure logger level based on quiet option
if opts[:quiet] do
Logger.configure(level: :warning)
else
Logger.configure(level: :info)
end
# Set the environment to test
Mix.env(:test)
try do
# Load the application configuration
Mix.Task.run("loadconfig")
# Start the application
{:ok, _} = Application.ensure_all_started(:wanderer_app)
if opts[:force] do
Mix.shell().info("🔄 Forcing database recreation...")
_ = DatabaseSetup.drop_database()
end
case DatabaseSetup.setup_test_database() do
:ok ->
if opts[:seed] do
Mix.shell().info("🌱 Seeding test data...")
case DatabaseSetup.seed_test_data() do
:ok ->
Mix.shell().info("✅ Test database setup and seeding completed successfully!")
{:error, reason} ->
Mix.shell().error("❌ Test data seeding failed: #{inspect(reason)}")
System.halt(1)
end
else
Mix.shell().info("✅ Test database setup completed successfully!")
end
{:error, reason} ->
Mix.shell().error("❌ Test database setup failed: #{inspect(reason)}")
print_troubleshooting_help()
System.halt(1)
end
rescue
error ->
Mix.shell().error("❌ Unexpected error during database setup: #{inspect(error)}")
print_troubleshooting_help()
System.halt(1)
end
end
defp print_troubleshooting_help do
Mix.shell().info("""
🔧 Troubleshooting Tips:
1. Ensure PostgreSQL is running:
• On macOS: brew services start postgresql
• On Ubuntu: sudo service postgresql start
• Using Docker: docker run --name postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres
2. Check database configuration in config/test.exs:
• Username: postgres
• Password: postgres
• Host: localhost
• Port: 5432
3. Verify database permissions:
• Ensure the postgres user can create databases
• Try connecting manually: psql -U postgres -h localhost
4. For connection refused errors:
• Check if PostgreSQL is listening on the correct port
• Verify firewall settings
5. Force recreation if corrupted:
• Run: mix test.setup --force
📚 For more help, see: https://hexdocs.pm/ecto/Ecto.Adapters.Postgres.html
""")
end
end

View File

@@ -0,0 +1,331 @@
defmodule Mix.Tasks.Test.Stability do
@moduledoc """
Runs tests multiple times to detect flaky tests.
## Usage
mix test.stability
mix test.stability --runs 10
mix test.stability --runs 5 --file test/specific_test.exs
mix test.stability --tag flaky
mix test.stability --detect --threshold 0.95
## Options
* `--runs` - Number of times to run tests (default: 5)
* `--file` - Specific test file to check
* `--tag` - Only run tests with specific tag
* `--detect` - Detection mode, identifies flaky tests
* `--threshold` - Success rate threshold for detection (default: 0.95)
* `--parallel` - Run iterations in parallel
* `--report` - Generate detailed report file
"""
use Mix.Task
@shortdoc "Detect flaky tests by running them multiple times"
@default_runs 5
@default_threshold 0.95
def run(args) do
{opts, test_args, _} =
OptionParser.parse(args,
switches: [
runs: :integer,
file: :string,
tag: :string,
detect: :boolean,
threshold: :float,
parallel: :boolean,
report: :string
],
aliases: [
r: :runs,
f: :file,
t: :tag,
d: :detect,
p: :parallel
]
)
runs = Keyword.get(opts, :runs, @default_runs)
threshold = Keyword.get(opts, :threshold, @default_threshold)
detect_mode = Keyword.get(opts, :detect, false)
parallel = Keyword.get(opts, :parallel, false)
report_file = Keyword.get(opts, :report)
Mix.shell().info("🔍 Running test stability check...")
Mix.shell().info(" Iterations: #{runs}")
Mix.shell().info(" Threshold: #{Float.round(threshold * 100, 1)}%")
Mix.shell().info("")
# Build test command
test_cmd = build_test_command(opts, test_args)
# Run tests multiple times
results =
if parallel do
run_tests_parallel(test_cmd, runs)
else
run_tests_sequential(test_cmd, runs)
end
# Analyze results
analysis = analyze_results(results, threshold)
# Display results
display_results(analysis, detect_mode)
# Generate report if requested
if report_file do
generate_report(analysis, report_file)
end
# Exit with appropriate code
if analysis.flaky_count > 0 and detect_mode do
Mix.shell().error("\n❌ Found #{analysis.flaky_count} flaky tests!")
exit({:shutdown, 1})
else
Mix.shell().info("\n✅ Test stability check complete")
end
end
defp build_test_command(opts, test_args) do
cmd_parts = ["test"]
cmd_parts =
if file = Keyword.get(opts, :file) do
cmd_parts ++ [file]
else
cmd_parts
end
cmd_parts =
if tag = Keyword.get(opts, :tag) do
cmd_parts ++ ["--only", tag]
else
cmd_parts
end
cmd_parts ++ test_args
end
defp run_tests_sequential(test_cmd, runs) do
for i <- 1..runs do
Mix.shell().info("Running iteration #{i}/#{runs}...")
start_time = System.monotonic_time(:millisecond)
# Capture test output
{output, exit_code} =
System.cmd("mix", test_cmd,
stderr_to_stdout: true,
env: [{"MIX_ENV", "test"}]
)
duration = System.monotonic_time(:millisecond) - start_time
# Parse test results
test_results = parse_test_output(output)
%{
iteration: i,
exit_code: exit_code,
duration: duration,
output: output,
tests: test_results.tests,
failures: test_results.failures,
failed_tests: test_results.failed_tests
}
end
end
defp run_tests_parallel(test_cmd, runs) do
Mix.shell().info("Running #{runs} iterations in parallel...")
tasks =
for i <- 1..runs do
Task.async(fn ->
start_time = System.monotonic_time(:millisecond)
{output, exit_code} =
System.cmd("mix", test_cmd,
stderr_to_stdout: true,
env: [{"MIX_ENV", "test"}]
)
duration = System.monotonic_time(:millisecond) - start_time
test_results = parse_test_output(output)
%{
iteration: i,
exit_code: exit_code,
duration: duration,
output: output,
tests: test_results.tests,
failures: test_results.failures,
failed_tests: test_results.failed_tests
}
end)
end
Task.await_many(tasks, :infinity)
end
defp parse_test_output(output) do
lines = String.split(output, "\n")
# Extract test count and failures
test_summary = Enum.find(lines, &String.contains?(&1, "test"))
{tests, failures} =
case Regex.run(~r/(\d+) tests?, (\d+) failures?/, test_summary || "") do
[_, tests, failures] ->
{String.to_integer(tests), String.to_integer(failures)}
_ ->
{0, 0}
end
# Extract failed test names
failed_tests = extract_failed_tests(output)
%{
tests: tests,
failures: failures,
failed_tests: failed_tests
}
end
defp extract_failed_tests(output) do
output
|> String.split("\n")
# More precise filtering for actual test failures
|> Enum.filter(
&(String.contains?(&1, "test ") and
(String.contains?(&1, "FAILED") or String.contains?(&1, "ERROR") or
Regex.match?(~r/^\s*\d+\)\s+test/, &1)))
)
|> Enum.map(&extract_test_name/1)
|> Enum.reject(&is_nil/1)
end
defp extract_test_name(line) do
case Regex.run(~r/test (.+) \((.+)\)/, line) do
[_, name, module] -> "#{module}: #{name}"
_ -> nil
end
end
defp analyze_results(results, threshold) do
total_runs = length(results)
# Group failures by test name
all_failures =
results
|> Enum.flat_map(& &1.failed_tests)
|> Enum.frequencies()
# Identify flaky tests
flaky_tests =
all_failures
|> Enum.filter(fn {_test, fail_count} ->
success_rate = (total_runs - fail_count) / total_runs
success_rate < threshold and success_rate > 0
end)
|> Enum.map(fn {test, fail_count} ->
success_rate = (total_runs - fail_count) / total_runs
%{
test: test,
failures: fail_count,
success_rate: success_rate,
failure_rate: fail_count / total_runs
}
end)
|> Enum.sort_by(& &1.failure_rate, :desc)
# Calculate statistics
total_tests = results |> Enum.map(& &1.tests) |> Enum.max(fn -> 0 end)
avg_duration = results |> Enum.map(& &1.duration) |> average()
success_runs = Enum.count(results, &(&1.exit_code == 0))
%{
total_runs: total_runs,
total_tests: total_tests,
success_runs: success_runs,
failed_runs: total_runs - success_runs,
success_rate: success_runs / total_runs,
avg_duration: avg_duration,
flaky_tests: flaky_tests,
flaky_count: length(flaky_tests),
all_failures: all_failures
}
end
defp average([]), do: 0
defp average(list), do: Enum.sum(list) / length(list)
defp display_results(analysis, detect_mode) do
Mix.shell().info("\n📊 Test Stability Results")
Mix.shell().info("=" |> String.duplicate(50))
Mix.shell().info("\nSummary:")
Mix.shell().info(" Total test runs: #{analysis.total_runs}")
Mix.shell().info(" Successful runs: #{analysis.success_runs}")
Mix.shell().info(" Failed runs: #{analysis.failed_runs}")
Mix.shell().info(" Overall success rate: #{format_percentage(analysis.success_rate)}")
Mix.shell().info(" Average duration: #{Float.round(analysis.avg_duration / 1000, 2)}s")
if analysis.flaky_count > 0 do
Mix.shell().info("\n⚠️ Flaky Tests Detected:")
Mix.shell().info("-" |> String.duplicate(50))
for test <- analysis.flaky_tests do
Mix.shell().info("\n #{test.test}")
Mix.shell().info(" Failure rate: #{format_percentage(test.failure_rate)}")
Mix.shell().info(" Failed #{test.failures} out of #{analysis.total_runs} runs")
end
else
Mix.shell().info("\n✅ No flaky tests detected!")
end
if not detect_mode and map_size(analysis.all_failures) > 0 do
Mix.shell().info("\n📝 All Test Failures:")
Mix.shell().info("-" |> String.duplicate(50))
for {test, count} <- analysis.all_failures do
percentage = count / analysis.total_runs
Mix.shell().info(" #{test}: #{count} failures (#{format_percentage(percentage)})")
end
end
end
defp format_percentage(rate) do
"#{Float.round(rate * 100, 1)}%"
end
defp generate_report(analysis, report_file) do
timestamp = DateTime.utc_now() |> DateTime.to_string()
report = %{
timestamp: timestamp,
summary: %{
total_runs: analysis.total_runs,
total_tests: analysis.total_tests,
success_runs: analysis.success_runs,
failed_runs: analysis.failed_runs,
success_rate: analysis.success_rate,
avg_duration_ms: analysis.avg_duration
},
flaky_tests: analysis.flaky_tests,
all_failures: analysis.all_failures
}
json = Jason.encode!(report, pretty: true)
File.write!(report_file, json)
Mix.shell().info("\n📄 Report written to: #{report_file}")
end
end

View File

@@ -1,7 +1,13 @@
defmodule WandererApp.Api do
@moduledoc false
use Ash.Domain
use Ash.Domain,
extensions: [AshJsonApi.Domain]
json_api do
prefix "/api/v1"
log_errors?(true)
end
resources do
resource WandererApp.Api.AccessList
@@ -29,5 +35,7 @@ defmodule WandererApp.Api do
resource WandererApp.Api.CorpWalletTransaction
resource WandererApp.Api.License
resource WandererApp.Api.MapPing
resource WandererApp.Api.MapInvite
resource WandererApp.Api.MapWebhookSubscription
end
end

View File

@@ -3,13 +3,32 @@ defmodule WandererApp.Api.AccessList do
use Ash.Resource,
domain: WandererApp.Api,
data_layer: AshPostgres.DataLayer
data_layer: AshPostgres.DataLayer,
extensions: [AshJsonApi.Resource]
postgres do
repo(WandererApp.Repo)
table("access_lists_v1")
end
json_api do
type "access_lists"
includes([:owner, :members])
derive_filter?(true)
derive_sort?(true)
routes do
base("/access_lists")
get(:read)
index :read
post(:new)
patch(:update)
delete(:destroy)
end
end
code_interface do
define(:create, action: :create)
define(:available, action: :available)
@@ -79,8 +98,11 @@ defmodule WandererApp.Api.AccessList do
relationships do
belongs_to :owner, WandererApp.Api.Character do
attribute_writable? true
public? true
end
has_many :members, WandererApp.Api.AccessListMember
has_many :members, WandererApp.Api.AccessListMember do
public? true
end
end
end

View File

@@ -3,13 +3,32 @@ defmodule WandererApp.Api.AccessListMember do
use Ash.Resource,
domain: WandererApp.Api,
data_layer: AshPostgres.DataLayer
data_layer: AshPostgres.DataLayer,
extensions: [AshJsonApi.Resource]
postgres do
repo(WandererApp.Repo)
table("access_list_members_v1")
end
json_api do
type "access_list_members"
includes([:access_list])
derive_filter?(true)
derive_sort?(true)
routes do
base("/access_list_members")
get(:read)
index :read
post(:create)
patch(:update_role)
delete(:destroy)
end
end
code_interface do
define(:create, action: :create)
define(:update_role, action: :update_role)
@@ -101,6 +120,7 @@ defmodule WandererApp.Api.AccessListMember do
relationships do
belongs_to :access_list, WandererApp.Api.AccessList do
attribute_writable? true
public? true
end
end

Some files were not shown because too many files have changed in this diff Show More