Compare commits

...

301 Commits

Author SHA1 Message Date
Dmitry Popov 35ea4e5f1e feat(signatures): Fixed creator visibility issues. Added 4.5 hour color for unsplashed
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2026-02-12 16:15:44 +01:00
Dmitry Popov 34a4d5dc9f feat(subscriptions): Added top map donators support
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2026-02-11 17:09:54 +01:00
Dmitry Popov 15142f188b Merge branch 'main' into develop 2026-02-11 10:35:32 +01:00
Dmitry Popov daf4a81568 Merge pull request #586 from wanderer-industries/routes-by
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
Add Routes By widget. Allow to find nearest blue loot and red l…
2026-02-09 02:49:29 +04:00
DanSylvest 8c5340e911 chore: add placeholder for no found destinations 2026-02-08 21:47:04 +03:00
DanSylvest 6b0f636964 chore: removed unnecessary comments 2026-02-08 19:45:36 +03:00
DanSylvest 09ebd29eb4 feat: Added lost files 2026-02-08 19:28:29 +03:00
DanSylvest 35bd5645bf feat: Added paywall for RoutesBy widget 2026-02-08 19:25:50 +03:00
DanSylvest a6948ee1da feat: removed unnecessary env variable for routes 2026-02-08 17:58:03 +03:00
Dmitry Popov 98b3f5855c Merge pull request #587 from wanderer-industries/multiple-structure-owners
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Multiple structure owners
2026-02-08 16:44:51 +04:00
CI 11ad48b40a chore: [skip ci] 2026-02-08 12:03:11 +00:00
CI ecd018abfe chore: release version v1.94.0 2026-02-08 12:03:11 +00:00
Dmitry Popov f430f74e98 feat(administration): Added registered characters admin view with cort/ally info, sort and filter options 2026-02-08 13:02:35 +01:00
CI 9e146d1117 chore: [skip ci] 2026-02-08 09:08:10 +00:00
CI 0a707fb423 chore: release version v1.93.0 2026-02-08 09:08:10 +00:00
Dmitry Popov 8cda76cc43 feat(subscriptions): Added an ability to withdraw from map to user balance 2026-02-08 10:04:03 +01:00
Dmitry Popov 2005e6f3dd fix(signatures): Fixed back linked sigs data sync and leading to system override issues 2026-02-07 17:18:29 +01:00
Dmitry Popov ab066a342f Merge branch 'develop' into multiple-structure-owners 2026-02-07 15:18:25 +01:00
Dmitry Popov 82b4a5f35a fix(signatures): Moved C1/C2/C3 and C4/C5 to the bottom of the available list
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2026-02-07 14:39:34 +01:00
Dmitry Popov ca3a25b836 Merge pull request #589 from guarzo/guarzo/ssecache
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
fix: use cache for sse
2026-02-07 04:20:13 +04:00
Guarzo 8e46c01a8a fix: use cache for sse 2026-02-06 22:48:13 +00:00
DanSylvest 9d9fa3c6b5 feat: Add systems with Security Status cleaning. Add trade hubs. Add ability to store data for this widget 2026-02-04 21:12:40 +03:00
Dmitry Popov 0e24501225 chore: Fixed review comments 2026-02-02 09:35:32 +01:00
DanSylvest 25a3d8951e feat: Add Routes By widget. Allow to find nearest blue loot and red loot stations. Added ability to set waypoint to station. 2026-01-31 12:29:25 +03:00
Dmitry Popov f4ddc8dc8b Merge pull request #530 from s-no1ukno/main
feat(map): Update Owners on Multiple Structures
2026-01-29 19:37:27 +04:00
Dmitry Popov ac9b46e24d Merge pull request #585 from guarzo/guarzo/addsysfromapi
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2026-01-27 12:21:35 +04:00
Guarzo 40d0a0777a fix: adding system when linked signature is provided 2026-01-27 03:10:33 +00:00
Dmitry Popov 608792d99a Merge pull request #584 from guarzo/guarzo/autoadd
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
feat: auto add system on sig addition
2026-01-26 22:45:57 +04:00
Guarzo dc9e0c821e feat: auto add system on sig addition 2026-01-26 13:47:37 +00:00
Dmitry Popov 79d4fd0e43 Merge pull request #582 from guarzo/guarzo/evenmoredev
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
fix: saving updates to unknown sigs
2026-01-25 15:20:19 +04:00
Guarzo 5d03c1ecc7 fix: saving updates to unknown sigs 2026-01-25 01:50:14 +00:00
Dmitry Popov 2eef05495e Merge pull request #580 from guarzo/guarzo/moreapidev
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
fix: wh position and sig type change
2026-01-24 02:07:53 +04:00
Guarzo f724455a1e fix: wh position and sig type change 2026-01-23 16:01:52 +00:00
Dmitry Popov 33bbb3425c Merge pull request #579 from guarzo/guarzo/apidev
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
fix: api updates and linked sig addition
2026-01-21 00:04:01 +04:00
Guarzo a919bd9038 fix: api updates and linked sig addition 2026-01-20 17:55:30 +00:00
Dmitry Popov 8ae34cd94a Merge pull request #577 from guarzo/guarzo/apisigfixes
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
fix: api fixes and format
2026-01-16 16:06:34 +04:00
Guarzo 2f38da52e8 fix: api fixes and format 2026-01-16 08:39:19 +00:00
CI 89d7df0ba2 chore: [skip ci] 2026-01-14 22:29:39 +00:00
CI ba0c10d2e4 chore: release version v1.92.0 2026-01-14 22:29:39 +00:00
Dmitry Popov 996c88d839 Merge pull request #575 from wanderer-industries/k162-selector
K162 selector
2026-01-15 02:29:09 +04:00
Dmitry Popov 80e998cf79 fix(core): Show c1/c2/c3 or c4/c5 or link signature modal 2026-01-14 23:28:47 +01:00
Dmitry Popov d2bcb89fa1 Merge branch 'main' into k162-selector 2026-01-13 20:27:48 +01:00
CI 922f296f17 chore: [skip ci] 2026-01-13 00:16:39 +00:00
CI 71dc20c933 chore: release version v1.91.11 2026-01-13 00:16:39 +00:00
Dmitry Popov 80f7d34d3d Merge pull request #573 from guarzo/guarzo/maprelayreturn
fix: allow sig api when map relay is off
2026-01-13 04:16:06 +04:00
Guarzo 113fe1c695 fix: allow sig api when map relay is off 2026-01-12 23:59:20 +00:00
DanSylvest 5550844912 feat: Added ability to select a range of wh classes for k162. 2026-01-12 12:39:53 +03:00
CI 0228e68a1d chore: [skip ci] 2026-01-07 12:35:19 +00:00
CI 3424667af1 chore: release version v1.91.10 2026-01-07 12:35:19 +00:00
Dmitry Popov 6c7b28a6c1 Merge pull request #571 from guarzo/guarzo/sigapi2
fix: remove actor context requirement from sig api
2026-01-07 16:34:34 +04:00
Guarzo 3988079cd3 fix: remove actor context requirement from sig api 2026-01-07 04:24:15 +00:00
CI f5d407fee0 chore: [skip ci] 2026-01-06 15:38:03 +00:00
CI a857422c46 chore: release version v1.91.9 2026-01-06 15:38:02 +00:00
Dmitry Popov ec6717d0ef Merge branch 'main' of github.com:wanderer-industries/wanderer 2026-01-06 16:37:32 +01:00
Dmitry Popov 56dacdcbbd fix(core): fixed rally point cancel logic 2026-01-06 16:37:29 +01:00
CI c8e17b1691 chore: [skip ci] 2026-01-06 14:07:08 +00:00
CI 19c7fe59ee chore: release version v1.91.8 2026-01-06 14:07:08 +00:00
Dmitry Popov 682100c231 Merge branch 'main' of github.com:wanderer-industries/wanderer 2026-01-06 15:06:34 +01:00
Dmitry Popov f9ac79cdcc fix(core): fixed rally point cancel logic 2026-01-06 15:06:31 +01:00
CI f09f220645 chore: [skip ci] 2026-01-05 20:29:10 +00:00
CI e585cdfd20 chore: release version v1.91.7 2026-01-05 20:29:10 +00:00
Dmitry Popov 3a3180f7b3 Merge branch 'main' of github.com:wanderer-industries/wanderer 2026-01-05 21:28:38 +01:00
Dmitry Popov 53abc580e5 chore: added promo on characters page 2026-01-05 21:28:35 +01:00
CI 8710d172a0 chore: [skip ci] 2026-01-04 23:49:15 +00:00
CI 301a380a4b chore: release version v1.91.6 2026-01-04 23:49:15 +00:00
Dmitry Popov 8c911f89e0 fix(core): fixed new connections got deleted after linked signature cleanup 2026-01-05 00:48:38 +01:00
CI d7e09fc94e chore: [skip ci] 2025-12-30 10:49:35 +00:00
CI 3b7e191898 chore: release version v1.91.5 2025-12-30 10:49:35 +00:00
Dmitry Popov f351fbaf20 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-30 11:49:02 +01:00
Dmitry Popov 016e793ba7 chore: Added 2026 roadmap blog post 2025-12-30 11:48:59 +01:00
CI db483fd253 chore: [skip ci] 2025-12-30 09:27:37 +00:00
CI 911ba231cd chore: release version v1.91.4 2025-12-30 09:27:37 +00:00
Dmitry Popov b3053f325d Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-30 10:27:06 +01:00
Dmitry Popov 4ab47334fc fix(core): fixed connections create between k-space systems (considered as wh connection) 2025-12-30 10:27:03 +01:00
CI e163f02526 chore: [skip ci] 2025-12-28 17:02:12 +00:00
CI 9e22dba8f1 chore: release version v1.91.3 2025-12-28 17:02:12 +00:00
Dmitry Popov 9631406def Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-28 18:01:43 +01:00
Dmitry Popov f6ae448c3b chore: update event post 2025-12-28 18:01:39 +01:00
CI 46345ef596 chore: [skip ci] 2025-12-27 22:11:03 +00:00
CI 1625f16c8f chore: release version v1.91.2 2025-12-27 22:11:03 +00:00
Dmitry Popov b4ef9ae983 fix(core): fixed map scopes updates & logic 2025-12-27 23:10:26 +01:00
CI 3b9c2dd996 chore: [skip ci] 2025-12-25 18:20:20 +00:00
CI 8a0f9a58d0 chore: release version v1.91.1 2025-12-25 18:20:20 +00:00
Dmitry Popov 5fe8caac0d Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-25 19:19:47 +01:00
Dmitry Popov f18f567727 chore: fix blog link styles 2025-12-25 19:19:44 +01:00
CI 91acc49980 chore: [skip ci] 2025-12-24 15:09:40 +00:00
CI ae3873a225 chore: release version v1.91.0 2025-12-24 15:09:40 +00:00
Dmitry Popov b351c6cc26 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-24 16:09:06 +01:00
Dmitry Popov 698244d945 feat(admin): added maps administration view with basic info, search, restore/delete, acls view and edit options 2025-12-24 16:09:03 +01:00
CI 2c7dd9dc5b chore: [skip ci] 2025-12-19 12:33:26 +00:00
CI 36934cce0b chore: release version v1.90.13 2025-12-19 12:33:26 +00:00
Dmitry Popov b7da7e4ecb Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-19 13:32:46 +01:00
Dmitry Popov 6471ea5590 fix(core): fixed welcome page 2025-12-19 13:32:44 +01:00
CI b46bcac642 chore: [skip ci] 2025-12-19 09:38:36 +00:00
CI 52d90361e9 chore: release version v1.90.12 2025-12-19 09:38:36 +00:00
Dmitry Popov 1c902d3319 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-19 10:38:02 +01:00
Dmitry Popov 8f671a359b fix(core): fixed permissions update after character corp updates 2025-12-19 10:37:59 +01:00
CI 840c416684 chore: [skip ci] 2025-12-18 21:47:59 +00:00
CI 56e29ad30a chore: release version v1.90.11 2025-12-18 21:47:59 +00:00
Dmitry Popov cd8f8b5801 chore: added promo codes support for map subs 2025-12-18 22:19:50 +01:00
Dmitry Popov 70e013fa3d Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-18 22:19:38 +01:00
Dmitry Popov d6bfaf8008 chore: added promo codes support for map subs 2025-12-18 22:19:26 +01:00
CI 95944199a0 chore: [skip ci] 2025-12-18 18:05:48 +00:00
CI 3bd5db8cf3 chore: release version v1.90.10 2025-12-18 18:05:48 +00:00
Dmitry Popov a245330ada Merge branch 'advent-challenge' 2025-12-18 19:05:10 +01:00
Dmitry Popov 1226b6abf3 chore: added advent challenge 2025-12-18 19:04:43 +01:00
Dmitry Popov 7a1f5c0966 chore: [skip ci] 2025-12-17 19:32:37 +01:00
CI e5afa1d5bc chore: [skip ci] 2025-12-15 11:46:40 +00:00
CI 1473fe8646 chore: release version v1.90.9 2025-12-15 11:46:40 +00:00
Dmitry Popov 7039ced11e fix(core): reduce chracters untrack grace period to 15 mins (after change/close/disconnect from map)
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-12-15 12:46:02 +01:00
CI 42b5bb337f chore: [skip ci] 2025-12-15 11:35:24 +00:00
CI 1dbb24f6ec chore: release version v1.90.8 2025-12-15 11:35:24 +00:00
Dmitry Popov c242f510e0 fix(core): skip systems or connections cleanup for not started maps 2025-12-15 12:34:55 +01:00
CI c59d51636e chore: [skip ci] 2025-12-15 00:36:18 +00:00
CI c5a8aa1b4d chore: release version v1.90.7 2025-12-15 00:36:18 +00:00
Dmitry Popov cba050a9e7 fix(core): fixed scopes 2025-12-15 01:35:41 +01:00
CI 59fcbef3b1 chore: [skip ci] 2025-12-12 18:49:02 +00:00
CI 2f1eb6eeaa chore: release version v1.90.6 2025-12-12 18:49:02 +00:00
Dmitry Popov 71ae326cf7 fix(core): fixed map scopes 2025-12-12 19:48:26 +01:00
CI 07829caf0f chore: [skip ci] 2025-12-12 18:36:03 +00:00
CI a5850b5a8d chore: release version v1.90.5 2025-12-12 18:36:03 +00:00
Dmitry Popov 9f6849209b fix(core): fixed map scopes 2025-12-12 19:35:26 +01:00
CI 7bd295cbad chore: [skip ci] 2025-12-12 17:07:55 +00:00
CI 078e5fc19e chore: release version v1.90.4 2025-12-12 17:07:55 +00:00
Dmitry Popov 3877e121c3 fix(core): fixed map scopes & signatures clean up behaviour 2025-12-12 18:07:18 +01:00
CI dcb2a0cdb2 chore: [skip ci] 2025-12-11 00:17:06 +00:00
CI f5294eee84 chore: release version v1.90.3 2025-12-11 00:17:06 +00:00
Dmitry Popov a5c87b6fa4 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-11 01:16:27 +01:00
Dmitry Popov eae275f515 fix(core): added pagination for long ACL lists 2025-12-11 01:16:24 +01:00
CI 68ae6706dd chore: [skip ci] 2025-12-10 23:56:28 +00:00
CI a34b30af15 chore: release version v1.90.2 2025-12-10 23:56:28 +00:00
Dmitry Popov 38b49266ed fix(core): added system position updates to SSE
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-12-11 00:55:52 +01:00
CI 049884bb4c chore: [skip ci] 2025-12-08 21:56:20 +00:00
CI 3c75b2b59f chore: release version v1.90.1 2025-12-08 21:56:20 +00:00
Dmitry Popov 4ad5d191a3 fix(core): fixed connections and signatures remove issues, added comprehensive audit log for auto removed connections and signatures 2025-12-08 22:55:39 +01:00
CI 2499c24cc1 chore: [skip ci] 2025-12-06 10:58:14 +00:00
CI 6f0043205c chore: release version v1.90.0 2025-12-06 10:58:14 +00:00
Dmitry Popov 597741fa60 Merge pull request #567 from wanderer-industries/develop
Develop
2025-12-06 14:57:27 +04:00
Dmitry Popov d313ae8cd2 fix(core): fixed clean up for linked signatures
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-12-04 11:33:42 +01:00
Dmitry Popov 06d5d8072e fix(core): fixed issue with default select mode
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-12-03 12:21:40 +01:00
CI f2d112df5c chore: [skip ci] 2025-12-02 23:44:54 +00:00
CI 716604fa84 chore: release version v1.89.6 2025-12-02 23:44:54 +00:00
Dmitry Popov cae958a1e6 Merge branch 'main' into develop
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-12-03 00:44:31 +01:00
Dmitry Popov 283b36c882 fix(kills): fixed zkb links (added "allow-popups-to-escape-sandbox" to CSP) 2025-12-03 00:44:23 +01:00
Dmitry Popov 051e71f1a6 Merge pull request #566 from guarzo/guarzo/sigapi
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
fix: apiV1 default fields updates
2025-12-03 00:20:13 +04:00
Guarzo 20a50e8db0 fix: apiV1 default fields updates 2025-12-02 17:55:05 +00:00
CI 79d7f7ce7d chore: [skip ci] 2025-12-02 12:46:26 +00:00
CI 6c4b65c446 chore: release version v1.89.5 2025-12-02 12:46:26 +00:00
Dmitry Popov 2b07af5e12 Merge branch 'main' into develop
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-12-02 13:45:57 +01:00
Dmitry Popov d0901eecb4 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-02 13:45:51 +01:00
Dmitry Popov ee85d29c54 chore: added tests 2025-12-02 13:45:47 +01:00
Dmitry Popov a237d6513d Merge branch 'main' into develop 2025-12-02 13:37:13 +01:00
CI 02979588c1 chore: [skip ci] 2025-12-02 12:35:31 +00:00
CI 3abe40855f chore: release version v1.89.4 2025-12-02 12:35:30 +00:00
Dmitry Popov d0d9418a89 fix(core): fixed acl character update issues 2025-12-02 13:34:55 +01:00
CI 3ce742eb01 chore: [skip ci] 2025-11-30 22:26:08 +00:00
CI ae566fb907 chore: release version v1.89.3 2025-11-30 22:26:08 +00:00
Dmitry Popov fa32c62f63 Merge branch 'main' into develop
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-11-30 23:25:48 +01:00
Dmitry Popov 6880be11c5 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-30 23:25:40 +01:00
Dmitry Popov 5289893264 fix(core): fixed tracking issues 2025-11-30 23:25:37 +01:00
CI f15370a3df chore: [skip ci] 2025-11-30 18:07:05 +00:00
CI cfac867c0a chore: release version v1.89.2 2025-11-30 18:07:05 +00:00
Dmitry Popov f50ea40b15 chore: updated tests for tracking 2025-11-30 19:06:15 +01:00
CI 04b2d57081 chore: [skip ci] 2025-11-30 17:52:21 +00:00
CI b235ea52e0 chore: release version v1.89.1 2025-11-30 17:52:21 +00:00
Dmitry Popov 2cb2dc526c Merge branch 'main' into develop
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-11-30 18:51:58 +01:00
Dmitry Popov f3c38ba62a fix(core): fixed tracking issues 2025-11-30 18:51:50 +01:00
CI 29473f2d3b chore: [skip ci] 2025-11-30 10:00:35 +00:00
CI 48654250e8 chore: release version v1.89.0 2025-11-30 10:00:35 +00:00
Aleksei Chichenkov 7aa24245b6 Merge pull request #564 from wanderer-industries/sig-panel
Sig panel
2025-11-30 13:00:08 +03:00
DanSylvest 6070d74684 feat: removed unnecessary command 2025-11-30 12:57:14 +03:00
Dmitry Popov c3de3c4e35 Merge branch 'main' into develop
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-11-29 20:17:14 +01:00
CI 5c513f3e50 chore: [skip ci] 2025-11-29 19:13:30 +00:00
CI 5a980c6b89 chore: release version v1.88.13 2025-11-29 19:13:30 +00:00
Dmitry Popov 85c075c5a6 fix(core): fixed tracking issues 2025-11-29 20:12:54 +01:00
DanSylvest f068afd16e Merge branch 'refs/heads/main' into sig-panel 2025-11-29 21:09:29 +03:00
DanSylvest ac71b0af64 feat: rework wormholes reference 2025-11-29 21:07:48 +03:00
DanSylvest 5c515d6acd Merge remote-tracking branch 'leesolway/sig-panel-pr' into sig-panel
# Conflicts:
#	assets/js/hooks/Mapper/mapRootProvider/hooks/useMapRootHandlers.ts
2025-11-29 17:32:35 +03:00
Dmitry Popov 4585c3a94b feat(core): Added several map scopes support (Wh, Hi, Low, Null, Pochven) 2025-11-29 14:36:45 +01:00
CI cf2c27c961 chore: [skip ci] 2025-11-29 11:35:52 +00:00
CI f8e403025c chore: release version v1.88.12 2025-11-29 11:35:52 +00:00
Dmitry Popov 46a1898be9 Merge branch 'fixed-warinings' into develop
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-11-29 12:35:36 +01:00
Dmitry Popov 25fa7c07bc fix(core): fixed c4 -> ns connections auto size issues 2025-11-29 12:35:22 +01:00
Dmitry Popov e7219e0eec chore: fixed compile warnings 2025-11-29 12:34:28 +01:00
CI 45130fcffa chore: [skip ci] 2025-11-29 09:16:34 +00:00
CI 5f75d4440d chore: release version v1.88.11 2025-11-29 09:16:34 +00:00
Dmitry Popov 34210f63e3 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-29 10:16:02 +01:00
Dmitry Popov 5f60fd4922 chore: fix tests workflow 2025-11-29 10:15:59 +01:00
CI 47ef7dda55 chore: [skip ci] 2025-11-29 00:15:17 +00:00
CI 0f3550a687 chore: release version v1.88.10 2025-11-29 00:15:17 +00:00
Dmitry Popov 8f242f3535 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-29 01:14:21 +01:00
Dmitry Popov 1ce39e5394 fix(core): fixed pings cleanup 2025-11-29 01:14:17 +01:00
CI cca7b912aa chore: [skip ci] 2025-11-29 00:11:43 +00:00
CI d939e32500 chore: release version v1.88.9 2025-11-29 00:11:43 +00:00
Dmitry Popov 97ebe66db5 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-29 01:11:04 +01:00
Dmitry Popov f437fc4541 fix(core): fixed linked signatures cleanup 2025-11-29 01:11:01 +01:00
CI 6c65538450 chore: [skip ci] 2025-11-28 23:54:56 +00:00
CI d566a74df4 chore: release version v1.88.8 2025-11-28 23:54:56 +00:00
Dmitry Popov 03e030a7d3 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-29 00:54:10 +01:00
Dmitry Popov e738e1da9c fix(core): fixed pings issue 2025-11-29 00:54:07 +01:00
CI 972b3a6cbe chore: [skip ci] 2025-11-28 23:43:54 +00:00
CI 96b4a3077e chore: release version v1.88.7 2025-11-28 23:43:53 +00:00
Dmitry Popov 6b308e8a1e Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-29 00:43:16 +01:00
Dmitry Popov d0874cbc6f fix(core): fixed tracking issues 2025-11-29 00:43:13 +01:00
CI f106a51bf5 chore: [skip ci] 2025-11-28 22:50:24 +00:00
CI dc47dc5f81 chore: release version v1.88.6 2025-11-28 22:50:24 +00:00
Dmitry Popov dc81cffeea Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-28 23:49:53 +01:00
Dmitry Popov 5766fcf4d8 fix(core): fixed tracking issues 2025-11-28 23:49:48 +01:00
CI c57a3b2cea chore: [skip ci] 2025-11-28 00:28:34 +00:00
CI 0c1fa8e79b chore: release version v1.88.5 2025-11-28 00:28:34 +00:00
Dmitry Popov 36cc91915c Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-28 01:27:30 +01:00
Dmitry Popov bb644fde31 fix(core): fixed env errors 2025-11-28 01:27:26 +01:00
CI 269b54d382 chore: [skip ci] 2025-11-27 11:17:21 +00:00
CI a9115cc653 chore: release version v1.88.4 2025-11-27 11:17:21 +00:00
Dmitry Popov eeea7aee8b Merge pull request #563 from guarzo/guarzo/killsdefense
fix: defensive check for undefined excluded systems
2025-11-27 15:16:52 +04:00
Guarzo 700089e381 fix: defensive check for undefined excluded systems 2025-11-27 04:12:59 +00:00
CI 932935557c chore: [skip ci] 2025-11-26 22:42:01 +00:00
CI 2890a76cf2 chore: release version v1.88.3 2025-11-26 22:42:01 +00:00
Dmitry Popov 4ac9b2e2b7 chore: Updated mix version 2025-11-26 23:41:24 +01:00
Dmitry Popov f92436f3f0 Merge branch 'develop' 2025-11-26 22:37:38 +01:00
Dmitry Popov 22d97cc99d fix(core): fixed env issues
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-11-26 22:18:02 +01:00
CI 305838573c chore: [skip ci] 2025-11-26 12:42:35 +00:00
CI cc7ad81d2f chore: release version v1.88.1 2025-11-26 12:42:35 +00:00
Dmitry Popov a694e57512 Merge pull request #561 from wanderer-industries/develop
Develop
2025-11-26 16:39:34 +04:00
Dmitry Popov 20be7fc67d Merge branch 'main' into develop
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-11-26 12:49:49 +01:00
CI 54bfee414b chore: [skip ci] 2025-11-25 21:55:15 +00:00
CI bcfa47bd94 chore: release version v1.88.0 2025-11-25 21:55:15 +00:00
Dmitry Popov b784f68818 Merge pull request #560 from wanderer-industries/zkb-evewho-links
feat: Add zkb and eve who links for characters where it possibly was add
2025-11-26 01:54:50 +04:00
DanSylvest 344ee54018 feat: Add zkb and eve who links for characters where it possibly was add 2025-11-25 23:28:54 +03:00
Dmitry Popov 42e0f8f660 Merge branch 'main' into develop
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-11-25 21:02:53 +01:00
CI 99b081887c chore: [skip ci] 2025-11-25 20:01:36 +00:00
CI dee8d0dae8 chore: release version v1.87.0 2025-11-25 20:01:36 +00:00
Dmitry Popov 147dd5880e Merge pull request #559 from wanderer-industries/markdown-description
feat: Add support markdown for system description
2025-11-26 00:01:09 +04:00
DanSylvest 69991fff72 feat: Add support markdown for system description 2025-11-25 22:50:11 +03:00
Dmitry Popov b881c84a52 Merge branch 'main' into develop 2025-11-25 20:11:53 +01:00
CI de4e1f859f chore: [skip ci] 2025-11-25 19:07:31 +00:00
CI 8e2a19540c chore: release version v1.86.1 2025-11-25 19:07:31 +00:00
Dmitry Popov 855c596672 Merge pull request #558 from wanderer-industries/show-passage-direction
fix(Map): Add ability to see character passage direction in list of p…
2025-11-25 23:06:45 +04:00
DanSylvest 36d3c0937b chore: Add ability to see character passage direction in list of passages - remove unnecessary log 2025-11-25 22:04:12 +03:00
CI d8fb1f78cf chore: [skip ci] 2025-11-25 19:03:24 +00:00
CI 98fa7e0235 chore: release version v1.86.0 2025-11-25 19:03:24 +00:00
Dmitry Popov e4396fe2f9 Merge pull request #557 from guarzo/guarzo/filteractivity
feat: add date filter for character activity
2025-11-25 23:02:58 +04:00
DanSylvest 1c117903f6 fix(Map): Add ability to see character passage direction in list of passages 2025-11-25 21:51:01 +03:00
Dmitry Popov 9e9dc39200 Merge pull request #556 from guarzo/guarzo/ticker2andsse
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
fix: sse enable checkbox, and kills ticker
2025-11-25 15:33:05 +04:00
Dmitry Popov abd7e4e15c chore: fix tests issues 2025-11-25 12:28:31 +01:00
Guarzo 88ed9cd39e feat: add date filter for character activity 2025-11-25 01:52:06 +00:00
Dmitry Popov 9666a8e78a chore: fix tests issues
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-11-25 00:41:40 +01:00
Dmitry Popov 271a3d90f8 Merge branch 'main' into tests-fixes-2 2025-11-24 23:58:08 +01:00
Dmitry Popov 01e291daf4 chore: fix tests issues 2025-11-24 23:57:52 +01:00
CI b7c0b45c15 chore: [skip ci] 2025-11-24 11:23:10 +00:00
CI 0874e3c51c chore: release version v1.85.5 2025-11-24 11:23:10 +00:00
Dmitry Popov d39fa0363a Merge branch 'main' into tests-fixes-2 2025-11-24 12:22:57 +01:00
Dmitry Popov 369b08a9ae fix(core): fixed connections cleanup and rally points delete issues 2025-11-24 12:22:40 +01:00
Dmitry Popov a872561b18 chore: fix tests issues 2025-11-24 11:33:08 +01:00
Dmitry Popov 857608f8ef chore: fix tests issues 2025-11-23 22:43:59 +01:00
Guarzo 7a74ae566b fix: sse enable checkbox, and kills ticker 2025-11-23 18:04:30 +00:00
Dmitry Popov f2c8724763 Merge branch 'tests-fixes' into develop
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-11-22 12:35:19 +01:00
Dmitry Popov 9a8dc4dbe5 Merge branch 'main' into tests-fixes 2025-11-22 12:29:22 +01:00
CI 01192dc637 chore: [skip ci] 2025-11-22 11:25:53 +00:00
CI 957cbcc561 chore: release version v1.85.4 2025-11-22 11:25:53 +00:00
Dmitry Popov 7eb6d093cf fix(core): invalidate map characters every 1 hour for any missing/revoked permissions 2025-11-22 12:25:24 +01:00
CI a23e544a9f chore: [skip ci] 2025-11-22 09:42:11 +00:00
CI 845ea7a576 chore: release version v1.85.3 2025-11-22 09:42:11 +00:00
Dmitry Popov ae8fbf30e4 fix(core): fixed connection time status issues. fixed character alliance update issues 2025-11-22 10:41:35 +01:00
Dmitry Popov 083e300ff5 chore: updated deps, fixed signatures and comments related issues
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-11-21 14:23:44 +01:00
Dmitry Popov ae4ebc0e36 Merge branch 'main' into develop
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 Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-11-20 12:05:40 +01:00
CI 3de385c902 chore: [skip ci] 2025-11-20 10:57:05 +00:00
CI 5f3d4dba37 chore: release version v1.85.2 2025-11-20 10:57:05 +00:00
Dmitry Popov 8acc7ddc25 fix(core): increased API pool limits 2025-11-20 11:56:31 +01:00
Dmitry Popov c175f19142 Merge branch 'main' into develop 2025-11-20 11:35:38 +01:00
CI ed6d25f3ea chore: [skip ci] 2025-11-20 10:35:09 +00:00
CI ab07d1321d chore: release version v1.85.1 2025-11-20 10:35:09 +00:00
Dmitry Popov a81e61bd70 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-20 11:31:39 +01:00
Dmitry Popov d2d33619c2 fix(core): increased API pool limits 2025-11-20 11:31:36 +01:00
CI fa464110c6 chore: [skip ci] 2025-11-19 23:13:02 +00:00
Dmitry Popov 0ebc703774 Merge pull request #551 from guarzo/guarzo/minorfixes
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
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
fix: apiv1  token auth and doc updates
2025-11-19 21:31:02 +04:00
Guarzo 4615e20838 reset dev.exs 2025-11-19 17:27:40 +00:00
guarzo f4d28f282a Merge branch 'develop' into guarzo/minorfixes 2025-11-19 11:03:43 -05:00
Dmitry Popov 1fe8ef17bd Merge branch 'main' into develop
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
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
2025-11-19 11:35:45 +01:00
Guarzo 6088afb38c openapi spec / api updates 2025-11-19 00:10:23 +00:00
Guarzo 5764c41d23 pr feedback 2025-11-18 20:46:06 +00:00
Guarzo 09444596ff fix: apiv1 token auth and structure fixes 2025-11-18 20:10:06 +00:00
Dmitry Popov ee15d90f9c fix: removed ipv6 distribution env settings
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
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
2025-11-18 19:47:29 +01:00
Dmitry Popov f5b014dae9 Merge branch 'main' into develop 2025-11-18 19:45:59 +01:00
Dmitry Popov 5e0965ead4 fix(tests): updated tests 2025-11-17 12:52:11 +01:00
Dmitry Popov 712379f4bb Merge branch 'main' into develop
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-11-17 00:09:27 +01:00
Dmitry Popov 4c39c6fb39 fix(tests): updated tests 2025-11-17 00:09:10 +01:00
Dmitry Popov a14e829f09 Merge pull request #547 from guarzo/guarzo/ssedisable
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
feature: disable sse by default
2025-11-15 19:36:29 +04:00
Guarzo 4002285882 test improvement 2025-11-15 12:46:03 +00:00
Guarzo d732d15ef6 feature: disable sse by default 2025-11-15 12:46:03 +00:00
Dmitry Popov 7613ca78da Merge branch 'main' into develop
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
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
2025-11-14 14:44:39 +01:00
Dmitry Popov c8631708b9 Merge branch 'main' into develop
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
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
2025-11-14 11:48:12 +01:00
Dmitry Popov 63ca473113 Merge pull request #502 from guarzo/guarzo/asyncfix
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
fix: resolve issue with async event processing
2025-11-12 15:10:08 +04:00
Jordan Snow a7d6b06332 feat(map): Reviewed changes
Adding the changes from first review of PR #530. This includes cleanup,
wrapping callbacks in a `useCallback()` hook, and inclusion of clsx
wrapper for styling.
2025-10-23 22:06:42 -06:00
Jordan Snow 8f6da817db Fix: Wrong file added to commits
This file should not have been added to previous commits, and was only
changed to allow for a fix in my local dev environment.
2025-10-19 12:26:28 -06:00
Jordan Snow 378f22a1ef feat(map): Logic for multiple owner updates
Finished all the logic for updating owners on multiple structures in a
single system.
2025-10-18 21:43:44 -06:00
Jordan Snow 14730097b2 feat(map) Adding all the things to the modal
Added a bunch of text and formatting to the system structures owners
dialog box
2025-10-18 20:26:28 -06:00
Jordan Snow e8bff3098a feat(map): wip New Dialog for Structure Owners
Added the new modal to be able to update all structures within a system
in a single update.
2025-10-18 19:24:19 -06:00
Lee Solway be7bbe6872 Create a signature list panel + hook into live events 2025-10-04 12:04:02 +01:00
guarzo 7df8284124 fix: clean up id generation 2025-08-30 02:05:28 +00:00
guarzo 21ca630abd fix: resolve issue with async event processing 2025-08-30 02:05:28 +00:00
355 changed files with 24146 additions and 2639 deletions
+5
View File
@@ -16,3 +16,8 @@ export WANDERER_SSE_ENABLED="true"
export WANDERER_WEBHOOKS_ENABLED="true" export WANDERER_WEBHOOKS_ENABLED="true"
export WANDERER_SSE_MAX_CONNECTIONS="1000" export WANDERER_SSE_MAX_CONNECTIONS="1000"
export WANDERER_WEBHOOK_TIMEOUT_MS="15000" export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
# Promo codes for map subscriptions (optional)
# Format: CODE:DISCOUNT_PERCENT,CODE2:DISCOUNT_PERCENT2
# Codes are case-insensitive, discounts stack with period discounts
# export WANDERER_PROMO_CODES="PROMO2025:10,NEWUSER:20"
+55 -55
View File
@@ -21,7 +21,7 @@ jobs:
test: test:
name: Test Suite name: Test Suite
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
postgres: postgres:
image: postgres:15 image: postgres:15
@@ -35,17 +35,17 @@ jobs:
--health-retries 5 --health-retries 5
ports: ports:
- 5432:5432 - 5432:5432
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Elixir/OTP - name: Setup Elixir/OTP
uses: erlef/setup-beam@v1 uses: erlef/setup-beam@v1
with: with:
elixir-version: ${{ env.ELIXIR_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }}
otp-version: ${{ env.OTP_VERSION }} otp-version: ${{ env.OTP_VERSION }}
- name: Cache Elixir dependencies - name: Cache Elixir dependencies
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
@@ -54,12 +54,12 @@ jobs:
_build _build
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix- restore-keys: ${{ runner.os }}-mix-
- name: Install Elixir dependencies - name: Install Elixir dependencies
run: | run: |
mix deps.get mix deps.get
mix deps.compile mix deps.compile
- name: Check code formatting - name: Check code formatting
id: format id: format
run: | run: |
@@ -71,42 +71,42 @@ jobs:
echo "count=1" >> $GITHUB_OUTPUT echo "count=1" >> $GITHUB_OUTPUT
fi fi
continue-on-error: true continue-on-error: true
- name: Compile code and capture warnings - name: Compile code and capture warnings
id: compile id: compile
run: | run: |
# Capture compilation output # Capture compilation output
output=$(mix compile 2>&1 || true) output=$(mix compile 2>&1 || true)
echo "$output" > compile_output.txt echo "$output" > compile_output.txt
# Count warnings # Count warnings
warning_count=$(echo "$output" | grep -c "warning:" || echo "0") warning_count=$(echo "$output" | grep -c "warning:" || echo "0")
# Check if compilation succeeded # Check if compilation succeeded
if mix compile > /dev/null 2>&1; then if mix compile > /dev/null 2>&1; then
echo "status=✅ Success" >> $GITHUB_OUTPUT echo "status=✅ Success" >> $GITHUB_OUTPUT
else else
echo "status=❌ Failed" >> $GITHUB_OUTPUT echo "status=❌ Failed" >> $GITHUB_OUTPUT
fi fi
echo "warnings=$warning_count" >> $GITHUB_OUTPUT echo "warnings=$warning_count" >> $GITHUB_OUTPUT
echo "output<<EOF" >> $GITHUB_OUTPUT echo "output<<EOF" >> $GITHUB_OUTPUT
echo "$output" >> $GITHUB_OUTPUT echo "$output" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT
continue-on-error: true continue-on-error: true
- name: Setup database - name: Setup database
run: | run: |
mix ecto.create mix ecto.create
mix ecto.migrate mix ecto.migrate
- name: Run tests with coverage - name: Run tests with coverage
id: tests id: tests
run: | run: |
# Run tests with coverage # Run tests with coverage
output=$(mix test --cover 2>&1 || true) output=$(mix test --cover 2>&1 || true)
echo "$output" > test_output.txt echo "$output" > test_output.txt
# Parse test results # Parse test results
if echo "$output" | grep -q "0 failures"; then if echo "$output" | grep -q "0 failures"; then
echo "status=✅ All Passed" >> $GITHUB_OUTPUT echo "status=✅ All Passed" >> $GITHUB_OUTPUT
@@ -115,16 +115,16 @@ jobs:
echo "status=❌ Some Failed" >> $GITHUB_OUTPUT echo "status=❌ Some Failed" >> $GITHUB_OUTPUT
test_status="failed" test_status="failed"
fi fi
# Extract test counts # Extract test counts
test_line=$(echo "$output" | grep -E "[0-9]+ tests?, [0-9]+ failures?" | head -1 || echo "0 tests, 0 failures") 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") 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") failures=$(echo "$test_line" | grep -o '[0-9]\+ failures\?' | grep -o '[0-9]\+' | head -1 || echo "0")
echo "total=$total_tests" >> $GITHUB_OUTPUT echo "total=$total_tests" >> $GITHUB_OUTPUT
echo "failures=$failures" >> $GITHUB_OUTPUT echo "failures=$failures" >> $GITHUB_OUTPUT
echo "passed=$((total_tests - failures))" >> $GITHUB_OUTPUT echo "passed=$((total_tests - failures))" >> $GITHUB_OUTPUT
# Calculate success rate # Calculate success rate
if [ "$total_tests" -gt 0 ]; then if [ "$total_tests" -gt 0 ]; then
success_rate=$(echo "scale=1; ($total_tests - $failures) * 100 / $total_tests" | bc) success_rate=$(echo "scale=1; ($total_tests - $failures) * 100 / $total_tests" | bc)
@@ -132,26 +132,26 @@ jobs:
success_rate="0" success_rate="0"
fi fi
echo "success_rate=$success_rate" >> $GITHUB_OUTPUT echo "success_rate=$success_rate" >> $GITHUB_OUTPUT
exit_code=$? exit_code=$?
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
continue-on-error: true continue-on-error: true
- name: Generate coverage report - name: Generate coverage report
id: coverage id: coverage
run: | run: |
# Generate coverage report with GitHub format # Generate coverage report with GitHub format
output=$(mix coveralls.github 2>&1 || true) output=$(mix coveralls.github 2>&1 || true)
echo "$output" > coverage_output.txt echo "$output" > coverage_output.txt
# Extract coverage percentage # Extract coverage percentage
coverage=$(echo "$output" | grep -o '[0-9]\+\.[0-9]\+%' | head -1 | sed 's/%//' || echo "0") coverage=$(echo "$output" | grep -o '[0-9]\+\.[0-9]\+%' | head -1 | sed 's/%//' || echo "0")
if [ -z "$coverage" ]; then if [ -z "$coverage" ]; then
coverage="0" coverage="0"
fi fi
echo "percentage=$coverage" >> $GITHUB_OUTPUT echo "percentage=$coverage" >> $GITHUB_OUTPUT
# Determine status # Determine status
if (( $(echo "$coverage >= 80" | bc -l) )); then if (( $(echo "$coverage >= 80" | bc -l) )); then
echo "status=✅ Excellent" >> $GITHUB_OUTPUT echo "status=✅ Excellent" >> $GITHUB_OUTPUT
@@ -161,14 +161,14 @@ jobs:
echo "status=❌ Needs Improvement" >> $GITHUB_OUTPUT echo "status=❌ Needs Improvement" >> $GITHUB_OUTPUT
fi fi
continue-on-error: true continue-on-error: true
- name: Run Credo analysis - name: Run Credo analysis
id: credo id: credo
run: | run: |
# Run Credo and capture output # Run Credo and capture output
output=$(mix credo --strict --format=json 2>&1 || true) output=$(mix credo --strict --format=json 2>&1 || true)
echo "$output" > credo_output.txt echo "$output" > credo_output.txt
# Try to parse JSON output # Try to parse JSON output
if echo "$output" | jq . > /dev/null 2>&1; then if echo "$output" | jq . > /dev/null 2>&1; then
issues=$(echo "$output" | jq '.issues | length' 2>/dev/null || echo "0") issues=$(echo "$output" | jq '.issues | length' 2>/dev/null || echo "0")
@@ -183,12 +183,12 @@ jobs:
normal_issues="0" normal_issues="0"
low_issues="0" low_issues="0"
fi fi
echo "total_issues=$issues" >> $GITHUB_OUTPUT echo "total_issues=$issues" >> $GITHUB_OUTPUT
echo "high_issues=$high_issues" >> $GITHUB_OUTPUT echo "high_issues=$high_issues" >> $GITHUB_OUTPUT
echo "normal_issues=$normal_issues" >> $GITHUB_OUTPUT echo "normal_issues=$normal_issues" >> $GITHUB_OUTPUT
echo "low_issues=$low_issues" >> $GITHUB_OUTPUT echo "low_issues=$low_issues" >> $GITHUB_OUTPUT
# Determine status # Determine status
if [ "$issues" -eq 0 ]; then if [ "$issues" -eq 0 ]; then
echo "status=✅ Clean" >> $GITHUB_OUTPUT echo "status=✅ Clean" >> $GITHUB_OUTPUT
@@ -198,24 +198,24 @@ jobs:
echo "status=❌ Needs Attention" >> $GITHUB_OUTPUT echo "status=❌ Needs Attention" >> $GITHUB_OUTPUT
fi fi
continue-on-error: true continue-on-error: true
- name: Run Dialyzer analysis - name: Run Dialyzer analysis
id: dialyzer id: dialyzer
run: | run: |
# Ensure PLT is built # Ensure PLT is built
mix dialyzer --plt mix dialyzer --plt
# Run Dialyzer and capture output # Run Dialyzer and capture output
output=$(mix dialyzer --format=github 2>&1 || true) output=$(mix dialyzer --format=github 2>&1 || true)
echo "$output" > dialyzer_output.txt echo "$output" > dialyzer_output.txt
# Count warnings and errors # Count warnings and errors
warnings=$(echo "$output" | grep -c "warning:" || echo "0") warnings=$(echo "$output" | grep -c "warning:" || echo "0")
errors=$(echo "$output" | grep -c "error:" || echo "0") errors=$(echo "$output" | grep -c "error:" || echo "0")
echo "warnings=$warnings" >> $GITHUB_OUTPUT echo "warnings=$warnings" >> $GITHUB_OUTPUT
echo "errors=$errors" >> $GITHUB_OUTPUT echo "errors=$errors" >> $GITHUB_OUTPUT
# Determine status # Determine status
if [ "$errors" -eq 0 ] && [ "$warnings" -eq 0 ]; then if [ "$errors" -eq 0 ] && [ "$warnings" -eq 0 ]; then
echo "status=✅ Clean" >> $GITHUB_OUTPUT echo "status=✅ Clean" >> $GITHUB_OUTPUT
@@ -225,7 +225,7 @@ jobs:
echo "status=❌ Has Errors" >> $GITHUB_OUTPUT echo "status=❌ Has Errors" >> $GITHUB_OUTPUT
fi fi
continue-on-error: true continue-on-error: true
- name: Create test results summary - name: Create test results summary
id: summary id: summary
run: | run: |
@@ -236,11 +236,11 @@ jobs:
coverage_score=${{ steps.coverage.outputs.percentage }} coverage_score=${{ steps.coverage.outputs.percentage }}
credo_score=$(echo "scale=0; (100 - ${{ steps.credo.outputs.total_issues }} * 2)" | bc | sed 's/^-.*$/0/') 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/') 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) 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 echo "overall_score=$overall_score" >> $GITHUB_OUTPUT
# Determine overall status # Determine overall status
if (( $(echo "$overall_score >= 90" | bc -l) )); then if (( $(echo "$overall_score >= 90" | bc -l) )); then
echo "overall_status=🌟 Excellent" >> $GITHUB_OUTPUT echo "overall_status=🌟 Excellent" >> $GITHUB_OUTPUT
@@ -252,7 +252,7 @@ jobs:
echo "overall_status=❌ Poor" >> $GITHUB_OUTPUT echo "overall_status=❌ Poor" >> $GITHUB_OUTPUT
fi fi
continue-on-error: true continue-on-error: true
- name: Find existing PR comment - name: Find existing PR comment
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
id: find_comment id: find_comment
@@ -261,7 +261,7 @@ jobs:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]' comment-author: 'github-actions[bot]'
body-includes: '## 🧪 Test Results Summary' body-includes: '## 🧪 Test Results Summary'
- name: Create or update PR comment - name: Create or update PR comment
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v4 uses: peter-evans/create-or-update-comment@v4
@@ -271,11 +271,11 @@ jobs:
edit-mode: replace edit-mode: replace
body: | body: |
## 🧪 Test Results Summary ## 🧪 Test Results Summary
**Overall Quality Score: ${{ steps.summary.outputs.overall_score }}%** ${{ steps.summary.outputs.overall_status }} **Overall Quality Score: ${{ steps.summary.outputs.overall_score }}%** ${{ steps.summary.outputs.overall_status }}
### 📊 Metrics Dashboard ### 📊 Metrics Dashboard
| Category | Status | Count | Details | | Category | Status | Count | Details |
|----------|---------|-------|---------| |----------|---------|-------|---------|
| 📝 **Code Formatting** | ${{ steps.format.outputs.status }} | ${{ steps.format.outputs.count }} issues | `mix format --check-formatted` | | 📝 **Code Formatting** | ${{ steps.format.outputs.status }} | ${{ steps.format.outputs.count }} issues | `mix format --check-formatted` |
@@ -284,50 +284,50 @@ jobs:
| 📊 **Coverage** | ${{ steps.coverage.outputs.status }} | ${{ steps.coverage.outputs.percentage }}% | `mix coveralls` | | 📊 **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 }} | | 🎯 **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` | | 🔍 **Dialyzer** | ${{ steps.dialyzer.outputs.status }} | ${{ steps.dialyzer.outputs.errors }} errors, ${{ steps.dialyzer.outputs.warnings }} warnings | `mix dialyzer` |
### 🎯 Quality Gates ### 🎯 Quality Gates
Based on the project's quality thresholds: Based on the project's quality thresholds:
- **Compilation Warnings**: ${{ steps.compile.outputs.warnings }}/148 (limit: 148) - **Compilation Warnings**: ${{ steps.compile.outputs.warnings }}/148 (limit: 148)
- **Credo Issues**: ${{ steps.credo.outputs.total_issues }}/87 (limit: 87) - **Credo Issues**: ${{ steps.credo.outputs.total_issues }}/87 (limit: 87)
- **Dialyzer Warnings**: ${{ steps.dialyzer.outputs.warnings }}/161 (limit: 161) - **Dialyzer Warnings**: ${{ steps.dialyzer.outputs.warnings }}/161 (limit: 161)
- **Test Coverage**: ${{ steps.coverage.outputs.percentage }}%/50% (minimum: 50%) - **Test Coverage**: ${{ steps.coverage.outputs.percentage }}%/50% (minimum: 50%)
- **Test Failures**: ${{ steps.tests.outputs.failures }}/0 (limit: 0) - **Test Failures**: ${{ steps.tests.outputs.failures }}/0 (limit: 0)
<details> <details>
<summary>📈 Progress Toward Goals</summary> <summary>📈 Progress Toward Goals</summary>
Target goals for the project: Target goals for the project:
- ✨ **Zero compilation warnings** (currently: ${{ steps.compile.outputs.warnings }}) - ✨ **Zero compilation warnings** (currently: ${{ steps.compile.outputs.warnings }})
- ✨ **≤10 Credo issues** (currently: ${{ steps.credo.outputs.total_issues }}) - ✨ **≤10 Credo issues** (currently: ${{ steps.credo.outputs.total_issues }})
- ✨ **Zero Dialyzer warnings** (currently: ${{ steps.dialyzer.outputs.warnings }}) - ✨ **Zero Dialyzer warnings** (currently: ${{ steps.dialyzer.outputs.warnings }})
- ✨ **≥85% test coverage** (currently: ${{ steps.coverage.outputs.percentage }}%) - ✨ **≥85% test coverage** (currently: ${{ steps.coverage.outputs.percentage }}%)
- ✅ **Zero test failures** (currently: ${{ steps.tests.outputs.failures }}) - ✅ **Zero test failures** (currently: ${{ steps.tests.outputs.failures }})
</details> </details>
<details> <details>
<summary>🔧 Quick Actions</summary> <summary>🔧 Quick Actions</summary>
To improve code quality: To improve code quality:
```bash ```bash
# Fix formatting issues # Fix formatting issues
mix format mix format
# View detailed Credo analysis # View detailed Credo analysis
mix credo --strict mix credo --strict
# Check Dialyzer warnings # Check Dialyzer warnings
mix dialyzer mix dialyzer
# Generate detailed coverage report # Generate detailed coverage report
mix coveralls.html mix coveralls.html
``` ```
</details> </details>
--- ---
🤖 *Auto-generated by GitHub Actions* • Updated: ${{ github.event.head_commit.timestamp }} 🤖 *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. > **Note**: This comment will be updated automatically when new commits are pushed to this PR.
+3
View File
@@ -17,6 +17,9 @@ repomix*
/priv/static/images/ /priv/static/images/
/priv/static/*.js /priv/static/*.js
/priv/static/*.css /priv/static/*.css
/priv/static/*-*.png
/priv/static/*-*.webp
/priv/static/*-*.webmanifest
# Dialyzer PLT files # Dialyzer PLT files
/priv/plts/ /priv/plts/
+503
View File
@@ -2,6 +2,509 @@
<!-- changelog --> <!-- changelog -->
## [v1.94.0](https://github.com/wanderer-industries/wanderer/compare/v1.93.0...v1.94.0) (2026-02-08)
### Features:
* administration: Added registered characters admin view with cort/ally info, sort and filter options
## [v1.93.0](https://github.com/wanderer-industries/wanderer/compare/v1.92.0...v1.93.0) (2026-02-08)
### Features:
* subscriptions: Added an ability to withdraw from map to user balance
## [v1.92.0](https://github.com/wanderer-industries/wanderer/compare/v1.91.11...v1.92.0) (2026-01-14)
### Features:
* Added ability to select a range of wh classes for k162.
### Bug Fixes:
* core: Show c1/c2/c3 or c4/c5 or link signature modal
## [v1.91.11](https://github.com/wanderer-industries/wanderer/compare/v1.91.10...v1.91.11) (2026-01-13)
### Bug Fixes:
* allow sig api when map relay is off
## [v1.91.10](https://github.com/wanderer-industries/wanderer/compare/v1.91.9...v1.91.10) (2026-01-07)
### Bug Fixes:
* remove actor context requirement from sig api
## [v1.91.9](https://github.com/wanderer-industries/wanderer/compare/v1.91.8...v1.91.9) (2026-01-06)
### Bug Fixes:
* core: fixed rally point cancel logic
## [v1.91.8](https://github.com/wanderer-industries/wanderer/compare/v1.91.7...v1.91.8) (2026-01-06)
### Bug Fixes:
* core: fixed rally point cancel logic
## [v1.91.7](https://github.com/wanderer-industries/wanderer/compare/v1.91.6...v1.91.7) (2026-01-05)
## [v1.91.6](https://github.com/wanderer-industries/wanderer/compare/v1.91.5...v1.91.6) (2026-01-04)
### Bug Fixes:
* core: fixed new connections got deleted after linked signature cleanup
## [v1.91.5](https://github.com/wanderer-industries/wanderer/compare/v1.91.4...v1.91.5) (2025-12-30)
## [v1.91.4](https://github.com/wanderer-industries/wanderer/compare/v1.91.3...v1.91.4) (2025-12-30)
### Bug Fixes:
* core: fixed connections create between k-space systems (considered as wh connection)
## [v1.91.3](https://github.com/wanderer-industries/wanderer/compare/v1.91.2...v1.91.3) (2025-12-28)
## [v1.91.2](https://github.com/wanderer-industries/wanderer/compare/v1.91.1...v1.91.2) (2025-12-27)
### Bug Fixes:
* core: fixed map scopes updates & logic
## [v1.91.1](https://github.com/wanderer-industries/wanderer/compare/v1.91.0...v1.91.1) (2025-12-25)
## [v1.91.0](https://github.com/wanderer-industries/wanderer/compare/v1.90.13...v1.91.0) (2025-12-24)
### Features:
* admin: added maps administration view with basic info, search, restore/delete, acls view and edit options
## [v1.90.13](https://github.com/wanderer-industries/wanderer/compare/v1.90.12...v1.90.13) (2025-12-19)
### Bug Fixes:
* core: fixed welcome page
## [v1.90.12](https://github.com/wanderer-industries/wanderer/compare/v1.90.11...v1.90.12) (2025-12-19)
### Bug Fixes:
* core: fixed permissions update after character corp updates
## [v1.90.11](https://github.com/wanderer-industries/wanderer/compare/v1.90.10...v1.90.11) (2025-12-18)
## [v1.90.10](https://github.com/wanderer-industries/wanderer/compare/v1.90.9...v1.90.10) (2025-12-18)
## [v1.90.9](https://github.com/wanderer-industries/wanderer/compare/v1.90.8...v1.90.9) (2025-12-15)
### Bug Fixes:
* core: reduce chracters untrack grace period to 15 mins (after change/close/disconnect from map)
## [v1.90.8](https://github.com/wanderer-industries/wanderer/compare/v1.90.7...v1.90.8) (2025-12-15)
### Bug Fixes:
* core: skip systems or connections cleanup for not started maps
## [v1.90.7](https://github.com/wanderer-industries/wanderer/compare/v1.90.6...v1.90.7) (2025-12-15)
### Bug Fixes:
* core: fixed scopes
## [v1.90.6](https://github.com/wanderer-industries/wanderer/compare/v1.90.5...v1.90.6) (2025-12-12)
### Bug Fixes:
* core: fixed map scopes
## [v1.90.5](https://github.com/wanderer-industries/wanderer/compare/v1.90.4...v1.90.5) (2025-12-12)
### Bug Fixes:
* core: fixed map scopes
## [v1.90.4](https://github.com/wanderer-industries/wanderer/compare/v1.90.3...v1.90.4) (2025-12-12)
### Bug Fixes:
* core: fixed map scopes & signatures clean up behaviour
## [v1.90.3](https://github.com/wanderer-industries/wanderer/compare/v1.90.2...v1.90.3) (2025-12-11)
### Bug Fixes:
* core: added pagination for long ACL lists
## [v1.90.2](https://github.com/wanderer-industries/wanderer/compare/v1.90.1...v1.90.2) (2025-12-10)
### Bug Fixes:
* core: added system position updates to SSE
## [v1.90.1](https://github.com/wanderer-industries/wanderer/compare/v1.90.0...v1.90.1) (2025-12-08)
### Bug Fixes:
* core: fixed connections and signatures remove issues, added comprehensive audit log for auto removed connections and signatures
## [v1.90.0](https://github.com/wanderer-industries/wanderer/compare/v1.89.6...v1.90.0) (2025-12-06)
### Features:
* core: Added several map scopes support (Wh, Hi, Low, Null, Pochven)
### Bug Fixes:
* core: fixed clean up for linked signatures
* core: fixed issue with default select mode
* apiV1 default fields updates
## [v1.89.6](https://github.com/wanderer-industries/wanderer/compare/v1.89.5...v1.89.6) (2025-12-02)
### Bug Fixes:
* kills: fixed zkb links (added "allow-popups-to-escape-sandbox" to CSP)
## [v1.89.5](https://github.com/wanderer-industries/wanderer/compare/v1.89.4...v1.89.5) (2025-12-02)
## [v1.89.4](https://github.com/wanderer-industries/wanderer/compare/v1.89.3...v1.89.4) (2025-12-02)
### Bug Fixes:
* core: fixed acl character update issues
## [v1.89.3](https://github.com/wanderer-industries/wanderer/compare/v1.89.2...v1.89.3) (2025-11-30)
### Bug Fixes:
* core: fixed tracking issues
## [v1.89.2](https://github.com/wanderer-industries/wanderer/compare/v1.89.1...v1.89.2) (2025-11-30)
## [v1.89.1](https://github.com/wanderer-industries/wanderer/compare/v1.89.0...v1.89.1) (2025-11-30)
### Bug Fixes:
* core: fixed tracking issues
## [v1.89.0](https://github.com/wanderer-industries/wanderer/compare/v1.88.13...v1.89.0) (2025-11-30)
### Features:
* removed unnecessary command
* rework wormholes reference
## [v1.88.13](https://github.com/wanderer-industries/wanderer/compare/v1.88.12...v1.88.13) (2025-11-29)
### Bug Fixes:
* core: fixed tracking issues
## [v1.88.12](https://github.com/wanderer-industries/wanderer/compare/v1.88.11...v1.88.12) (2025-11-29)
### Bug Fixes:
* core: fixed c4 -> ns connections auto size issues
## [v1.88.11](https://github.com/wanderer-industries/wanderer/compare/v1.88.10...v1.88.11) (2025-11-29)
## [v1.88.10](https://github.com/wanderer-industries/wanderer/compare/v1.88.9...v1.88.10) (2025-11-29)
### Bug Fixes:
* core: fixed pings cleanup
## [v1.88.9](https://github.com/wanderer-industries/wanderer/compare/v1.88.8...v1.88.9) (2025-11-29)
### Bug Fixes:
* core: fixed linked signatures cleanup
## [v1.88.8](https://github.com/wanderer-industries/wanderer/compare/v1.88.7...v1.88.8) (2025-11-28)
### Bug Fixes:
* core: fixed pings issue
## [v1.88.7](https://github.com/wanderer-industries/wanderer/compare/v1.88.6...v1.88.7) (2025-11-28)
### Bug Fixes:
* core: fixed tracking issues
## [v1.88.6](https://github.com/wanderer-industries/wanderer/compare/v1.88.5...v1.88.6) (2025-11-28)
### Bug Fixes:
* core: fixed tracking issues
## [v1.88.5](https://github.com/wanderer-industries/wanderer/compare/v1.88.4...v1.88.5) (2025-11-28)
### Bug Fixes:
* core: fixed env errors
## [v1.88.4](https://github.com/wanderer-industries/wanderer/compare/v1.88.3...v1.88.4) (2025-11-27)
### Bug Fixes:
* defensive check for undefined excluded systems
## [v1.88.3](https://github.com/wanderer-industries/wanderer/compare/v1.88.2...v1.88.3) (2025-11-26)
### Bug Fixes:
* core: fixed env issues
## [v1.88.1](https://github.com/wanderer-industries/wanderer/compare/v1.88.0...v1.88.1) (2025-11-26)
### Bug Fixes:
* sse enable checkbox, and kills ticker
* apiv1 token auth and structure fixes
* removed ipv6 distribution env settings
* tests: updated tests
* tests: updated tests
* clean up id generation
* resolve issue with async event processing
## [v1.88.0](https://github.com/wanderer-industries/wanderer/compare/v1.87.0...v1.88.0) (2025-11-25)
### Features:
* Add zkb and eve who links for characters where it possibly was add
## [v1.87.0](https://github.com/wanderer-industries/wanderer/compare/v1.86.1...v1.87.0) (2025-11-25)
### Features:
* Add support markdown for system description
## [v1.86.1](https://github.com/wanderer-industries/wanderer/compare/v1.86.0...v1.86.1) (2025-11-25)
### Bug Fixes:
* Map: Add ability to see character passage direction in list of passages
## [v1.86.0](https://github.com/wanderer-industries/wanderer/compare/v1.85.5...v1.86.0) (2025-11-25)
### Features:
* add date filter for character activity
## [v1.85.5](https://github.com/wanderer-industries/wanderer/compare/v1.85.4...v1.85.5) (2025-11-24)
### Bug Fixes:
* core: fixed connections cleanup and rally points delete issues
## [v1.85.4](https://github.com/wanderer-industries/wanderer/compare/v1.85.3...v1.85.4) (2025-11-22)
### Bug Fixes:
* core: invalidate map characters every 1 hour for any missing/revoked permissions
## [v1.85.3](https://github.com/wanderer-industries/wanderer/compare/v1.85.2...v1.85.3) (2025-11-22)
### Bug Fixes:
* core: fixed connection time status issues. fixed character alliance update issues
## [v1.85.2](https://github.com/wanderer-industries/wanderer/compare/v1.85.1...v1.85.2) (2025-11-20)
### Bug Fixes:
* core: increased API pool limits
## [v1.85.1](https://github.com/wanderer-industries/wanderer/compare/v1.85.0...v1.85.1) (2025-11-20)
### Bug Fixes:
* core: increased API pool limits
## [v1.85.0](https://github.com/wanderer-industries/wanderer/compare/v1.84.37...v1.85.0) (2025-11-19) ## [v1.85.0](https://github.com/wanderer-industries/wanderer/compare/v1.84.37...v1.85.0) (2025-11-19)
+51 -1
View File
@@ -32,8 +32,58 @@ format f:
test t: test t:
MIX_ENV=test mix test MIX_ENV=test mix test
# Run tests in 4 parallel partitions (useful for CI or faster local runs)
test-parallel tp:
@echo "Running tests in 4 parallel partitions..."
@mkdir -p /tmp/wanderer_test_results
@rm -f /tmp/wanderer_test_results/partition_*.txt /tmp/wanderer_test_results/exit_*.txt
@for i in 1 2 3 4; do \
(MIX_TEST_PARTITION=$$i MIX_ENV=test mix test --partitions 4 2>&1; echo $$? > /tmp/wanderer_test_results/exit_$$i.txt) | \
tee /tmp/wanderer_test_results/partition_$$i.txt | sed "s/^/[P$$i] /" & \
done; \
wait
@echo ""
@echo "========================================"
@echo " TEST RESULTS SUMMARY"
@echo "========================================"
@total_tests=0; total_failures=0; total_excluded=0; all_passed=true; \
for i in 1 2 3 4; do \
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
if [ "$$exit_code" != "0" ]; then all_passed=false; fi; \
summary=$$(grep -E "^[0-9]+ (tests?|doctest)" /tmp/wanderer_test_results/partition_$$i.txt | tail -1 || echo "No results"); \
tests=$$(echo "$$summary" | grep -oE "^[0-9]+" || echo "0"); \
failures=$$(echo "$$summary" | grep -oE "[0-9]+ failures?" | grep -oE "^[0-9]+" || echo "0"); \
excluded=$$(echo "$$summary" | grep -oE "[0-9]+ excluded" | grep -oE "^[0-9]+" || echo "0"); \
total_tests=$$((total_tests + tests)); \
total_failures=$$((total_failures + failures)); \
total_excluded=$$((total_excluded + excluded)); \
if [ "$$exit_code" = "0" ]; then \
echo "Partition $$i: ✓ $$summary"; \
else \
echo "Partition $$i: ✗ $$summary (exit code: $$exit_code)"; \
fi; \
done; \
echo "========================================"; \
echo "TOTAL: $$total_tests tests, $$total_failures failures, $$total_excluded excluded"; \
echo "========================================"; \
if [ "$$all_passed" = "true" ]; then \
echo "✓ All partitions passed!"; \
else \
echo "✗ Some partitions failed. Details below:"; \
echo ""; \
for i in 1 2 3 4; do \
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
if [ "$$exit_code" != "0" ]; then \
echo "======== PARTITION $$i FAILURES ========"; \
grep -A 50 "Failures:" /tmp/wanderer_test_results/partition_$$i.txt 2>/dev/null || cat /tmp/wanderer_test_results/partition_$$i.txt; \
echo ""; \
fi; \
done; \
exit 1; \
fi
coverage cover co: coverage cover co:
mix test --cover MIX_ENV=test mix test --cover
unit-tests ut: unit-tests ut:
@echo "Running unit tests..." @echo "Running unit tests..."
+24
View File
@@ -1001,3 +1001,27 @@ body > div:first-of-type {
.verticalTabsContainer .p-tabview-panel { .verticalTabsContainer .p-tabview-panel {
flex-grow: 1; flex-grow: 1;
} }
/* Blog post CTA links - only in main post content */
.post-content a {
display: inline-block;
background: linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);
color: white !important;
padding: 0.5rem 1.25rem;
border-radius: 0.5rem;
text-decoration: none !important;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(236, 72, 153, 0.3);
}
.post-content a:hover {
background: linear-gradient(135deg, #db2777 0%, #7c3aed 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(236, 72, 153, 0.4);
}
.post-content a:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(236, 72, 153, 0.3);
}
@@ -8,3 +8,15 @@
} }
} }
} }
.ContextMenu {
width: max-content;
min-width: unset;
:global {
.p-submenu-list {
width: max-content;
min-width: unset !important;
}
}
}
@@ -1,21 +1,23 @@
import React, { RefObject, useMemo } from 'react'; import React, { RefObject, useCallback, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu'; import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api'; import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem'; import { MenuItem } from 'primereact/menuitem';
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types'; import { CharacterTypeRaw, SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
import classes from './ContextMenuSystemInfo.module.scss'; import classes from './ContextMenuSystemInfo.module.scss';
import { getSystemById } from '@/hooks/Mapper/helpers'; import { getSystemById } from '@/hooks/Mapper/helpers';
import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks'; import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts'; import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components'; import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components';
import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks'; import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks';
import { Route } from '@/hooks/Mapper/types/routes.ts'; import { Route, RouteStationSummary } from '@/hooks/Mapper/types/routes.ts';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts'; import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons'; import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
import { useGetOwnOnlineCharacters } from '@/hooks/Mapper/components/hooks/useGetOwnOnlineCharacters.ts';
import { sortStationsByDistance } from './sortStationsByDistance.ts';
export interface ContextMenuSystemInfoProps { export interface ContextMenuSystemInfoProps {
systemStatics: Map<number, SolarSystemStaticInfoRaw>; systemStatics: Map<number, SolarSystemStaticInfoRaw>;
hubs: string[];
contextMenuRef: RefObject<ContextMenu>; contextMenuRef: RefObject<ContextMenu>;
systemId: string | undefined; systemId: string | undefined;
systemIdFrom?: string | undefined; systemIdFrom?: string | undefined;
@@ -37,11 +39,106 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
onWaypointSet, onWaypointSet,
systemId, systemId,
systemIdFrom, systemIdFrom,
hubs,
routes, routes,
}) => { }) => {
const getWaypointMenu = useWaypointMenu(onWaypointSet); const getWaypointMenu = useWaypointMenu(onWaypointSet);
const getJumpPlannerMenu = useJumpPlannerMenu(systems, systemIdFrom); const getJumpPlannerMenu = useJumpPlannerMenu(systems, systemIdFrom);
const { toggleHubCommand, hubs } = useRouteProvider();
const getOwnOnlineCharacters = useGetOwnOnlineCharacters();
const getStationWaypointItems = useCallback(
(destinationId: string, chars: CharacterTypeRaw[]): MenuItem[] => [
{
label: 'Set Destination',
icon: PrimeIcons.SEND,
command: () => {
onWaypointSet({
fromBeginning: true,
clearWay: true,
destination: destinationId,
charIds: chars.map(char => char.eve_id),
});
},
},
{
label: 'Add Waypoint',
icon: PrimeIcons.DIRECTIONS_ALT,
command: () => {
onWaypointSet({
fromBeginning: false,
clearWay: false,
destination: destinationId,
charIds: chars.map(char => char.eve_id),
});
},
},
{
label: 'Add Waypoint Front',
icon: PrimeIcons.DIRECTIONS,
command: () => {
onWaypointSet({
fromBeginning: true,
clearWay: false,
destination: destinationId,
charIds: chars.map(char => char.eve_id),
});
},
},
],
[onWaypointSet],
);
const getStationsMenu = useCallback(
(stations: RouteStationSummary[]) => {
const chars = getOwnOnlineCharacters().filter(x => x.online);
const sortedStations = sortStationsByDistance(stations);
return [
{
label: 'Stations',
icon: PrimeIcons.MAP_MARKER,
items: sortedStations.map(station => {
const destinationId = station.station_id.toString();
const specialClass = station.special ? '[&_.p-menuitem-text]:text-orange-400' : '';
if (chars.length === 0) {
return {
label: station.station_name,
className: specialClass || undefined,
items: [{ label: 'No online characters', disabled: true }],
};
}
if (chars.length === 1) {
return {
label: station.station_name,
className: specialClass || undefined,
items: getStationWaypointItems(destinationId, chars.slice(0, 1)),
};
}
return {
label: station.station_name,
className: `${specialClass} w-[500px]`.trim(),
items: [
{
label: 'All',
icon: PrimeIcons.USERS,
items: getStationWaypointItems(destinationId, chars),
},
...chars.map(char => ({
label: char.name,
icon: PrimeIcons.USER,
items: getStationWaypointItems(destinationId, [char]),
})),
],
};
}),
},
];
},
[getOwnOnlineCharacters, getStationWaypointItems],
);
const items: MenuItem[] = useMemo(() => { const items: MenuItem[] = useMemo(() => {
const system = systemId ? systemStatics.get(parseInt(systemId)) : undefined; const system = systemId ? systemStatics.get(parseInt(systemId)) : undefined;
@@ -50,6 +147,10 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
if (!systemId || !system) { if (!systemId || !system) {
return []; return [];
} }
const route = routes.find(x => x.destination?.toString() === systemId);
const stationItems = route?.stations?.length ? getStationsMenu(route.stations) : [];
return [ return [
{ {
className: classes.FastActions, className: classes.FastActions,
@@ -69,15 +170,20 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
{ separator: true }, { separator: true },
...getJumpPlannerMenu(system, routes), ...getJumpPlannerMenu(system, routes),
...getWaypointMenu(systemId, system.system_class), ...getWaypointMenu(systemId, system.system_class),
{ ...stationItems,
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route', ...(toggleHubCommand
icon: !hubs.includes(systemId) ? ( ? [
<MapAddIcon className="mr-1 relative left-[-2px]" /> {
) : ( label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
<MapDeleteIcon className="mr-1 relative left-[-2px]" /> icon: !hubs.includes(systemId) ? (
), <MapAddIcon className="mr-1 relative left-[-2px]" />
command: onHubToggle, ) : (
}, <MapDeleteIcon className="mr-1 relative left-[-2px]" />
),
command: onHubToggle,
},
]
: []),
...(!systemOnMap ...(!systemOnMap
? [ ? [
{ {
@@ -94,15 +200,18 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
systems, systems,
getJumpPlannerMenu, getJumpPlannerMenu,
getWaypointMenu, getWaypointMenu,
getStationsMenu,
hubs, hubs,
onHubToggle, onHubToggle,
onAddSystem, onAddSystem,
onOpenSettings, onOpenSettings,
toggleHubCommand,
routes,
]); ]);
return ( return (
<> <>
<ContextMenu model={items} ref={contextMenuRef} breakpoint="767px" /> <ContextMenu className={classes.ContextMenu} model={items} ref={contextMenuRef} breakpoint="767px" />
</> </>
); );
}; };
@@ -0,0 +1,90 @@
import { RouteStationSummary } from '@/hooks/Mapper/types/routes.ts';
const ROMAN_VALUES: Record<string, number> = {
I: 1,
V: 5,
X: 10,
L: 50,
C: 100,
D: 500,
M: 1000,
};
const MAX_DISTANCE = Number.MAX_SAFE_INTEGER;
const romanToInt = (value: string): number | null => {
const chars = value.toUpperCase().split('');
if (chars.length === 0 || chars.some(char => ROMAN_VALUES[char] === undefined)) {
return null;
}
let total = 0;
let prev = 0;
for (let i = chars.length - 1; i >= 0; i--) {
const current = ROMAN_VALUES[chars[i]];
if (current < prev) {
total -= current;
} else {
total += current;
prev = current;
}
}
return total;
};
const parseOrbitIndex = (value: string | undefined): number | null => {
if (!value) {
return null;
}
const trimmed = value.trim();
const asInt = Number.parseInt(trimmed, 10);
if (!Number.isNaN(asInt) && `${asInt}` === trimmed) {
return asInt;
}
return romanToInt(trimmed);
};
const extractPlanetOrbit = (name: string): number | null => {
const firstPart = name.split(' - ')[0] ?? '';
const match = firstPart.match(/([IVXLCDM]+|\d+)(?:\s*\([^)]*\))?$/i);
return parseOrbitIndex(match?.[1]);
};
const extractMoonOrbit = (name: string): number | null => {
const match = name.match(/\bMoon\s+([IVXLCDM]+|\d+)\b/i);
return parseOrbitIndex(match?.[1]);
};
const stationSortKey = (station: RouteStationSummary): [number, number, string, number] => {
return [
extractPlanetOrbit(station.station_name) ?? MAX_DISTANCE,
// If there is no moon in the station name, treat it as closer than moon orbits.
extractMoonOrbit(station.station_name) ?? 0,
station.station_name.toLowerCase(),
station.station_id,
];
};
export const sortStationsByDistance = (stations: RouteStationSummary[]): RouteStationSummary[] => {
return [...stations].sort((a, b) => {
const aKey = stationSortKey(a);
const bKey = stationSortKey(b);
for (let i = 0; i < aKey.length; i++) {
if (aKey[i] < bKey[i]) {
return -1;
}
if (aKey[i] > bKey[i]) {
return 1;
}
}
return 0;
});
};
@@ -38,7 +38,7 @@ export const useContextMenuSystemInfoHandlers = () => {
return; return;
} }
ref.current.toggleHubCommand(system); ref.current.toggleHubCommand?.(system);
setSystem(undefined); setSystem(undefined);
}, []); }, []);
@@ -6,6 +6,7 @@ export const useDetectSettingsChanged = () => {
storedSettings: { storedSettings: {
interfaceSettings, interfaceSettings,
settingsRoutes, settingsRoutes,
settingsRoutesBy,
settingsLocal, settingsLocal,
settingsSignatures, settingsSignatures,
settingsOnTheMap, settingsOnTheMap,
@@ -16,7 +17,15 @@ export const useDetectSettingsChanged = () => {
useEffect( useEffect(
() => setCounter(x => x + 1), () => setCounter(x => x + 1),
[interfaceSettings, settingsRoutes, settingsLocal, settingsSignatures, settingsOnTheMap, settingsKills], [
interfaceSettings,
settingsRoutes,
settingsRoutesBy,
settingsLocal,
settingsSignatures,
settingsOnTheMap,
settingsKills,
],
); );
return counter; return counter;
@@ -39,6 +39,10 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
return customInfo?.time_status === TimeStatus._1h; return customInfo?.time_status === TimeStatus._1h;
}, [customInfo]); }, [customInfo]);
const is4H = useMemo(() => {
return customInfo?.time_status === TimeStatus._4h;
}, [customInfo]);
const whClassStyle = useMemo(() => { const whClassStyle = useMemo(() => {
if (signature.type === 'K162' && k162TypeOption) { if (signature.type === 'K162' && k162TypeOption) {
const k162Data = wormholesData[k162TypeOption.whClassName]; const k162Data = wormholesData[k162TypeOption.whClassName];
@@ -65,6 +69,7 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg"> <svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">
<rect y="1" width="13" height="4" rx="2" className={whClassStyle} fill="currentColor" /> <rect y="1" width="13" height="4" rx="2" className={whClassStyle} fill="currentColor" />
{isEOL && <rect x="4" width="5" height="6" rx="1" className={clsx(classes.Eol)} fill="#a153ac" />} {isEOL && <rect x="4" width="5" height="6" rx="1" className={clsx(classes.Eol)} fill="#a153ac" />}
{is4H && <rect x="4" width="5" height="6" rx="1" className={clsx(classes.Eol)} fill="#d8b4fe" />}
</svg> </svg>
</div> </div>
</WdTooltipWrapper> </WdTooltipWrapper>
@@ -72,7 +72,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const { const {
storedSettings: { interfaceSettings }, storedSettings: { interfaceSettings },
data: { systemSignatures: mapSystemSignatures }, data: { systemSignatures: mapSystemSignatures, pings },
} = useMapRootState(); } = useMapRootState();
const systemStaticInfo = useMemo(() => { const systemStaticInfo = useMemo(() => {
@@ -108,7 +108,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
visibleNodes, visibleNodes,
showKSpaceBG, showKSpaceBG,
isThickConnections, isThickConnections,
pings,
systemHighlighted, systemHighlighted,
}, },
outCommand, outCommand,
@@ -1,7 +1,7 @@
import { MarkdownComment } from '@/hooks/Mapper/components/mapInterface/components/Comments/components'; import { MarkdownComment } from '@/hooks/Mapper/components/mapInterface/components/Comments/components';
import { useEffect, useRef, useState } from 'react';
import { CommentType } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommentType } from '@/hooks/Mapper/types';
import { useEffect, useMemo, useRef, useState } from 'react';
export interface CommentsProps {} export interface CommentsProps {}
@@ -14,7 +14,9 @@ export const Comments = ({}: CommentsProps) => {
comments: { loadComments, comments, lastUpdateKey }, comments: { loadComments, comments, lastUpdateKey },
} = useMapRootState(); } = useMapRootState();
const [systemId] = selectedSystems; const systemId = useMemo(() => {
return +selectedSystems[0];
}, [selectedSystems]);
const ref = useRef({ loadComments, systemId }); const ref = useRef({ loadComments, systemId });
ref.current = { loadComments, systemId }; ref.current = { loadComments, systemId };
@@ -1,4 +1,3 @@
import classes from './MarkdownComment.module.scss';
import clsx from 'clsx'; import clsx from 'clsx';
import { import {
InfoDrawer, InfoDrawer,
@@ -49,7 +48,11 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
<> <>
<InfoDrawer <InfoDrawer
labelClassName="mb-[3px]" labelClassName="mb-[3px]"
className={clsx(classes.MarkdownCommentRoot, 'p-1 bg-stone-700/20 ')} className={clsx(
'p-1 bg-stone-700/20',
'text-[12px] leading-[1.2] text-stone-300 break-words',
'bg-gradient-to-r from-stone-600/40 via-stone-600/10 to-stone-600/0',
)}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
title={ title={
@@ -0,0 +1,9 @@
.CERoot {
@apply border border-stone-400/30 rounded-[2px];
:global {
.cm-content {
@apply bg-stone-600/40;
}
}
}
@@ -3,9 +3,10 @@ import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api'; import { PrimeIcons } from 'primereact/api';
import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor'; import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor';
import { useHotkey } from '@/hooks/Mapper/hooks'; import { useHotkey } from '@/hooks/Mapper/hooks';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import { OutCommand } from '@/hooks/Mapper/types'; import { OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import classes from './CommentsEditor.module.scss';
export interface CommentsEditorProps {} export interface CommentsEditorProps {}
@@ -18,7 +19,9 @@ export const CommentsEditor = ({}: CommentsEditorProps) => {
outCommand, outCommand,
} = useMapRootState(); } = useMapRootState();
const [systemId] = selectedSystems; const systemId = useMemo(() => {
return +selectedSystems[0];
}, [selectedSystems]);
const ref = useRef({ outCommand, systemId, textVal }); const ref = useRef({ outCommand, systemId, textVal });
ref.current = { outCommand, systemId, textVal }; ref.current = { outCommand, systemId, textVal };
@@ -48,6 +51,7 @@ export const CommentsEditor = ({}: CommentsEditorProps) => {
return ( return (
<MarkdownEditor <MarkdownEditor
className={classes.CERoot}
value={textVal} value={textVal}
onChange={setTextVal} onChange={setTextVal}
overlayContent={ overlayContent={
@@ -1,9 +1,9 @@
.CERoot { .CERoot {
@apply border border-stone-400/30 rounded-[2px]; @apply border border-stone-500/30 rounded-[2px];
:global { :global {
.cm-content { .cm-content {
@apply bg-stone-600/40; @apply bg-stone-950/70;
} }
.cm-scroller { .cm-scroller {
@@ -44,9 +44,17 @@ export interface MarkdownEditorProps {
overlayContent?: ReactNode; overlayContent?: ReactNode;
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
height?: string;
className?: string;
} }
export const MarkdownEditor = ({ value, onChange, overlayContent }: MarkdownEditorProps) => { export const MarkdownEditor = ({
value,
onChange,
overlayContent,
height = '70px',
className,
}: MarkdownEditorProps) => {
const [hasShift, setHasShift] = useState(false); const [hasShift, setHasShift] = useState(false);
const refData = useRef({ onChange }); const refData = useRef({ onChange });
@@ -66,9 +74,9 @@ export const MarkdownEditor = ({ value, onChange, overlayContent }: MarkdownEdit
<div className={clsx(classes.MarkdownEditor, 'relative')}> <div className={clsx(classes.MarkdownEditor, 'relative')}>
<CodeMirror <CodeMirror
value={value} value={value}
height="70px" height={height}
extensions={CODE_MIRROR_EXTENSIONS} extensions={CODE_MIRROR_EXTENSIONS}
className={classes.CERoot} className={clsx(classes.CERoot, className)}
theme={oneDark} theme={oneDark}
onChange={handleOnChange} onChange={handleOnChange}
placeholder="Start typing..." placeholder="Start typing..."
@@ -121,6 +121,7 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
useEffect(() => { useEffect(() => {
if (!ping) { if (!ping) {
setIsShow(false);
return; return;
} }
@@ -161,27 +162,26 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
}; };
}, [interfaceSettings]); }, [interfaceSettings]);
if (!ping) { const isShowSelectedSystem = ping && selectedSystem != null && selectedSystem !== ping.solar_system_id;
return null;
}
const isShowSelectedSystem = selectedSystem != null && selectedSystem !== ping.solar_system_id;
// Only render Toast when there's a ping
return ( return (
<> <>
<Toast {ping && (
position={placement as never} <Toast
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)} key={ping.id}
ref={toast} position={placement as never}
content={({ message }) => ( className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
<section ref={toast}
className={clsx( content={({ message }) => (
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]', <section
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70', className={clsx(
)} 'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
> 'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
<div className="flex gap-3"> )}
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i> >
<div className="flex gap-3">
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
<div className="flex flex-col gap-1 w-full"> <div className="flex flex-col gap-1 w-full">
<div className="flex justify-between"> <div className="flex justify-between">
<div> <div>
@@ -253,28 +253,33 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
{/*/>*/} {/*/>*/}
</div> </div>
</section> </section>
)} )}
></Toast> ></Toast>
)}
<WdButton {ping && (
icon="pi pi-bell" <>
severity="warning" <WdButton
aria-label="Notification" icon="pi pi-bell"
size="small" severity="warning"
className="w-[33px] h-[33px]" aria-label="Notification"
outlined size="small"
onClick={handleClickShow} className="w-[33px] h-[33px]"
disabled={isShow} outlined
/> onClick={handleClickShow}
disabled={isShow}
/>
<ConfirmPopup <ConfirmPopup
target={cfRef.current} target={cfRef.current}
visible={cfVisible} visible={cfVisible}
onHide={cfHide} onHide={cfHide}
message="Are you sure you want to delete ping?" message="Are you sure you want to delete ping?"
icon="pi pi-exclamation-triangle text-orange-400" icon="pi pi-exclamation-triangle text-orange-400"
accept={removePing} accept={removePing}
/> />
</>
)}
</> </>
); );
}; };
@@ -3,9 +3,9 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useSystemInfo } from '@/hooks/Mapper/components/hooks'; import { useSystemInfo } from '@/hooks/Mapper/components/hooks';
import { import {
SOLAR_SYSTEM_CLASS_IDS, SOLAR_SYSTEM_CLASS_IDS,
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS, SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME, WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
} from '@/hooks/Mapper/components/map/constants.ts'; } from '@/hooks/Mapper/components/map/constants.ts';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent'; import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts'; import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
@@ -91,7 +91,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
if (k162TypeInfo) { if (k162TypeInfo) {
// Check if the k162Type matches our target system class // Check if the k162Type matches our target system class
return customInfo.k162Type === targetSystemClassGroup; return k162TypeInfo.value.includes(targetSystemClassGroup);
} }
} }
@@ -8,8 +8,8 @@ import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { Dialog } from 'primereact/dialog'; import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield'; import { IconField } from 'primereact/iconfield';
import { InputText } from 'primereact/inputtext'; import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor';
interface SystemSettingsDialog { interface SystemSettingsDialog {
systemId: string; systemId: string;
@@ -214,13 +214,9 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label htmlFor="username">Description</label> <label htmlFor="username">Description</label>
<InputTextarea <div className="h-[200px]">
autoResize <MarkdownEditor value={description} onChange={e => setDescription(e)} height="180px" />
rows={5} </div>
cols={30}
value={description}
onChange={e => setDescription(e.target.value)}
/>
</div> </div>
</div> </div>
@@ -7,6 +7,7 @@ import {
SystemStructures, SystemStructures,
WRoutesPublic, WRoutesPublic,
WRoutesUser, WRoutesUser,
WRoutesBy,
WSystemKills, WSystemKills,
} from '@/hooks/Mapper/components/mapInterface/widgets'; } from '@/hooks/Mapper/components/mapInterface/widgets';
@@ -18,6 +19,7 @@ export enum WidgetsIds {
signatures = 'signatures', signatures = 'signatures',
local = 'local', local = 'local',
routes = 'routes', routes = 'routes',
routesBy = 'routesBy',
structures = 'structures', structures = 'structures',
kills = 'kills', kills = 'kills',
comments = 'comments', comments = 'comments',
@@ -60,6 +62,13 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
zIndex: 0, zIndex: 0,
content: () => <WRoutesPublic />, content: () => <WRoutesPublic />,
}, },
{
id: WidgetsIds.routesBy,
position: { x: 10, y: 740 },
size: { width: 510, height: 200 },
zIndex: 0,
content: () => <WRoutesBy />,
},
{ {
id: WidgetsIds.userRoutes, id: WidgetsIds.userRoutes,
position: { x: 10, y: 10 }, position: { x: 10, y: 10 },
@@ -112,6 +121,10 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
id: WidgetsIds.routes, id: WidgetsIds.routes,
label: 'Routes', label: 'Routes',
}, },
{
id: WidgetsIds.routesBy,
label: 'Routes By',
},
{ {
id: WidgetsIds.userRoutes, id: WidgetsIds.userRoutes,
label: 'User Routes', label: 'User Routes',
@@ -41,7 +41,7 @@ export const RoutesWidgetContent = () => {
const { const {
data: { selectedSystems, systems, isSubscriptionActive }, data: { selectedSystems, systems, isSubscriptionActive },
} = useMapRootState(); } = useMapRootState();
const { hubs = [], routesList, isRestricted, loading } = useRouteProvider(); const { hubs = [], routesList, isRestricted, loading, nohubsPlaceholder } = useRouteProvider();
const [systemId] = selectedSystems; const [systemId] = selectedSystems;
@@ -105,7 +105,11 @@ export const RoutesWidgetContent = () => {
} }
if (hubs.length === 0) { if (hubs.length === 0) {
return <div className="w-full h-full flex justify-center items-center select-none">Routes not set</div>; return (
<div className="w-full h-full flex justify-center items-center select-none">
{nohubsPlaceholder ?? 'Routes not set'}
</div>
);
} }
return ( return (
@@ -129,7 +133,6 @@ export const RoutesWidgetContent = () => {
offset: 10, offset: 10,
}} }}
/> />
<SystemView <SystemView
systemId={route.destination.toString()} systemId={route.destination.toString()}
className={clsx('select-none text-center cursor-context-menu')} className={clsx('select-none text-center cursor-context-menu')}
@@ -138,7 +141,7 @@ export const RoutesWidgetContent = () => {
showCustomName showCustomName
/> />
</div> </div>
<div className="text-right pl-1">{route.has_connection ? route.systems?.length ?? 2 : ''}</div> <div className="text-right pl-1">{route.has_connection ? (route.systems?.length ?? 2) : ''}</div>
<div className="pl-2 pb-0.5"> <div className="pl-2 pb-0.5">
<RoutesList data={route} onContextMenu={handleContextMenu} /> <RoutesList data={route} onContextMenu={handleContextMenu} />
</div> </div>
@@ -147,9 +150,7 @@ export const RoutesWidgetContent = () => {
})} })}
</div> </div>
</LoadingWrapper> </LoadingWrapper>
<ContextMenuSystemInfo <ContextMenuSystemInfo
hubs={hubs}
routes={preparedRoutes} routes={preparedRoutes}
systems={systems} systems={systems}
systemStatics={systemStatics} systemStatics={systemStatics}
@@ -162,9 +163,10 @@ export const RoutesWidgetContent = () => {
type RoutesWidgetCompProps = { type RoutesWidgetCompProps = {
title: ReactNode | string; title: ReactNode | string;
renderContent?: (content: ReactNode, compact: boolean) => ReactNode;
}; };
export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => { export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps) => {
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false); const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
const { data, update, addHubCommand } = useRouteProvider(); const { data, update, addHubCommand } = useRouteProvider();
@@ -183,7 +185,7 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
const onAddSystem = useCallback(() => setOpenAddSystem(true), []); const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback( const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => addHubCommand(item.value.toString()), async item => addHubCommand?.(item.value.toString()),
[addHubCommand], [addHubCommand],
); );
@@ -191,15 +193,17 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
<Widget <Widget
label={ label={
<div className="flex justify-between items-center text-xs w-full" ref={ref}> <div className="flex justify-between items-center text-xs w-full" ref={ref}>
<span className="select-none">{title}</span> <div className="select-none flex items-center gap-2">{title}</div>
<LayoutEventBlocker className="flex items-center gap-2"> <LayoutEventBlocker className="flex items-center gap-2">
<WdImgButton {addHubCommand && (
className={PrimeIcons.PLUS_CIRCLE} <WdImgButton
onClick={onAddSystem} className={PrimeIcons.PLUS_CIRCLE}
tooltip={{ onClick={onAddSystem}
content: 'Click here to add new system to routes', tooltip={{
}} content: 'Click here to add new system to routes',
/> }}
/>
)}
<WdTooltipWrapper content="Show shortest route" position={TooltipPosition.top}> <WdTooltipWrapper content="Show shortest route" position={TooltipPosition.top}>
<WdCheckbox <WdCheckbox
@@ -223,24 +227,38 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
</div> </div>
} }
> >
<RoutesWidgetContent /> {renderContent ? (
renderContent(
<div className="h-full overflow-auto bg-opacity-5 custom-scrollbar">
<RoutesWidgetContent />
</div>,
compact,
)
) : (
<div className="h-full overflow-auto bg-opacity-5 custom-scrollbar">
<RoutesWidgetContent />
</div>
)}
<RoutesSettingsDialog visible={routeSettingsVisible} setVisible={setRouteSettingsVisible} /> <RoutesSettingsDialog visible={routeSettingsVisible} setVisible={setRouteSettingsVisible} />
<AddSystemDialog {addHubCommand && (
title="Add system to routes" <AddSystemDialog
visible={openAddSystem} title="Add system to routes"
setVisible={() => setOpenAddSystem(false)} visible={openAddSystem}
onSubmit={handleSubmitAddSystem} setVisible={() => setOpenAddSystem(false)}
/> onSubmit={handleSubmitAddSystem}
/>
)}
</Widget> </Widget>
); );
}; };
export const RoutesWidget = forwardRef<RoutesImperativeHandle, RoutesWidgetProps & RoutesWidgetCompProps>( export const RoutesWidget = forwardRef<RoutesImperativeHandle, RoutesWidgetProps & RoutesWidgetCompProps>(
({ title, ...props }, ref) => { ({ title, renderContent, ...props }, ref) => {
return ( return (
<RoutesProvider {...props} ref={ref}> <RoutesProvider {...props} ref={ref}>
<RoutesWidgetComp title={title} /> <RoutesWidgetComp title={title} renderContent={renderContent} />
</RoutesProvider> </RoutesProvider>
); );
}, },
@@ -1 +1,2 @@
export * from './useLoadRoutes'; export * from './useLoadRoutes';
export * from './useLoadRoutesBy';
@@ -0,0 +1,71 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { flattenValues } from '@/hooks/Mapper/utils/flattenValues.ts';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types';
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
type UseLoadRoutesByProps = {
loadRoutesCommand: LoadRoutesCommand;
routesList: RoutesList | undefined;
data: RoutesType;
deps?: unknown[];
};
export const useLoadRoutesBy = ({
data: routesSettings,
loadRoutesCommand,
routesList,
deps = [],
}: UseLoadRoutesByProps) => {
const [loading, setLoading] = useState(false);
const {
data: { selectedSystems },
} = useMapRootState();
const prevSys = usePrevious(selectedSystems);
const ref = useRef({ prevSys, selectedSystems });
ref.current = { prevSys, selectedSystems };
const loadRoutes = useCallback(
(systemId: string, settings: RoutesType) => {
loadRoutesCommand(systemId, settings);
setLoading(true);
},
[loadRoutesCommand],
);
useMapEventListener(event => {
if (event.name === Commands.routesListBy) {
setLoading(false);
}
});
useEffect(() => {
setLoading(false);
}, [routesList]);
useEffect(() => {
if (selectedSystems.length !== 1) {
return;
}
const [systemId] = selectedSystems;
loadRoutes(systemId, routesSettings);
}, [loadRoutes, selectedSystems, ...flattenValues(routesSettings), ...deps]);
return { loading, loadRoutes, setLoading };
};
@@ -12,9 +12,10 @@ export type RoutesWidgetProps = {
routesList: RoutesList | undefined; routesList: RoutesList | undefined;
loading: boolean; loading: boolean;
addHubCommand: AddHubCommand; addHubCommand?: AddHubCommand;
toggleHubCommand: ToggleHubCommand; toggleHubCommand?: ToggleHubCommand;
isRestricted?: boolean; isRestricted?: boolean;
nohubsPlaceholder?: string;
}; };
export type RoutesProviderInnerProps = RoutesWidgetProps; export type RoutesProviderInnerProps = RoutesWidgetProps;
@@ -2,7 +2,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts'; import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { getSystemById, sortWHClasses } from '@/hooks/Mapper/helpers'; import { getSystemById, sortWHClasses } from '@/hooks/Mapper/helpers';
import { InfoDrawer, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit'; import { InfoDrawer, MarkdownTextViewer, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic'; import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
interface SystemInfoContentProps { interface SystemInfoContentProps {
@@ -51,7 +51,7 @@ export const SystemInfoContent = ({ systemId }: SystemInfoContentProps) => {
</div> </div>
} }
> >
<div className="break-words">{description}</div> <MarkdownTextViewer>{description}</MarkdownTextViewer>
</InfoDrawer> </InfoDrawer>
)} )}
</div> </div>
@@ -1,6 +1,16 @@
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types'; import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
import { FINAL_DURATION_MS } from '../constants'; import { FINAL_DURATION_MS } from '../constants';
// Strip frontend-only fields that should never be sent to the backend.
// "linked_system" is an object the frontend uses; the backend expects "linked_system_id" (integer)
// which is set via a separate linkSignatureToSystem call.
function stripFrontendFields(s: ExtendedSystemSignature) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { linked_system, pendingDeletion, pendingAddition, pendingUntil, finalTimeoutId, character_name, ...rest } =
s as any;
return rest;
}
export function prepareUpdatePayload( export function prepareUpdatePayload(
systemId: string, systemId: string,
added: ExtendedSystemSignature[], added: ExtendedSystemSignature[],
@@ -9,9 +19,9 @@ export function prepareUpdatePayload(
) { ) {
return { return {
system_id: systemId, system_id: systemId,
added: added.map(s => ({ ...s })), added: added.map(stripFrontendFields),
updated: updated.map(s => ({ ...s })), updated: updated.map(stripFrontendFields),
removed: removed.map(s => ({ ...s })), removed: removed.map(stripFrontendFields),
}; };
} }
@@ -35,7 +35,7 @@ export const useSignatureFetching = ({ systemId, settings, signaturesRef, setSig
const extended = serverSigs.map(s => ({ const extended = serverSigs.map(s => ({
...s, ...s,
character_name: characters.find(c => c.eve_id === s.character_eve_id)?.name, character_name: s.character_name ?? characters.find(c => c.eve_id === s.character_eve_id)?.name,
})) as ExtendedSystemSignature[]; })) as ExtendedSystemSignature[];
setSignatures(() => extended); setSignatures(() => extended);
@@ -1,4 +1,4 @@
import React, { useCallback, ClipboardEvent, useRef } from 'react'; import React, { useCallback, ClipboardEvent, useRef, useState } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth'; import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { import {
@@ -13,7 +13,9 @@ import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemStructuresContent } from './SystemStructuresContent/SystemStructuresContent'; import { SystemStructuresContent } from './SystemStructuresContent/SystemStructuresContent';
import { useSystemStructures } from './hooks/useSystemStructures'; import { useSystemStructures } from './hooks/useSystemStructures';
import { processSnippetText } from './helpers'; import { processSnippetText, StructureItem } from './helpers';
import { SystemStructuresOwnersDialog } from './SystemStructuresOwnersDialog/SystemStructuresOwnersDialog';
import clsx from 'clsx';
export const SystemStructures: React.FC = () => { export const SystemStructures: React.FC = () => {
const { const {
@@ -24,6 +26,7 @@ export const SystemStructures: React.FC = () => {
const isNotSelectedSystem = selectedSystems.length !== 1; const isNotSelectedSystem = selectedSystems.length !== 1;
const { structures, handleUpdateStructures } = useSystemStructures({ systemId, outCommand }); const { structures, handleUpdateStructures } = useSystemStructures({ systemId, outCommand });
const [showEditDialog, setShowEditDialog] = useState(false);
const labelRef = useRef<HTMLDivElement>(null); const labelRef = useRef<HTMLDivElement>(null);
const isCompact = useMaxWidth(labelRef, 260); const isCompact = useMaxWidth(labelRef, 260);
@@ -48,6 +51,18 @@ export const SystemStructures: React.FC = () => {
[processClipboard], [processClipboard],
); );
const handleSave = (updatedStructures: StructureItem[]) => {
handleUpdateStructures(updatedStructures)
}
const handleOpenDialog = useCallback(() => {
setShowEditDialog(true)
}, [])
const handleCloseDialog = useCallback(() => {
setShowEditDialog(false)
}, [])
const handlePasteTimer = useCallback(async () => { const handlePasteTimer = useCallback(async () => {
try { try {
const text = await navigator.clipboard.readText(); const text = await navigator.clipboard.readText();
@@ -71,8 +86,19 @@ export const SystemStructures: React.FC = () => {
</div> </div>
<LayoutEventBlocker className="flex gap-2.5"> <LayoutEventBlocker className="flex gap-2.5">
{structures.length > 1 && (
<WdImgButton
className={clsx(PrimeIcons.USER_EDIT, 'text-sky-400 hover:text-sky-200 transition duration-300')}
onClick={handleOpenDialog}
tooltip={{
position: TooltipPosition.left,
// @ts-ignore
content: 'Update all structure owners',
}}
/>
)}
<WdImgButton <WdImgButton
className={`${PrimeIcons.CLOCK} text-sky-400 hover:text-sky-200 transition duration-300`} className={clsx(PrimeIcons.CLOCK, 'text-sky-400 hover:text-sky-200 transition duration-300')}
onClick={handlePasteTimer} onClick={handlePasteTimer}
tooltip={{ tooltip={{
position: TooltipPosition.left, position: TooltipPosition.left,
@@ -117,6 +143,15 @@ export const SystemStructures: React.FC = () => {
<SystemStructuresContent structures={structures} onUpdateStructures={handleUpdateStructures} /> <SystemStructuresContent structures={structures} onUpdateStructures={handleUpdateStructures} />
)} )}
</Widget> </Widget>
{showEditDialog && (
<SystemStructuresOwnersDialog
visible={showEditDialog}
structures={structures}
onClose={handleCloseDialog}
onSave={handleSave}
/>
)}
</div> </div>
); );
}; };
@@ -4,7 +4,14 @@ import { AutoComplete } from 'primereact/autocomplete';
import { Calendar } from 'primereact/calendar'; import { Calendar } from 'primereact/calendar';
import clsx from 'clsx'; import clsx from 'clsx';
import { formatToISO, statusesRequiringTimer, StructureItem, StructureStatus } from '../helpers'; import {
calendarDateToUtcIso,
formatToISO,
statusesRequiringTimer,
StructureItem,
StructureStatus,
utcToCalendarDate,
} from '../helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types'; import { OutCommand } from '@/hooks/Mapper/types';
import { WdButton } from '@/hooks/Mapper/components/ui-kit'; import { WdButton } from '@/hooks/Mapper/components/ui-kit';
@@ -72,7 +79,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
// If this is the endTime (Date from Calendar), we store as ISO or string: // If this is the endTime (Date from Calendar), we store as ISO or string:
if (field === 'endTime' && val instanceof Date) { if (field === 'endTime' && val instanceof Date) {
return { ...prev, endTime: val.toISOString() }; return { ...prev, endTime: calendarDateToUtcIso(val) };
} }
return { ...prev, [field]: val }; return { ...prev, [field]: val };
@@ -188,7 +195,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
Timer <br /> (Eve Time): Timer <br /> (Eve Time):
</span> </span>
<Calendar <Calendar
value={editData.endTime ? new Date(editData.endTime) : undefined} value={editData.endTime ? utcToCalendarDate(editData.endTime) : undefined}
onChange={e => handleChange('endTime', e.value ?? '')} onChange={e => handleChange('endTime', e.value ?? '')}
showTime showTime
hourFormat="24" hourFormat="24"
@@ -0,0 +1,31 @@
.systemStructuresOwnersDialog {
.p-dialog-content {
background-color: var(--surface-800) !important;
}
.p-dialog-header {
background-color: var(--surface-700);
color: var(--text-color);
}
.p-dialog-header-icon,
.p-dialog-header-title {
color: var(--gray-200);
}
.p-inputtext {
background-color: #2a2a2a !important;
color: #ddd !important;
font-size: 12px !important;
padding: 0.25rem 0.5rem !important;
}
.p-dialog-footer {
.p-button {
font-size: 12px !important;
padding: 0.3rem 0.75rem !important;
}
}
}
@@ -0,0 +1,180 @@
import clsx from 'clsx';
import { AutoComplete } from 'primereact/autocomplete';
import { Dialog } from 'primereact/dialog';
import React, { useCallback, useState } from 'react';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useToast } from '@/hooks/Mapper/ToastProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { StructureItem } from '../helpers';
interface StructuresOwnersEditDialogProps {
visible: boolean;
structures: StructureItem[];
onClose: () => void;
onSave: (updatedStuctures: StructureItem[]) => void;
}
export const SystemStructuresOwnersDialog: React.FC<StructuresOwnersEditDialogProps> = ({
visible,
structures,
onClose,
onSave,
}) => {
const [ownerInput, setOwnerInput] = useState('');
const [ownerSuggestions, setOwnerSuggestions] = useState<{ label: string; value: string }[]>([]);
const { outCommand } = useMapRootState();
const { show } = useToast();
const [prevQuery, setPrevQuery] = useState('');
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
const [editData, setEditData] = useState<StructureItem[]>(structures);
// Searching corporation owners via auto-complete
const searchOwners = useCallback(
async (e: { query: string }) => {
const newQuery = e.query.trim();
if (!newQuery) {
setOwnerSuggestions([]);
return;
}
// If user typed more text but we have partial match in prevResults
if (newQuery.startsWith(prevQuery) && prevResults.length > 0) {
const filtered = prevResults.filter(item => item.label.toLowerCase().includes(newQuery.toLowerCase()));
setOwnerSuggestions(filtered);
return;
}
try {
// TODO fix it
const { results = [] } = await outCommand({
type: OutCommand.getCorporationNames,
data: { search: newQuery },
});
setOwnerSuggestions(results);
setPrevQuery(newQuery);
setPrevResults(results);
} catch (err) {
show({
severity: 'error',
summary: 'Failed to fetch owners',
detail: `${err}`,
life: 10000,
});
}
},
[prevQuery, prevResults, outCommand],
);
// when user picks a corp from auto-complete
const handleSelectOwner = (selected: { label: string; value: string }) => {
setOwnerInput(selected.label);
setEditData(
structures.map(item => {
return { ...item, ownerName: selected.label, ownerId: selected.value };
}),
);
};
const handleSaveClick = async () => {
if (!editData) return;
// Get all unique owner IDs that need ticker lookup
const allOwnerIds = editData.filter(x => x.ownerId != null).map(x => x.ownerId as string);
const uniqueOwnerIds = [...new Set(allOwnerIds)];
// Fetch all tickers in parallel
const tickerResults = await Promise.all(
uniqueOwnerIds.map(async ownerId => {
try {
const { ticker } = await outCommand({
type: OutCommand.getCorporationTicker,
data: { corp_id: ownerId },
});
return { ownerId, ticker: ticker ?? '' };
} catch (err) {
console.error('Failed to fetch ticker for ownerId:', ownerId, err);
return { ownerId, ticker: '' };
}
}),
);
// Create a map of ownerId -> ticker for quick lookup
const tickerMap = new Map(tickerResults.map(r => [r.ownerId, r.ticker]));
// Create new array with updated values (no mutation)
const updatedStructures = editData.map(structure => {
if (!structure.ownerId) {
return structure;
}
return {
...structure,
ownerTicker: tickerMap.get(structure.ownerId) ?? '',
};
});
onSave(updatedStructures);
onClose();
};
return (
<Dialog
visible={visible}
onHide={onClose}
header={'Update All Structure Owners'}
className={clsx('myStructuresOwnersDialog', 'text-stone-200 w-full max-w-md')}
>
<div className="flex flex-col gap-2 text-[14px]">
<div className="flex gap-2">
Updating the corporation name below will update all structures currently saved within the system.
</div>
<hr />
<div className="flex flex-col gap-2">
<label className="grid grid-cols-[100px_1fr] gap-2 items-start mt-2">
<span className="mt-1">Structures to update:</span>
<ul>
{structures &&
structures.map((item, i) => (
<li key={i}>
{item.structureType || 'Unknown Type'} - {item.name}
</li>
))}
</ul>
</label>
</div>
<hr />
<div>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<span>Owner:</span>
<AutoComplete
id="owner"
value={ownerInput}
suggestions={ownerSuggestions}
completeMethod={searchOwners}
minLength={3}
delay={400}
field="label"
placeholder="Corporation name..."
onChange={e => setOwnerInput(e.value)}
onSelect={e => handleSelectOwner(e.value)}
/>
</label>
</div>
</div>
<div className="flex justify-end items-center gap-2 mt-4">
<WdButton label="Save" className="p-button-sm" onClick={handleSaveClick} />
</div>
</Dialog>
);
};
@@ -43,6 +43,29 @@ export function mapServerStructure(serverData: any): StructureItem {
}; };
} }
export function utcToCalendarDate(utcIso: string): Date {
// Parse ISO components manually to avoid browser quirks with
// 6-digit microsecond precision from Elixir's :utc_datetime_usec.
const m = utcIso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/);
if (m) {
const [, yr, mo, dy, hr, mi, sc] = m;
return new Date(+yr, +mo - 1, +dy, +hr, +mi, +sc);
}
// Fallback for non-ISO strings
const d = new Date(utcIso);
return new Date(d.getTime() + d.getTimezoneOffset() * 60_000);
}
export function calendarDateToUtcIso(localDate: Date): string {
// Read local-time components (which represent EVE/UTC time) and
// build the ISO string directly — no timezone arithmetic needed.
const pad = (n: number) => String(n).padStart(2, '0');
return (
`${localDate.getFullYear()}-${pad(localDate.getMonth() + 1)}-${pad(localDate.getDate())}` +
`T${pad(localDate.getHours())}:${pad(localDate.getMinutes())}:${pad(localDate.getSeconds())}.000Z`
);
}
export function formatToISO(datetimeLocal: string): string { export function formatToISO(datetimeLocal: string): string {
if (!datetimeLocal) return ''; if (!datetimeLocal) return '';
@@ -0,0 +1,202 @@
import { useCallback, useMemo, useRef } from 'react';
import { RoutesWidget } from '@/hooks/Mapper/components/mapInterface/widgets';
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { useLoadRoutesBy } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { Dropdown } from 'primereact/dropdown';
import { SelectItemOptionsType } from 'primereact/selectitem';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import clsx from 'clsx';
import { RoutesByCategoryType, RoutesByScopeType, RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { DEFAULT_ROUTES_SETTINGS } from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { PrimeIcons } from 'primereact/api';
export type RoutesByType = RoutesByCategoryType;
type WRoutesByProps = {
type?: RoutesByType;
title?: string;
};
const ROUTES_BY_OPTIONS: SelectItemOptionsType = [
{
label: 'Blue Loot',
value: 'blueLoot',
icon: 'images/30747_64.png',
},
{
label: 'Red Loot',
value: 'redLoot',
icon: 'images/89219_64.png',
},
{
label: 'Thera',
value: 'thera',
icon: 'images/map.png',
},
{
label: 'Turnur',
value: 'turnur',
icon: 'images/map.png',
},
{
label: 'Security Office',
value: 'so_cleaning',
icon: 'images/concord-so.png',
},
{
label: 'Trade Hubs',
value: 'trade_hubs',
icon: 'images/market.png',
},
];
const ROUTES_BY_SECURITY_OPTIONS = [
{ label: 'All', value: 'ALL' },
{ label: 'High', value: 'HIGH' },
];
export const WRoutesBy = ({ type = 'blueLoot', title = 'Routes By' }: WRoutesByProps) => {
const {
outCommand,
storedSettings: { settingsRoutesBy, settingsRoutesByUpdate },
data,
} = useMapRootState();
const criteriaType = settingsRoutesBy.type ?? type;
const securityType = settingsRoutesBy.scope ?? 'ALL';
const routesSettings = settingsRoutesBy.routes ?? DEFAULT_ROUTES_SETTINGS;
const routesListBy = data.routesListBy;
const availableRoutesBy = data.availableRoutesBy;
const routesByOptions = useMemo(() => {
if (!availableRoutesBy || availableRoutesBy.length === 0) {
return ROUTES_BY_OPTIONS;
}
return ROUTES_BY_OPTIONS.filter(option => availableRoutesBy.includes(option.value as RoutesByType));
}, [availableRoutesBy]);
const resolvedCriteriaType = useMemo(() => {
const optionValues = routesByOptions.map(option => option.value as RoutesByType);
if (optionValues.length === 0) {
return criteriaType;
}
return optionValues.includes(criteriaType) ? criteriaType : optionValues[0];
}, [routesByOptions, criteriaType]);
const loadRoutesCommand: LoadRoutesCommand = useCallback(
async (systemId, currentRoutesSettings) => {
await outCommand({
type: OutCommand.getRoutesBy,
data: {
system_id: systemId,
type: resolvedCriteriaType,
securityType: securityType === 'HIGH' ? 'high' : 'both',
routes_settings: currentRoutesSettings,
},
});
},
[outCommand, resolvedCriteriaType, securityType],
);
const hubs = useMemo(() => routesListBy?.routes?.map(route => route.destination.toString()) ?? [], [routesListBy]);
const { loading: internalLoading } = useLoadRoutesBy({
data: routesSettings,
loadRoutesCommand,
routesList: routesListBy,
deps: [resolvedCriteriaType, securityType],
});
const updateRoutesSettings = useCallback(
(next: RoutesType) => settingsRoutesByUpdate(prev => ({ ...prev, routes: next })),
[settingsRoutesByUpdate],
);
const ref = useRef<HTMLDivElement>(null);
const compactSmall = useMaxWidth(ref, 180);
const compactMiddle = useMaxWidth(ref, 245);
const titleNode = useMemo(
() => (
<div className="flex items-center gap-2">
<span className="select-none">{title}</span>
<WdImgButton
className={PrimeIcons.QUESTION_CIRCLE}
tooltip={{
position: TooltipPosition.top,
content: 'Alpha map users can access only 1 route',
}}
/>
</div>
),
[title],
);
return (
<RoutesWidget
title={titleNode}
nohubsPlaceholder="Not found any destinations"
renderContent={(content /*, compact*/) => (
<div className="h-full grid grid-rows-[1fr_auto]" ref={ref}>
{content}
<div className="flex items-center gap-2 justify-end mb-2 px-2 pt-2">
{!compactSmall && (
<Dropdown
value={securityType}
options={ROUTES_BY_SECURITY_OPTIONS}
onChange={e => settingsRoutesByUpdate(prev => ({ ...prev, scope: e.value as RoutesByScopeType }))}
className="w-[90px] [&_span]:!text-[12px]"
/>
)}
<Dropdown
value={resolvedCriteriaType}
itemTemplate={e => (
<div className="flex items-center gap-2">
{e.icon && <img src={e.icon} height="18" width="18" />}
<span className="text-[12px]">{e.label}</span>
</div>
)}
valueTemplate={e => {
if (!e) {
return null;
}
if (compactMiddle) {
return (
<div className="flex items-center gap-2 min-w-[50px]">
{e.icon ? <img src={e.icon} height="18" width="18" /> : <span>{e.label}</span>}
</div>
);
}
return (
<div className="flex items-center gap-2">
{e.icon && <img src={e.icon} height="18" width="18" />}
<span className="text-[12px]">{e.label}</span>
</div>
);
}}
options={routesByOptions}
onChange={e => settingsRoutesByUpdate(prev => ({ ...prev, type: e.value as RoutesByCategoryType }))}
className={clsx({
['w-[130px]']: !compactMiddle,
['w-[65px]']: compactMiddle,
})}
/>
</div>
</div>
)}
data={routesSettings}
update={updateRoutesSettings}
hubs={hubs}
routesList={routesListBy}
loading={internalLoading}
/>
);
};
@@ -0,0 +1,2 @@
export { WRoutesBy } from './WRoutesBy';
export type { RoutesByType } from './WRoutesBy';
@@ -31,7 +31,7 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
storedSettings: { settingsKills }, storedSettings: { settingsKills },
} = useMapRootState(); } = useMapRootState();
const excludedSystems = useStableValue(settingsKills.excludedSystems); const excludedSystems = useStableValue(settingsKills.excludedSystems ?? []);
const effectiveSystemIds = useMemo(() => { const effectiveSystemIds = useMemo(() => {
if (showAllVisible) { if (showAllVisible) {
@@ -6,4 +6,5 @@ export * from './SystemStructures';
export * from './WSystemKills'; export * from './WSystemKills';
export * from './WRoutesUser'; export * from './WRoutesUser';
export * from './WRoutesPublic'; export * from './WRoutesPublic';
export * from './WRoutesBy';
export * from './CommentsWidget'; export * from './CommentsWidget';
@@ -9,6 +9,7 @@ import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/compone
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu'; import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
import { MapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings'; import { MapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings';
import { CharacterActivity } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity'; import { CharacterActivity } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity';
import { WormholeSignaturesDialog } from '@/hooks/Mapper/components/mapRootContent/components/WormholeSignaturesDialog';
import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandlers'; import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandlers';
import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog'; import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog';
import { useMapEventListener } from '@/hooks/Mapper/events'; import { useMapEventListener } from '@/hooks/Mapper/events';
@@ -34,6 +35,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
const [showOnTheMap, setShowOnTheMap] = useState(false); const [showOnTheMap, setShowOnTheMap] = useState(false);
const [showMapSettings, setShowMapSettings] = useState(false); const [showMapSettings, setShowMapSettings] = useState(false);
const [showTrackingDialog, setShowTrackingDialog] = useState(false); const [showTrackingDialog, setShowTrackingDialog] = useState(false);
const [showWormholeList, setShowWormholeList] = useState(false);
/* Important Notice - this solution needs for use one instance of MapInterface */ /* Important Notice - this solution needs for use one instance of MapInterface */
const mapInterface = isReady ? <MapInterface /> : null; const mapInterface = isReady ? <MapInterface /> : null;
@@ -41,6 +43,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []); const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []); const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []);
const handleShowTrackingDialog = useCallback(() => setShowTrackingDialog(true), []); const handleShowTrackingDialog = useCallback(() => setShowTrackingDialog(true), []);
const handleShowWormholesReference = useCallback(() => setShowWormholeList(true), []);
useMapEventListener(event => { useMapEventListener(event => {
if (event.name === Commands.showTracking) { if (event.name === Commands.showTracking) {
@@ -65,6 +68,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
onShowOnTheMap={handleShowOnTheMap} onShowOnTheMap={handleShowOnTheMap}
onShowMapSettings={handleShowMapSettings} onShowMapSettings={handleShowMapSettings}
onShowTrackingDialog={handleShowTrackingDialog} onShowTrackingDialog={handleShowTrackingDialog}
onShowWormholesReference={handleShowWormholesReference}
additionalContent={<PingsInterface hasLeftOffset />} additionalContent={<PingsInterface hasLeftOffset />}
/> />
</div> </div>
@@ -79,6 +83,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
onShowOnTheMap={handleShowOnTheMap} onShowOnTheMap={handleShowOnTheMap}
onShowMapSettings={handleShowMapSettings} onShowMapSettings={handleShowMapSettings}
onShowTrackingDialog={handleShowTrackingDialog} onShowTrackingDialog={handleShowTrackingDialog}
onShowWormholesReference={handleShowWormholesReference}
/> />
</div> </div>
</Topbar> </Topbar>
@@ -93,6 +98,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
{showTrackingDialog && ( {showTrackingDialog && (
<TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} /> <TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} />
)} )}
<WormholeSignaturesDialog visible={showWormholeList} onHide={() => setShowWormholeList(false)} />
{hasOldSettings && <OldSettingsDialog />} {hasOldSettings && <OldSettingsDialog />}
</Layout> </Layout>
@@ -1,4 +1,7 @@
import { Dialog } from 'primereact/dialog'; import { Dialog } from 'primereact/dialog';
import { Menu } from 'primereact/menu';
import { MenuItem } from 'primereact/menuitem';
import { useState, useCallback, useRef, useMemo } from 'react';
import { CharacterActivityContent } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivityContent.tsx'; import { CharacterActivityContent } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivityContent.tsx';
interface CharacterActivityProps { interface CharacterActivityProps {
@@ -6,17 +9,69 @@ interface CharacterActivityProps {
onHide: () => void; onHide: () => void;
} }
const periodOptions = [
{ value: 30, label: '30 Days' },
{ value: 365, label: '1 Year' },
{ value: null, label: 'All Time' },
];
export const CharacterActivity = ({ visible, onHide }: CharacterActivityProps) => { export const CharacterActivity = ({ visible, onHide }: CharacterActivityProps) => {
const [selectedPeriod, setSelectedPeriod] = useState<number | null>(30);
const menuRef = useRef<Menu>(null);
const handlePeriodChange = useCallback((days: number | null) => {
setSelectedPeriod(days);
}, []);
const menuItems: MenuItem[] = useMemo(
() => [
{
label: 'Period',
items: periodOptions.map(option => ({
label: option.label,
icon: selectedPeriod === option.value ? 'pi pi-check' : undefined,
command: () => handlePeriodChange(option.value),
})),
},
],
[selectedPeriod, handlePeriodChange],
);
const selectedPeriodLabel = useMemo(
() => periodOptions.find(opt => opt.value === selectedPeriod)?.label || 'All Time',
[selectedPeriod],
);
const headerIcons = (
<>
<button
type="button"
className="p-dialog-header-icon p-link"
onClick={e => menuRef.current?.toggle(e)}
aria-label="Filter options"
>
<span className="pi pi-bars" />
</button>
<Menu model={menuItems} popup ref={menuRef} />
</>
);
return ( return (
<Dialog <Dialog
header="Character Activity" header={
<div className="flex items-center gap-2">
<span>Character Activity</span>
<span className="text-xs text-stone-400">({selectedPeriodLabel})</span>
</div>
}
visible={visible} visible={visible}
className="w-[550px] max-h-[90vh]" className="w-[550px] max-h-[90vh]"
onHide={onHide} onHide={onHide}
dismissableMask dismissableMask
contentClassName="p-0 h-full flex flex-col" contentClassName="p-0 h-full flex flex-col"
icons={headerIcons}
> >
<CharacterActivityContent /> <CharacterActivityContent selectedPeriod={selectedPeriod} />
</Dialog> </Dialog>
); );
}; };
@@ -7,16 +7,28 @@ import {
} from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/helpers.tsx'; } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/helpers.tsx';
import { Column } from 'primereact/column'; import { Column } from 'primereact/column';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo } from 'react'; import { useMemo, useEffect } from 'react';
import { useCharacterActivityHandlers } from '@/hooks/Mapper/components/mapRootContent/hooks/useCharacterActivityHandlers';
export const CharacterActivityContent = () => { interface CharacterActivityContentProps {
selectedPeriod: number | null;
}
export const CharacterActivityContent = ({ selectedPeriod }: CharacterActivityContentProps) => {
const { const {
data: { characterActivityData }, data: { characterActivityData },
} = useMapRootState(); } = useMapRootState();
const { handleShowActivity } = useCharacterActivityHandlers();
const activity = useMemo(() => characterActivityData?.activity || [], [characterActivityData]); const activity = useMemo(() => characterActivityData?.activity || [], [characterActivityData]);
const loading = useMemo(() => characterActivityData?.loading !== false, [characterActivityData]); const loading = useMemo(() => characterActivityData?.loading !== false, [characterActivityData]);
// Reload activity data when period changes
useEffect(() => {
handleShowActivity(selectedPeriod);
}, [selectedPeriod, handleShowActivity]);
if (loading) { if (loading) {
return ( return (
<div className="flex flex-col items-center justify-center h-full w-full"> <div className="flex flex-col items-center justify-center h-full w-full">
@@ -3,7 +3,7 @@
} }
.SidebarOnTheMap { .SidebarOnTheMap {
width: 400px; width: 500px;
padding: 0 !important; padding: 0 !important;
:global { :global {
@@ -5,6 +5,7 @@ import {
ConnectionType, ConnectionType,
OutCommand, OutCommand,
Passage, Passage,
PassageWithSourceTarget,
SolarSystemConnection, SolarSystemConnection,
} from '@/hooks/Mapper/types'; } from '@/hooks/Mapper/types';
import clsx from 'clsx'; import clsx from 'clsx';
@@ -19,7 +20,7 @@ import { PassageCard } from './PassageCard';
const sortByDate = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime(); const sortByDate = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime();
const itemTemplate = (item: Passage, options: VirtualScrollerTemplateOptions) => { const itemTemplate = (item: PassageWithSourceTarget, options: VirtualScrollerTemplateOptions) => {
return ( return (
<div <div
className={clsx(classes.CharacterRow, 'w-full box-border', { className={clsx(classes.CharacterRow, 'w-full box-border', {
@@ -35,7 +36,7 @@ const itemTemplate = (item: Passage, options: VirtualScrollerTemplateOptions) =>
}; };
export interface ConnectionPassagesContentProps { export interface ConnectionPassagesContentProps {
passages: Passage[]; passages: PassageWithSourceTarget[];
} }
export const ConnectionPassages = ({ passages = [] }: ConnectionPassagesContentProps) => { export const ConnectionPassages = ({ passages = [] }: ConnectionPassagesContentProps) => {
@@ -113,6 +114,20 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
[outCommand], [outCommand],
); );
const preparedPassages = useMemo(() => {
if (!cnInfo) {
return [];
}
return passages
.sort((a, b) => sortByDate(b.inserted_at, a.inserted_at))
.map<PassageWithSourceTarget>(x => ({
...x,
source: x.from ? cnInfo.target : cnInfo.source,
target: x.from ? cnInfo.source : cnInfo.target,
}));
}, [cnInfo, passages]);
useEffect(() => { useEffect(() => {
if (!selectedConnection) { if (!selectedConnection) {
return; return;
@@ -145,12 +160,14 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
<InfoDrawer title="Connection" rightSide> <InfoDrawer title="Connection" rightSide>
<div className="flex justify-end gap-2 items-center"> <div className="flex justify-end gap-2 items-center">
<SystemView <SystemView
showCustomName
systemId={cnInfo.source} systemId={cnInfo.source}
className={clsx(classes.InfoTextSize, 'select-none text-center')} className={clsx(classes.InfoTextSize, 'select-none text-center')}
hideRegion hideRegion
/> />
<span className="pi pi-angle-double-right text-stone-500 text-[15px]"></span> <span className="pi pi-angle-double-right text-stone-500 text-[15px]"></span>
<SystemView <SystemView
showCustomName
systemId={cnInfo.target} systemId={cnInfo.target}
className={clsx(classes.InfoTextSize, 'select-none text-center')} className={clsx(classes.InfoTextSize, 'select-none text-center')}
hideRegion hideRegion
@@ -184,7 +201,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
{/* separator */} {/* separator */}
<div className="w-full h-px bg-neutral-800 px-0.5"></div> <div className="w-full h-px bg-neutral-800 px-0.5"></div>
<ConnectionPassages passages={passages} /> <ConnectionPassages passages={preparedPassages} />
</div> </div>
</Sidebar> </Sidebar>
); );
@@ -35,6 +35,10 @@
&.ThreeColumns { &.ThreeColumns {
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;
} }
&.FourColumns {
grid-template-columns: auto auto 1fr auto;
}
} }
.CardBorderLeftIsOwn { .CardBorderLeftIsOwn {
@@ -1,17 +1,19 @@
import clsx from 'clsx'; import clsx from 'clsx';
import classes from './PassageCard.module.scss'; import classes from './PassageCard.module.scss';
import { Passage } from '@/hooks/Mapper/types'; import { PassageWithSourceTarget } from '@/hooks/Mapper/types';
import { TimeAgo } from '@/hooks/Mapper/components/ui-kit'; import { SystemView, TimeAgo, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper'; import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts'; import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
import { useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { ZKB_ICON } from '@/hooks/Mapper/icons';
import { charEveWhoLink, charZKBLink } from '@/hooks/Mapper/helpers/linkHelpers.ts';
type PassageCardType = { type PassageCardType = {
// compact?: boolean; // compact?: boolean;
showShipName?: boolean; showShipName?: boolean;
// showSystem?: boolean; // showSystem?: boolean;
// useSystemsCache?: boolean; // useSystemsCache?: boolean;
} & Passage; } & PassageWithSourceTarget;
const SHIP_NAME_RX = /u'|'/g; const SHIP_NAME_RX = /u'|'/g;
export const getShipName = (name: string) => { export const getShipName = (name: string) => {
@@ -25,7 +27,7 @@ export const getShipName = (name: string) => {
}); });
}; };
export const PassageCard = ({ inserted_at, character: char, ship }: PassageCardType) => { export const PassageCard = ({ inserted_at, character: char, ship, source, target, from }: PassageCardType) => {
const isOwn = false; const isOwn = false;
const insertedAt = useMemo(() => { const insertedAt = useMemo(() => {
@@ -33,11 +35,46 @@ export const PassageCard = ({ inserted_at, character: char, ship }: PassageCardT
return date.toLocaleString(); return date.toLocaleString();
}, [inserted_at]); }, [inserted_at]);
const handleOpenZKB = useCallback(() => window.open(charZKBLink(char.eve_id), '_blank'), [char]);
const handleOpenEveWho = useCallback(() => window.open(charEveWhoLink(char.eve_id), '_blank'), [char]);
return ( return (
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')}> <div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')}>
<div className="flex flex-col justify-between px-2 py-1 gap-1"> <div className="flex flex-col justify-between px-2 py-1 gap-1">
{/*here icon and other*/} {/*here icon and other*/}
<div className={clsx(classes.CharRow, classes.ThreeColumns)}> <div className={clsx(classes.CharRow, classes.FourColumns)}>
<WdTooltipWrapper
position={TooltipPosition.top}
content={
<div className="flex justify-between gap-2 items-center">
<SystemView
showCustomName
systemId={source}
className="select-none text-center !text-[12px]"
hideRegion
/>
<span className="pi pi-angle-double-right text-stone-500 text-[15px]"></span>
<SystemView
showCustomName
systemId={target}
className="select-none text-center !text-[12px]"
hideRegion
/>
</div>
}
>
<div
className={clsx(
'transition-all transform ease-in duration-200',
'pi text-stone-500 text-[15px] w-[35px] h-[33px] !flex items-center justify-center border rounded-[6px]',
{
['pi-angle-double-right !text-orange-400 border-orange-400 hover:bg-orange-400/30']: from,
['pi-angle-double-left !text-stone-500/70 border-stone-500/70 hover:bg-stone-500/30']: !from,
},
)}
/>
</WdTooltipWrapper>
{/*portrait*/} {/*portrait*/}
<span <span
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')} className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
@@ -49,7 +86,7 @@ export const PassageCard = ({ inserted_at, character: char, ship }: PassageCardT
{/*here name and ship name*/} {/*here name and ship name*/}
<div className="grid gap-1 justify-between grid-cols-[max-content_1fr]"> <div className="grid gap-1 justify-between grid-cols-[max-content_1fr]">
{/*char name*/} {/*char name*/}
<div className="grid gap-1 grid-cols-[auto_1px_1fr]"> <div className="grid gap-1 grid-cols-[auto_1px_1fr_auto]">
<span <span
className={clsx(classes.MaxWidth, 'text-ellipsis overflow-hidden whitespace-nowrap', { className={clsx(classes.MaxWidth, 'text-ellipsis overflow-hidden whitespace-nowrap', {
[classes.CardBorderLeftIsOwn]: isOwn, [classes.CardBorderLeftIsOwn]: isOwn,
@@ -62,6 +99,21 @@ export const PassageCard = ({ inserted_at, character: char, ship }: PassageCardT
<div className="h-3 border-r border-neutral-500 my-0.5"></div> <div className="h-3 border-r border-neutral-500 my-0.5"></div>
{char.alliance_ticker && <span className="text-neutral-400">{char.alliance_ticker}</span>} {char.alliance_ticker && <span className="text-neutral-400">{char.alliance_ticker}</span>}
{!char.alliance_ticker && <span className="text-neutral-400">{char.corporation_ticker}</span>} {!char.alliance_ticker && <span className="text-neutral-400">{char.corporation_ticker}</span>}
<div className={clsx('flex gap-1 items-center h-full ml-[2px]')}>
<WdImgButton
width={16}
height={16}
tooltip={{ position: TooltipPosition.top, content: 'Open zkillboard' }}
source={ZKB_ICON}
onClick={handleOpenZKB}
/>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open Eve Who' }}
className={clsx('pi pi-user', '!text-[12px] relative top-[-1px]')}
onClick={handleOpenEveWho}
/>
</div>
</div> </div>
{/*ship name*/} {/*ship name*/}
@@ -12,9 +12,15 @@ export interface MapContextMenuProps {
onShowOnTheMap?: () => void; onShowOnTheMap?: () => void;
onShowMapSettings?: () => void; onShowMapSettings?: () => void;
onShowTrackingDialog?: () => void; onShowTrackingDialog?: () => void;
onShowWormholesReference?: () => void;
} }
export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings, onShowTrackingDialog }: MapContextMenuProps) => { export const MapContextMenu = ({
onShowOnTheMap,
onShowMapSettings,
onShowTrackingDialog,
onShowWormholesReference,
}: MapContextMenuProps) => {
const { const {
outCommand, outCommand,
storedSettings: { setInterfaceSettings }, storedSettings: { setInterfaceSettings },
@@ -52,6 +58,12 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings, onShowTracki
command: onShowOnTheMap, command: onShowOnTheMap,
visible: canTrackCharacters, visible: canTrackCharacters,
}, },
{
label: 'Wormholes Ref.',
icon: 'pi pi-bullseye',
command: onShowWormholesReference,
visible: canTrackCharacters,
},
{ separator: true, visible: true }, { separator: true, visible: true },
{ {
label: 'Settings', label: 'Settings',
@@ -38,9 +38,11 @@ export const OldSettingsDialog = () => {
localWidget: createSettings(widgetLocal, {}), localWidget: createSettings(widgetLocal, {}),
widgets: createSettings(widgetsOld, {}), widgets: createSettings(widgetsOld, {}),
routes: createSettings(widgetRoutes, {}), routes: createSettings(widgetRoutes, {}),
routesBy: createSettings(widgetRoutes, {}),
onTheMap: createSettings(onTheMapOld, {}), onTheMap: createSettings(onTheMapOld, {}),
signaturesWidget: createSettings(signatures, {}), signaturesWidget: createSettings(signatures, {}),
interface: createSettings(interfaceSettings, {}), interface: createSettings(interfaceSettings, {}),
map: createSettings(null, { viewport: { zoom: 1, x: 0, y: 0 } }),
}; };
if (asFile) { if (asFile) {
@@ -14,6 +14,7 @@ interface RightBarProps {
onShowOnTheMap?: () => void; onShowOnTheMap?: () => void;
onShowMapSettings?: () => void; onShowMapSettings?: () => void;
onShowTrackingDialog?: () => void; onShowTrackingDialog?: () => void;
onShowWormholesReference?: () => void;
additionalContent?: ReactNode; additionalContent?: ReactNode;
} }
@@ -21,6 +22,7 @@ export const RightBar = ({
onShowOnTheMap, onShowOnTheMap,
onShowMapSettings, onShowMapSettings,
onShowTrackingDialog, onShowTrackingDialog,
onShowWormholesReference,
additionalContent, additionalContent,
}: RightBarProps) => { }: RightBarProps) => {
const { const {
@@ -90,6 +92,16 @@ export const RightBar = ({
<i className="pi pi-hashtag"></i> <i className="pi pi-hashtag"></i>
</button> </button>
</WdTooltipWrapper> </WdTooltipWrapper>
<WdTooltipWrapper content="Wormholes Reference" 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={onShowWormholesReference}
>
<i className="pi pi-bullseye"></i>
</button>
</WdTooltipWrapper>
</div> </div>
</> </>
)} )}
@@ -13,6 +13,26 @@ export const renderK162Type = (option: K162Type) => {
return renderNoValue(); return renderNoValue();
} }
if (['c1_c2_c3', 'c4_c5'].includes(value)) {
const arr = whClassName.split('_');
return (
<div className="flex gap-1 items-center">
{arr.map(x => (
<WHClassView
key={x}
classNameWh="!text-[11px] !font-bold"
hideWhClassName
hideTooltip
whClassName={x}
noOffset
useShortTitle
/>
))}
</div>
);
}
return ( return (
<WHClassView <WHClassView
classNameWh="!text-[11px] !font-bold" classNameWh="!text-[11px] !font-bold"
@@ -1,8 +1,9 @@
import { createContext, useCallback, useContext, useRef, useState } from 'react'; import { createContext, useCallback, useContext, useRef, useState, useEffect } from 'react';
import { OutCommand, TrackingCharacter } from '@/hooks/Mapper/types'; import { Commands, OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { IncomingEvent, WithChildren } from '@/hooks/Mapper/types/common.ts'; import { IncomingEvent, WithChildren } from '@/hooks/Mapper/types/common.ts';
import { CommandInCharactersTrackingInfo } from '@/hooks/Mapper/types/commandsIn.ts'; import { CommandInCharactersTrackingInfo } from '@/hooks/Mapper/types/commandsIn.ts';
import { useMapEventListener } from '@/hooks/Mapper/events';
type DiffTrackingInfo = { characterId: string; tracked: boolean }; type DiffTrackingInfo = { characterId: string; tracked: boolean };
@@ -122,6 +123,14 @@ export const TrackingProvider = ({ children }: WithChildren) => {
[outCommand], [outCommand],
); );
// Listen for refresh_tracking_data event (triggered when ACL members change)
useMapEventListener(event => {
if (event.name === Commands.refreshTrackingData) {
loadTracking();
return true;
}
});
return ( return (
<TrackingContext.Provider <TrackingContext.Provider
value={{ value={{
@@ -0,0 +1,170 @@
import { useMemo, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WormholeDataRaw } from '@/hooks/Mapper/types';
import { RespawnTag, WHClassView } from '@/hooks/Mapper/components/ui-kit';
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
import clsx from 'clsx';
import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
import { InputIcon } from 'primereact/inputicon';
const renderSpawns = (w: WormholeDataRaw) => (
<div className="flex gap-1 flex-wrap">
{w.src.map(s => {
const group = s.split('-')[0];
const info = WORMHOLES_ADDITIONAL_INFO[group];
if (!info) {
return (
<span
key={s}
className="px-[4px] py-[1px] rounded bg-stone-800 text-stone-300 text-xs border border-stone-700"
>
{s}
</span>
);
}
const cls = WORMHOLE_CLASS_STYLES[String(info.wormholeClassID)] || '';
const label = `${info.shortName}`;
return (
<span
key={s}
className={clsx(cls, 'px-[4px] py-[1px] rounded text-xs border border-stone-700 bg-stone-900/40')}
>
{label}
</span>
);
})}
</div>
);
const renderName = (w: WormholeDataRaw) => (
<div className="flex items-center gap-2">
<WHClassView
whClassName={w.name}
noOffset
useShortTitle
classNameWh="overflow-hidden text-ellipsis whitespace-nowrap"
/>
</div>
);
const renderRespawn = (w: WormholeDataRaw) => (
<div className="flex gap-1 flex-wrap">
{w.respawn.map(r => (
<RespawnTag key={r} value={r} />
))}
</div>
);
export interface WormholeSignaturesDialogProps {
visible: boolean;
onHide: () => void;
}
export const WormholeSignaturesDialog = ({ visible, onHide }: WormholeSignaturesDialogProps) => {
const {
data: { wormholes },
} = useMapRootState();
const [filter, setFilter] = useState('');
const filtered = useMemo(() => {
const q = filter.trim().toLowerCase();
if (!q) return wormholes;
return wormholes.filter(w => {
const destInfo = WORMHOLES_ADDITIONAL_INFO[w.dest];
const spawnsLabels = w.src
.map(s => {
const group = s.split('-')[0];
const info = WORMHOLES_ADDITIONAL_INFO[group];
if (!info) return s;
return `${info.title} ${info.shortName}`.trim();
})
.join(' ');
return [
w.name,
destInfo?.title,
destInfo?.shortName,
spawnsLabels,
String(w.total_mass),
String(w.max_mass_per_jump),
w.lifetime,
w.respawn.join(','),
]
.filter(Boolean)
.join(' ')
.toLowerCase()
.includes(q);
});
}, [wormholes, filter]);
return (
<Dialog
header="Wormholes Reference"
visible={visible}
draggable={false}
resizable={false}
className="w-[950px] h-[600px]"
onHide={onHide}
contentClassName="!p-0 flex flex-col h-full"
>
<div className="p-3 flex items-center justify-between gap-2 border-b border-stone-800">
<div className="font-semibold text-sm text-stone-200">Reference list of all wormhole types</div>
<IconField iconPosition="right">
<InputIcon
className={clsx('pi pi-times', {
['cursor-pointer text-stone-400 hover:text-stone-200']: filter,
['text-stone-700 opacity-50 cursor-default']: !filter,
})}
onClick={() => filter && setFilter('')}
role="button"
aria-label="Clear search"
aria-disabled={!filter}
title={filter ? 'Clear' : 'Nothing to clear'}
/>
<InputText className="w-64" placeholder="Search" value={filter} onChange={e => setFilter(e.target.value)} />
</IconField>
</div>
<div className="flex-1 p-3 overflow-x-hidden">
<DataTable value={filtered} size="small" scrollable scrollHeight="flex" stripedRows>
<Column header="Type" body={renderName} className="w-[160px]" bodyClassName="whitespace-normal break-words" />
<Column header="Spawns In" body={renderSpawns} bodyClassName="whitespace-normal break-words text-[13px]" />
<Column
field="lifetime"
header="Lifetime"
className="w-[90px]"
bodyClassName="whitespace-normal break-words text-[13px]"
/>
<Column
header="Total Mass"
className="w-[120px]"
body={(w: WormholeDataRaw) => kgToTons(w.total_mass)}
bodyClassName="whitespace-normal break-words text-[13px]"
/>
<Column
header="Max/jump"
className="w-[120px]"
body={(w: WormholeDataRaw) => kgToTons(w.max_mass_per_jump)}
bodyClassName="whitespace-normal break-words text-[13px]"
/>
<Column
header="Respawn"
className="w-[150px]"
body={renderRespawn}
bodyClassName="whitespace-normal break-words text-[13px]"
/>
</DataTable>
</div>
</Dialog>
);
};
@@ -0,0 +1 @@
export * from './WormholeSignaturesDialog';
@@ -23,17 +23,17 @@ export const useCharacterActivityHandlers = () => {
/** /**
* Handle showing the character activity dialog * Handle showing the character activity dialog
*/ */
const handleShowActivity = useCallback(() => { const handleShowActivity = useCallback((days?: number | null) => {
// Update local state to show the dialog // Update local state to show the dialog
update(state => ({ update(state => ({
...state, ...state,
showCharacterActivity: true, showCharacterActivity: true,
})); }));
// Send the command to the server // Send the command to the server with optional days parameter
outCommand({ outCommand({
type: OutCommand.showActivity, type: OutCommand.showActivity,
data: {}, data: days !== undefined ? { days } : {},
}); });
}, [outCommand, update]); }, [outCommand, update]);
@@ -3,6 +3,7 @@ import {
WdEveEntityPortrait, WdEveEntityPortrait,
WdEveEntityPortraitSize, WdEveEntityPortraitSize,
WdEveEntityPortraitType, WdEveEntityPortraitType,
WdImgButton,
WdTooltipWrapper, WdTooltipWrapper,
} from '@/hooks/Mapper/components/ui-kit'; } from '@/hooks/Mapper/components/ui-kit';
import { SystemView } from '@/hooks/Mapper/components/ui-kit/SystemView'; import { SystemView } from '@/hooks/Mapper/components/ui-kit/SystemView';
@@ -14,6 +15,8 @@ import { Commands } from '@/hooks/Mapper/types/mapHandlers';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback } from 'react'; import { useCallback } from 'react';
import classes from './CharacterCard.module.scss'; import classes from './CharacterCard.module.scss';
import { ZKB_ICON } from '@/hooks/Mapper/icons';
import { charEveWhoLink, charZKBLink } from '@/hooks/Mapper/helpers/linkHelpers.ts';
export type CharacterCardProps = { export type CharacterCardProps = {
compact?: boolean; compact?: boolean;
@@ -66,6 +69,9 @@ export const CharacterCard = ({
const shipType = char.ship?.ship_type_info?.name; const shipType = char.ship?.ship_type_info?.name;
const locationShown = showSystem && char.location?.solar_system_id; const locationShown = showSystem && char.location?.solar_system_id;
const handleOpenZKB = useCallback(() => window.open(charZKBLink(char.eve_id), '_blank'), [char]);
const handleOpenEveWho = useCallback(() => window.open(charEveWhoLink(char.eve_id), '_blank'), [char]);
// INFO: Simple mode show only name and icon of ally/corp. By default it compact view // INFO: Simple mode show only name and icon of ally/corp. By default it compact view
if (simpleMode) { if (simpleMode) {
return ( return (
@@ -244,7 +250,24 @@ export const CharacterCard = ({
{char.name} {char.name}
</span> </span>
{showTicker && <span className="flex-shrink-0 text-gray-400 ml-1">[{tickerText}]</span>} {showTicker && <span className="flex-shrink-0 text-gray-400 ml-1">[{tickerText}]</span>}
<div className={clsx('flex gap-1 items-center h-full ml-[6px]')}>
<WdImgButton
width={16}
height={16}
tooltip={{ position: TooltipPosition.top, content: 'Open zkillboard' }}
source={ZKB_ICON}
onClick={handleOpenZKB}
className="min-w-[16px]"
/>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open Eve Who' }}
className={clsx('pi pi-user', '!text-[12px] relative top-[-1px]')}
onClick={handleOpenEveWho}
/>
</div>
</div> </div>
{locationShown ? ( {locationShown ? (
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap"> <div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
<SystemView <SystemView
@@ -1,8 +1,5 @@
.MarkdownCommentRoot { .MarkdownTextViewer {
border-left-width: 3px;
@apply text-[12px] leading-[1.2] text-stone-300 break-words; @apply text-[12px] leading-[1.2] text-stone-300 break-words;
@apply bg-gradient-to-r from-stone-600/40 via-stone-600/10 to-stone-600/0;
.h1 { .h1 {
@apply text-[12px] font-normal m-0 p-0 border-none break-words whitespace-normal; @apply text-[12px] font-normal m-0 p-0 border-none break-words whitespace-normal;
@@ -56,6 +53,10 @@
@apply font-bold text-green-400 break-words whitespace-normal; @apply font-bold text-green-400 break-words whitespace-normal;
} }
strong {
font-weight: bold;
}
i, em { i, em {
@apply italic text-pink-400 break-words whitespace-normal; @apply italic text-pink-400 break-words whitespace-normal;
} }
@@ -2,10 +2,16 @@ import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks'; import remarkBreaks from 'remark-breaks';
import classes from './MarkdownTextViewer.module.scss';
const REMARK_PLUGINS = [remarkGfm, remarkBreaks]; const REMARK_PLUGINS = [remarkGfm, remarkBreaks];
type MarkdownTextViewerProps = { children: string }; type MarkdownTextViewerProps = { children: string };
export const MarkdownTextViewer = ({ children }: MarkdownTextViewerProps) => { export const MarkdownTextViewer = ({ children }: MarkdownTextViewerProps) => {
return <Markdown remarkPlugins={REMARK_PLUGINS}>{children}</Markdown>; return (
<div className={classes.MarkdownTextViewer}>
<Markdown remarkPlugins={REMARK_PLUGINS}>{children}</Markdown>
</div>
);
}; };
@@ -0,0 +1,20 @@
import { Respawn } from '@/hooks/Mapper/types';
import clsx from 'clsx';
export const WORMHOLE_SPAWN_CLASSES_BG = {
[Respawn.static]: 'bg-lime-400/80 text-stone-950',
[Respawn.wandering]: 'bg-stone-800',
[Respawn.reverse]: 'bg-blue-400 text-stone-950',
};
type RespawnTagProps = { value: string };
export const RespawnTag = ({ value }: RespawnTagProps) => (
<span
className={clsx(
'px-[6px] py-[0px] rounded text-stone-300 text-[12px] font-[500] border border-stone-700',
WORMHOLE_SPAWN_CLASSES_BG[value as Respawn],
)}
>
{value}
</span>
);
@@ -13,7 +13,7 @@ export type SystemViewProps = {
export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomName, ...rest }: SystemViewProps) => { export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomName, ...rest }: SystemViewProps) => {
const memSystems = useMemo(() => [systemId], [systemId]); const memSystems = useMemo(() => [systemId], [systemId]);
const { systems, loading } = useLoadSystemStatic({ systems: memSystems }); const { systems, lastUpdateKey, loading } = useLoadSystemStatic({ systems: memSystems });
const { const {
data: { systems: mapSystems }, data: { systems: mapSystems },
@@ -23,9 +23,10 @@ export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomN
if (!systemId) { if (!systemId) {
return customSystemInfo; return customSystemInfo;
} }
return systems.get(parseInt(systemId)); return systems.get(parseInt(systemId));
// eslint-disable-next-line // eslint-disable-next-line
}, [customSystemInfo, systemId, systems, loading]); }, [customSystemInfo, systemId, systems, lastUpdateKey, loading]);
const mapSystemInfo = useMemo(() => { const mapSystemInfo = useMemo(() => {
if (!showCustomName) { if (!showCustomName) {
@@ -23,3 +23,4 @@ export * from './MenuItemWithInfo';
export * from './MarkdownTextViewer.tsx'; export * from './MarkdownTextViewer.tsx';
export * from './WdButton.tsx'; export * from './WdButton.tsx';
export * from './constants.ts'; export * from './constants.ts';
export * from './RespawnTag';
+10
View File
@@ -133,6 +133,16 @@ export const K162_TYPES: K162Type[] = [
value: 'pochven', value: 'pochven',
whClassName: 'F216', whClassName: 'F216',
}, },
{
label: 'C1/C2/C3',
value: 'c1_c2_c3',
whClassName: 'E004_D382_L477',
},
{
label: 'C4/C5',
value: 'c4_c5',
whClassName: 'M001_L614',
},
]; ];
export const K162_TYPES_MAP: { [key: string]: K162Type } = K162_TYPES.reduce( export const K162_TYPES_MAP: { [key: string]: K162Type } = K162_TYPES.reduce(
@@ -0,0 +1,2 @@
export const charZKBLink = (characterId: string) => `https://zkillboard.com/character/${characterId}/`;
export const charEveWhoLink = (characterId: string) => `https://evewho.com/character/${characterId}`;
@@ -6,7 +6,6 @@ import {
MapUnionTypes, MapUnionTypes,
OutCommandHandler, OutCommandHandler,
SolarSystemConnection, SolarSystemConnection,
StringBoolean,
TrackingCharacter, TrackingCharacter,
UseCharactersCacheData, UseCharactersCacheData,
UseCommentsData, UseCommentsData,
@@ -28,12 +27,14 @@ import {
MapSettings, MapSettings,
MapUserSettings, MapUserSettings,
OnTheMapSettingsType, OnTheMapSettingsType,
RoutesByType,
RoutesType, RoutesType,
} from '@/hooks/Mapper/mapRootProvider/types.ts'; } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { import {
DEFAULT_KILLS_WIDGET_SETTINGS, DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_MAP_SETTINGS, DEFAULT_MAP_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS, DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_BY_SETTINGS,
DEFAULT_ROUTES_SETTINGS, DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS, DEFAULT_WIDGET_LOCAL_SETTINGS,
STORED_INTERFACE_DEFAULT_VALUES, STORED_INTERFACE_DEFAULT_VALUES,
@@ -76,6 +77,8 @@ const INITIAL_DATA: MapRootData = {
userHubs: [], userHubs: [],
routes: undefined, routes: undefined,
userRoutes: undefined, userRoutes: undefined,
routesListBy: undefined,
availableRoutesBy: [],
kills: [], kills: [],
connections: [], connections: [],
detailedKills: {}, detailedKills: {},
@@ -132,6 +135,8 @@ export interface MapRootContextProps {
setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>; setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>;
settingsRoutes: RoutesType; settingsRoutes: RoutesType;
settingsRoutesUpdate: Dispatch<SetStateAction<RoutesType>>; settingsRoutesUpdate: Dispatch<SetStateAction<RoutesType>>;
settingsRoutesBy: RoutesByType;
settingsRoutesByUpdate: Dispatch<SetStateAction<RoutesByType>>;
settingsLocal: LocalWidgetSettings; settingsLocal: LocalWidgetSettings;
settingsLocalUpdate: Dispatch<SetStateAction<LocalWidgetSettings>>; settingsLocalUpdate: Dispatch<SetStateAction<LocalWidgetSettings>>;
settingsSignatures: SignatureSettingsType; settingsSignatures: SignatureSettingsType;
@@ -179,6 +184,8 @@ const MapRootContext = createContext<MapRootContextProps>({
setInterfaceSettings: () => null, setInterfaceSettings: () => null,
settingsRoutes: DEFAULT_ROUTES_SETTINGS, settingsRoutes: DEFAULT_ROUTES_SETTINGS,
settingsRoutesUpdate: () => null, settingsRoutesUpdate: () => null,
settingsRoutesBy: { ...DEFAULT_ROUTES_BY_SETTINGS, routes: { ...DEFAULT_ROUTES_BY_SETTINGS.routes } },
settingsRoutesByUpdate: () => null,
settingsLocal: DEFAULT_WIDGET_LOCAL_SETTINGS, settingsLocal: DEFAULT_WIDGET_LOCAL_SETTINGS,
settingsLocalUpdate: () => null, settingsLocalUpdate: () => null,
settingsSignatures: DEFAULT_SIGNATURE_SETTINGS, settingsSignatures: DEFAULT_SIGNATURE_SETTINGS,
@@ -7,6 +7,7 @@ import {
MiniMapPlacement, MiniMapPlacement,
OnTheMapSettingsType, OnTheMapSettingsType,
PingsPlacement, PingsPlacement,
RoutesByType,
RoutesType, RoutesType,
} from '@/hooks/Mapper/mapRootProvider/types.ts'; } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { DEFAULT_WIDGETS, STORED_VISIBLE_WIDGETS_DEFAULT } from '@/hooks/Mapper/components/mapInterface/constants.tsx'; import { DEFAULT_WIDGETS, STORED_VISIBLE_WIDGETS_DEFAULT } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
@@ -43,6 +44,12 @@ export const DEFAULT_WIDGET_LOCAL_SETTINGS: LocalWidgetSettings = {
showShipName: false, showShipName: false,
}; };
export const DEFAULT_ROUTES_BY_SETTINGS: RoutesByType = {
routes: DEFAULT_ROUTES_SETTINGS,
scope: 'ALL',
type: 'blueLoot',
};
export const DEFAULT_ON_THE_MAP_SETTINGS: OnTheMapSettingsType = { export const DEFAULT_ON_THE_MAP_SETTINGS: OnTheMapSettingsType = {
hideOffline: false, hideOffline: false,
}; };
@@ -3,6 +3,7 @@ import {
DEFAULT_KILLS_WIDGET_SETTINGS, DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_MAP_SETTINGS, DEFAULT_MAP_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS, DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_BY_SETTINGS,
DEFAULT_ROUTES_SETTINGS, DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS, DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps, getDefaultWidgetProps,
@@ -17,6 +18,11 @@ export const createWidgetSettings = <T>(settings: T) => {
}; };
export const createDefaultStoredSettings = (): MapUserSettings => { export const createDefaultStoredSettings = (): MapUserSettings => {
const defaultRoutesBy = {
...DEFAULT_ROUTES_BY_SETTINGS,
routes: { ...DEFAULT_ROUTES_BY_SETTINGS.routes },
};
return { return {
version: STORED_SETTINGS_VERSION, version: STORED_SETTINGS_VERSION,
migratedFromOld: false, migratedFromOld: false,
@@ -24,6 +30,7 @@ export const createDefaultStoredSettings = (): MapUserSettings => {
localWidget: createWidgetSettings(DEFAULT_WIDGET_LOCAL_SETTINGS), localWidget: createWidgetSettings(DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createWidgetSettings(getDefaultWidgetProps()), widgets: createWidgetSettings(getDefaultWidgetProps()),
routes: createWidgetSettings(DEFAULT_ROUTES_SETTINGS), routes: createWidgetSettings(DEFAULT_ROUTES_SETTINGS),
routesBy: createWidgetSettings(defaultRoutesBy),
onTheMap: createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS), onTheMap: createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createWidgetSettings(DEFAULT_SIGNATURE_SETTINGS), signaturesWidget: createWidgetSettings(DEFAULT_SIGNATURE_SETTINGS),
interface: createWidgetSettings(STORED_INTERFACE_DEFAULT_VALUES), interface: createWidgetSettings(STORED_INTERFACE_DEFAULT_VALUES),
@@ -43,6 +50,11 @@ export const getDefaultSettingsByType = (type: SettingsTypes): SettingsWrapper<a
return createWidgetSettings(getDefaultWidgetProps()); return createWidgetSettings(getDefaultWidgetProps());
case SettingsTypes.routes: case SettingsTypes.routes:
return createWidgetSettings(DEFAULT_ROUTES_SETTINGS); return createWidgetSettings(DEFAULT_ROUTES_SETTINGS);
case SettingsTypes.routesBy:
return createWidgetSettings({
...DEFAULT_ROUTES_BY_SETTINGS,
routes: { ...DEFAULT_ROUTES_BY_SETTINGS.routes },
});
case SettingsTypes.onTheMap: case SettingsTypes.onTheMap:
return createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS); return createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS);
case SettingsTypes.signaturesWidget: case SettingsTypes.signaturesWidget:
@@ -10,3 +10,4 @@ export * from './useCommandComments';
export * from './useGetCacheCharacter'; export * from './useGetCacheCharacter';
export * from './useCommandsActivity'; export * from './useCommandsActivity';
export * from './useCommandPings'; export * from './useCommandPings';
export * from './useCommandPingBlocked';
@@ -12,7 +12,7 @@ export const useCommandComments = () => {
}, []); }, []);
const removeComment = useCallback((data: CommandCommentRemoved) => { const removeComment = useCallback((data: CommandCommentRemoved) => {
ref.current.removeComment(data.solarSystemId.toString(), data.commentId); ref.current.removeComment(data.solarSystemId, data.commentId);
}, []); }, []);
return { addComment, removeComment }; return { addComment, removeComment };
@@ -0,0 +1,21 @@
import { useToast } from '@/hooks/Mapper/ToastProvider';
import { CommandPingBlocked } from '@/hooks/Mapper/types';
import { useCallback } from 'react';
export const useCommandPingBlocked = () => {
const { show } = useToast();
const pingBlocked = useCallback(
({ message }: CommandPingBlocked) => {
show({
severity: 'warn',
summary: 'Cannot create ping',
detail: message,
life: 5000,
});
},
[show],
);
return { pingBlocked };
};
@@ -14,8 +14,8 @@ export const useCommandPings = () => {
ref.current.update({ pings }); ref.current.update({ pings });
}, []); }, []);
const pingCancelled = useCallback(({ type, id }: CommandPingCancelled) => { const pingCancelled = useCallback(({ id }: CommandPingCancelled) => {
const newPings = ref.current.pings.filter(x => x.id !== id && x.type !== type); const newPings = ref.current.pings.filter(x => x.id !== id);
ref.current.update({ pings: newPings }); ref.current.update({ pings: newPings });
}, []); }, []);
@@ -24,6 +24,7 @@ export const useMapInit = () => {
user_permissions, user_permissions,
options, options,
is_subscription_active, is_subscription_active,
available_routes_by,
main_character_eve_id, main_character_eve_id,
following_character_eve_id, following_character_eve_id,
user_hubs, user_hubs,
@@ -85,6 +86,10 @@ export const useMapInit = () => {
updateData.isSubscriptionActive = is_subscription_active; updateData.isSubscriptionActive = is_subscription_active;
} }
if (available_routes_by) {
updateData.availableRoutesBy = available_routes_by;
}
if (system_static_infos) { if (system_static_infos) {
system_static_infos.forEach(static_info => { system_static_infos.forEach(static_info => {
addSystemStatic(static_info); addSystemStatic(static_info);
@@ -112,3 +112,23 @@ export const useUserRoutes = () => {
update({ userRoutes: value }); update({ userRoutes: value });
}, []); }, []);
}; };
export const useRoutesListBy = () => {
const {
update,
data: { routesListBy },
} = useMapRootState();
const ref = useRef({ update, routesListBy });
ref.current = { update, routesListBy };
return useCallback((value: CommandRoutes) => {
const { update, routesListBy } = ref.current;
if (areRoutesListsEqual(routesListBy, value)) {
return;
}
update({ routesListBy: value });
}, []);
};
@@ -1,5 +1,5 @@
import { useCallback, useRef, useState } from 'react';
import { CommentSystem, CommentType, OutCommand, OutCommandHandler, UseCommentsData } from '@/hooks/Mapper/types'; import { CommentSystem, CommentType, OutCommand, OutCommandHandler, UseCommentsData } from '@/hooks/Mapper/types';
import { useCallback, useRef, useState } from 'react';
interface UseCommentsProps { interface UseCommentsProps {
outCommand: OutCommandHandler; outCommand: OutCommandHandler;
@@ -8,12 +8,12 @@ interface UseCommentsProps {
export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData => { export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData => {
const [lastUpdateKey, setLastUpdateKey] = useState(0); const [lastUpdateKey, setLastUpdateKey] = useState(0);
const commentBySystemsRef = useRef<Map<string, CommentSystem>>(new Map()); const commentBySystemsRef = useRef<Map<number, CommentSystem>>(new Map());
const ref = useRef({ outCommand }); const ref = useRef({ outCommand });
ref.current = { outCommand }; ref.current = { outCommand };
const loadComments = useCallback(async (systemId: string) => { const loadComments = useCallback(async (systemId: number) => {
let cSystem = commentBySystemsRef.current.get(systemId); let cSystem = commentBySystemsRef.current.get(systemId);
if (cSystem?.loading || cSystem?.loaded) { if (cSystem?.loading || cSystem?.loaded) {
return; return;
@@ -45,7 +45,7 @@ export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData =
setLastUpdateKey(x => x + 1); setLastUpdateKey(x => x + 1);
}, []); }, []);
const addComment = useCallback((systemId: string, comment: CommentType) => { const addComment = useCallback((systemId: number, comment: CommentType) => {
const cSystem = commentBySystemsRef.current.get(systemId); const cSystem = commentBySystemsRef.current.get(systemId);
if (cSystem) { if (cSystem) {
cSystem.comments.push(comment); cSystem.comments.push(comment);
@@ -61,7 +61,7 @@ export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData =
setLastUpdateKey(x => x + 1); setLastUpdateKey(x => x + 1);
}, []); }, []);
const removeComment = useCallback((systemId: string, commentId: string) => { const removeComment = useCallback((systemId: number, commentId: string) => {
const cSystem = commentBySystemsRef.current.get(systemId); const cSystem = commentBySystemsRef.current.get(systemId);
if (!cSystem) { if (!cSystem) {
return; return;
@@ -12,6 +12,7 @@ import {
CommandLinkSignatureToSystem, CommandLinkSignatureToSystem,
CommandMapUpdated, CommandMapUpdated,
CommandPingAdded, CommandPingAdded,
CommandPingBlocked,
CommandPingCancelled, CommandPingCancelled,
CommandPresentCharacters, CommandPresentCharacters,
CommandRemoveConnections, CommandRemoveConnections,
@@ -29,6 +30,7 @@ import { ForwardedRef, useImperativeHandle } from 'react';
import { import {
useCommandComments, useCommandComments,
useCommandPingBlocked,
useCommandPings, useCommandPings,
useCommandsCharacters, useCommandsCharacters,
useCommandsConnections, useCommandsConnections,
@@ -36,6 +38,7 @@ import {
useMapInit, useMapInit,
useMapUpdated, useMapUpdated,
useRoutes, useRoutes,
useRoutesListBy,
useUserRoutes, useUserRoutes,
} from './api'; } from './api';
@@ -59,131 +62,134 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
const mapUpdated = useMapUpdated(); const mapUpdated = useMapUpdated();
const mapRoutes = useRoutes(); const mapRoutes = useRoutes();
const mapUserRoutes = useUserRoutes(); const mapUserRoutes = useUserRoutes();
const mapRoutesListBy = useRoutesListBy();
const { addComment, removeComment } = useCommandComments(); const { addComment, removeComment } = useCommandComments();
const { pingAdded, pingCancelled } = useCommandPings(); const { pingAdded, pingCancelled } = useCommandPings();
const { pingBlocked } = useCommandPingBlocked();
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity(); const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
useImperativeHandle( useImperativeHandle(ref, () => {
ref, return {
() => { command(type, data) {
return { switch (type) {
command(type, data) { case Commands.init: // USED
switch (type) { mapInit(data as CommandInit);
case Commands.init: // USED break;
mapInit(data as CommandInit); case Commands.addSystems: // USED
break; addSystems(data as CommandAddSystems);
case Commands.addSystems: // USED break;
addSystems(data as CommandAddSystems); case Commands.updateSystems: // USED
break; updateSystems(data as CommandUpdateSystems);
case Commands.updateSystems: // USED break;
updateSystems(data as CommandUpdateSystems); case Commands.removeSystems: // USED
break; removeSystems(data as CommandRemoveSystems);
case Commands.removeSystems: // USED break;
removeSystems(data as CommandRemoveSystems); case Commands.addConnections: // USED
break; addConnections(data as CommandAddConnections);
case Commands.addConnections: // USED break;
addConnections(data as CommandAddConnections); case Commands.removeConnections: // USED
break; removeConnections(data as CommandRemoveConnections);
case Commands.removeConnections: // USED break;
removeConnections(data as CommandRemoveConnections); case Commands.updateConnection: // USED
break; updateConnection(data as CommandUpdateConnection);
case Commands.updateConnection: // USED break;
updateConnection(data as CommandUpdateConnection); case Commands.charactersUpdated: // USED
break; charactersUpdated(data as CommandCharactersUpdated);
case Commands.charactersUpdated: // USED break;
charactersUpdated(data as CommandCharactersUpdated); case Commands.characterAdded: // USED
break; characterAdded(data as CommandCharacterAdded);
case Commands.characterAdded: // USED break;
characterAdded(data as CommandCharacterAdded); case Commands.characterRemoved: // USED
break; characterRemoved(data as CommandCharacterRemoved);
case Commands.characterRemoved: // USED break;
characterRemoved(data as CommandCharacterRemoved); case Commands.characterUpdated: // USED
break; characterUpdated(data as CommandCharacterUpdated);
case Commands.characterUpdated: // USED break;
characterUpdated(data as CommandCharacterUpdated); case Commands.presentCharacters: // USED
break; presentCharacters(data as CommandPresentCharacters);
case Commands.presentCharacters: // USED break;
presentCharacters(data as CommandPresentCharacters); case Commands.mapUpdated: // USED
break; mapUpdated(data as CommandMapUpdated);
case Commands.mapUpdated: // USED break;
mapUpdated(data as CommandMapUpdated); case Commands.routes:
break; mapRoutes(data as CommandRoutes);
case Commands.routes: break;
mapRoutes(data as CommandRoutes); case Commands.userRoutes:
break; mapUserRoutes(data as CommandRoutes);
case Commands.userRoutes: break;
mapUserRoutes(data as CommandRoutes); case Commands.routesListBy:
break; mapRoutesListBy(data as CommandRoutes);
break;
case Commands.signaturesUpdated: // USED case Commands.signaturesUpdated: // USED
updateSystemSignatures(data as CommandSignaturesUpdated); updateSystemSignatures(data as CommandSignaturesUpdated);
break; break;
case Commands.linkSignatureToSystem: // USED case Commands.linkSignatureToSystem: // USED
setTimeout(() => { setTimeout(() => {
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem); updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
}, 200); }, 200);
break; break;
case Commands.centerSystem: // USED case Commands.centerSystem: // USED
// do nothing here // do nothing here
break; break;
case Commands.selectSystem: // USED case Commands.selectSystem: // USED
// do nothing here // do nothing here
break; break;
case Commands.killsUpdated: case Commands.killsUpdated:
// do nothing here // do nothing here
break; break;
case Commands.detailedKillsUpdated: case Commands.detailedKillsUpdated:
updateDetailedKills(data as Record<string, DetailedKill[]>); updateDetailedKills(data as Record<string, DetailedKill[]>);
break; break;
case Commands.characterActivityData: case Commands.characterActivityData:
characterActivityData(data as CommandCharacterActivityData); characterActivityData(data as CommandCharacterActivityData);
break; break;
case Commands.trackingCharactersData: case Commands.trackingCharactersData:
trackingCharactersData(data as CommandTrackingCharactersData); trackingCharactersData(data as CommandTrackingCharactersData);
break; break;
case Commands.updateActivity: case Commands.updateActivity:
break; break;
case Commands.updateTracking: case Commands.updateTracking:
break; break;
case Commands.userSettingsUpdated: case Commands.userSettingsUpdated:
userSettingsUpdated(data as CommandUserSettingsUpdated); userSettingsUpdated(data as CommandUserSettingsUpdated);
break; break;
case Commands.systemCommentAdded: case Commands.systemCommentAdded:
addComment(data as CommandCommentAdd); addComment(data as CommandCommentAdd);
break; break;
case Commands.systemCommentRemoved: case Commands.systemCommentRemoved:
removeComment(data as CommandCommentRemoved); removeComment(data as CommandCommentRemoved);
break; break;
case Commands.pingAdded: case Commands.pingAdded:
pingAdded(data as CommandPingAdded); pingAdded(data as CommandPingAdded);
break; break;
case Commands.pingCancelled: case Commands.pingCancelled:
pingCancelled(data as CommandPingCancelled); pingCancelled(data as CommandPingCancelled);
break; break;
case Commands.pingBlocked:
pingBlocked(data as CommandPingBlocked);
break;
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;
}
default: emitMapEvent({ name: type, data });
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data); },
break; };
} }, []);
emitMapEvent({ name: type, data });
},
};
},
[],
);
}; };
@@ -56,6 +56,12 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
map_slug, map_slug,
'routes', 'routes',
); );
const [settingsRoutesBy, settingsRoutesByUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'routesBy',
);
const [settingsLocal, settingsLocalUpdate] = useSettingsValueAndSetter( const [settingsLocal, settingsLocalUpdate] = useSettingsValueAndSetter(
mapUserSettings, mapUserSettings,
@@ -188,6 +194,8 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
setInterfaceSettings, setInterfaceSettings,
settingsRoutes, settingsRoutes,
settingsRoutesUpdate, settingsRoutesUpdate,
settingsRoutesBy,
settingsRoutesByUpdate,
settingsLocal, settingsLocal,
settingsLocalUpdate, settingsLocalUpdate,
settingsSignatures, settingsSignatures,
@@ -1,5 +1,6 @@
import { to_1 } from './to_1.ts'; import { to_1 } from './to_1.ts';
import { to_2 } from './to_2.ts'; import { to_2 } from './to_2.ts';
import { to_3 } from './to_3.ts';
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts'; import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export default [to_1, to_2] as MigrationStructure[]; export default [to_1, to_2, to_3] as MigrationStructure[];
@@ -0,0 +1,31 @@
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { DEFAULT_ROUTES_BY_SETTINGS, DEFAULT_ROUTES_SETTINGS } from '@/hooks/Mapper/mapRootProvider/constants.ts';
export const to_3: MigrationStructure = {
to: 3,
up: (prev: any) => {
const rawRoutesBy = prev?.routesBy;
const hasStructuredRoutesBy =
rawRoutesBy && typeof rawRoutesBy === 'object' && 'routes' in rawRoutesBy;
const routes = hasStructuredRoutesBy
? { ...DEFAULT_ROUTES_SETTINGS, ...rawRoutesBy.routes }
: { ...DEFAULT_ROUTES_SETTINGS, ...(rawRoutesBy ?? prev?.routes ?? {}) };
const scopeRaw = hasStructuredRoutesBy ? rawRoutesBy?.scope : undefined;
const scope = scopeRaw === 'HIGH' ? 'HIGH' : 'ALL';
const type = hasStructuredRoutesBy && rawRoutesBy?.type ? rawRoutesBy.type : DEFAULT_ROUTES_BY_SETTINGS.type;
return {
...prev,
routesBy: {
...DEFAULT_ROUTES_BY_SETTINGS,
...(hasStructuredRoutesBy ? rawRoutesBy : {}),
scope,
type,
routes,
},
};
},
};
@@ -47,6 +47,22 @@ export type RoutesType = {
avoid: number[]; avoid: number[];
}; };
export type RoutesByCategoryType =
| 'blueLoot'
| 'redLoot'
| 'thera'
| 'turnur'
| 'so_cleaning'
| 'trade_hubs';
export type RoutesByScopeType = 'ALL' | 'HIGH';
export type RoutesByType = {
routes: RoutesType;
scope: RoutesByScopeType;
type: RoutesByCategoryType;
};
export type LocalWidgetSettings = { export type LocalWidgetSettings = {
compact: boolean; compact: boolean;
showOffline: boolean; showOffline: boolean;
@@ -79,6 +95,7 @@ export type MapUserSettings = {
interface: SettingsWrapper<InterfaceStoredSettings>; interface: SettingsWrapper<InterfaceStoredSettings>;
onTheMap: SettingsWrapper<OnTheMapSettingsType>; onTheMap: SettingsWrapper<OnTheMapSettingsType>;
routes: SettingsWrapper<RoutesType>; routes: SettingsWrapper<RoutesType>;
routesBy: SettingsWrapper<RoutesByType>;
localWidget: SettingsWrapper<LocalWidgetSettings>; localWidget: SettingsWrapper<LocalWidgetSettings>;
signaturesWidget: SettingsWrapper<SignatureSettingsType>; signaturesWidget: SettingsWrapper<SignatureSettingsType>;
killsWidget: SettingsWrapper<KillsWidgetSettings>; killsWidget: SettingsWrapper<KillsWidgetSettings>;
@@ -98,6 +115,7 @@ export enum SettingsTypes {
localWidget = 'localWidget', localWidget = 'localWidget',
widgets = 'widgets', widgets = 'widgets',
routes = 'routes', routes = 'routes',
routesBy = 'routesBy',
onTheMap = 'onTheMap', onTheMap = 'onTheMap',
signaturesWidget = 'signaturesWidget', signaturesWidget = 'signaturesWidget',
interface = 'interface', interface = 'interface',
@@ -1,4 +1,4 @@
export const STORED_SETTINGS_VERSION = 2; export const STORED_SETTINGS_VERSION = 3;
export const LS_KEY_LEGASY = 'map-user-settings'; export const LS_KEY_LEGASY = 'map-user-settings';
export const LS_KEY = 'map-user-settings-v3'; export const LS_KEY = 'map-user-settings-v3';
@@ -68,4 +68,5 @@ export interface ActivitySummary {
passages: number; passages: number;
connections: number; connections: number;
signatures: number; signatures: number;
timestamp?: string;
} }
+4 -4
View File
@@ -13,9 +13,9 @@ export type CommentSystem = {
}; };
export interface UseCommentsData { export interface UseCommentsData {
loadComments: (systemId: string) => Promise<void>; loadComments: (systemId: number) => Promise<void>;
addComment: (systemId: string, comment: CommentType) => void; addComment: (systemId: number, comment: CommentType) => void;
removeComment: (systemId: string, commentId: string) => void; removeComment: (systemId: number, commentId: string) => void;
comments: Map<string, CommentSystem>; comments: Map<number, CommentSystem>;
lastUpdateKey: number; lastUpdateKey: number;
} }
@@ -6,11 +6,17 @@ export type PassageLimitedCharacterType = Pick<
>; >;
export type Passage = { export type Passage = {
from: boolean;
inserted_at: string; // Date inserted_at: string; // Date
ship: ShipTypeRaw; ship: ShipTypeRaw;
character: PassageLimitedCharacterType; character: PassageLimitedCharacterType;
}; };
export type PassageWithSourceTarget = {
source: string;
target: string;
} & Passage;
export type ConnectionInfoOutput = { export type ConnectionInfoOutput = {
marl_eol_time: string; marl_eol_time: string;
}; };
+20 -2
View File
@@ -3,6 +3,7 @@ import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Ma
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts'; import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts'; import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts'; import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { RoutesByCategoryType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types/system.ts'; import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types/system.ts';
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts'; import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
@@ -25,6 +26,7 @@ export enum Commands {
detailedKillsUpdated = 'detailed_kills_updated', detailedKillsUpdated = 'detailed_kills_updated',
routes = 'routes', routes = 'routes',
userRoutes = 'user_routes', userRoutes = 'user_routes',
routesListBy = 'routes_list_by',
centerSystem = 'center_system', centerSystem = 'center_system',
selectSystem = 'select_system', selectSystem = 'select_system',
selectSystems = 'select_systems', selectSystems = 'select_systems',
@@ -38,8 +40,10 @@ export enum Commands {
updateTracking = 'update_tracking', updateTracking = 'update_tracking',
userSettingsUpdated = 'user_settings_updated', userSettingsUpdated = 'user_settings_updated',
showTracking = 'show_tracking', showTracking = 'show_tracking',
refreshTrackingData = 'refresh_tracking_data',
pingAdded = 'ping_added', pingAdded = 'ping_added',
pingCancelled = 'ping_cancelled', pingCancelled = 'ping_cancelled',
pingBlocked = 'ping_blocked',
} }
export type Command = export type Command =
@@ -60,6 +64,7 @@ export type Command =
| Commands.detailedKillsUpdated | Commands.detailedKillsUpdated
| Commands.routes | Commands.routes
| Commands.userRoutes | Commands.userRoutes
| Commands.routesListBy
| Commands.selectSystem | Commands.selectSystem
| Commands.selectSystems | Commands.selectSystems
| Commands.centerSystem | Commands.centerSystem
@@ -74,8 +79,10 @@ export type Command =
| Commands.updateActivity | Commands.updateActivity
| Commands.updateTracking | Commands.updateTracking
| Commands.showTracking | Commands.showTracking
| Commands.refreshTrackingData
| Commands.pingAdded | Commands.pingAdded
| Commands.pingCancelled; | Commands.pingCancelled
| Commands.pingBlocked;
export type CommandInit = { export type CommandInit = {
systems: SolarSystemRawType[]; systems: SolarSystemRawType[];
@@ -97,6 +104,7 @@ export type CommandInit = {
options: MapOptions; options: MapOptions;
reset?: boolean; reset?: boolean;
is_subscription_active?: boolean; is_subscription_active?: boolean;
available_routes_by?: RoutesByCategoryType[];
main_character_eve_id?: string | null; main_character_eve_id?: string | null;
following_character_eve_id?: string | null; following_character_eve_id?: string | null;
map_slug?: string; map_slug?: string;
@@ -117,6 +125,7 @@ export type CommandSignaturesUpdated = string;
export type CommandMapUpdated = Partial<CommandInit>; export type CommandMapUpdated = Partial<CommandInit>;
export type CommandRoutes = RoutesList; export type CommandRoutes = RoutesList;
export type CommandUserRoutes = RoutesList; export type CommandUserRoutes = RoutesList;
export type CommandRoutesListBy = RoutesList;
export type CommandKillsUpdated = Kill[]; export type CommandKillsUpdated = Kill[];
export type CommandDetailedKillsUpdated = Record<string, DetailedKill[]>; export type CommandDetailedKillsUpdated = Record<string, DetailedKill[]>;
export type CommandSelectSystem = string | undefined; export type CommandSelectSystem = string | undefined;
@@ -131,7 +140,7 @@ export type CommandLinkSignatureToSystem = {
}; };
export type CommandLinkSignaturesUpdated = number; export type CommandLinkSignaturesUpdated = number;
export type CommandCommentAdd = { export type CommandCommentAdd = {
solarSystemId: string; solarSystemId: number;
comment: CommentType; comment: CommentType;
}; };
export type CommandCommentRemoved = { export type CommandCommentRemoved = {
@@ -145,6 +154,7 @@ export type CommandUserSettingsUpdated = {
}; };
export type CommandShowTracking = null; export type CommandShowTracking = null;
export type CommandRefreshTrackingData = Record<string, never>;
export type CommandUpdateActivity = { export type CommandUpdateActivity = {
characterId: number; characterId: number;
systemId: number; systemId: number;
@@ -158,6 +168,10 @@ export type CommandUpdateTracking = {
}; };
export type CommandPingAdded = PingData[]; export type CommandPingAdded = PingData[];
export type CommandPingCancelled = Pick<PingData, 'type' | 'id'>; export type CommandPingCancelled = Pick<PingData, 'type' | 'id'>;
export type CommandPingBlocked = {
reason: string;
message: string;
};
export interface UserSettings { export interface UserSettings {
primaryCharacterId?: string; primaryCharacterId?: string;
@@ -190,6 +204,7 @@ export interface CommandData {
[Commands.mapUpdated]: CommandMapUpdated; [Commands.mapUpdated]: CommandMapUpdated;
[Commands.routes]: CommandRoutes; [Commands.routes]: CommandRoutes;
[Commands.userRoutes]: CommandUserRoutes; [Commands.userRoutes]: CommandUserRoutes;
[Commands.routesListBy]: CommandRoutesListBy;
[Commands.killsUpdated]: CommandKillsUpdated; [Commands.killsUpdated]: CommandKillsUpdated;
[Commands.detailedKillsUpdated]: CommandDetailedKillsUpdated; [Commands.detailedKillsUpdated]: CommandDetailedKillsUpdated;
[Commands.selectSystem]: CommandSelectSystem; [Commands.selectSystem]: CommandSelectSystem;
@@ -206,8 +221,10 @@ export interface CommandData {
[Commands.systemCommentRemoved]: CommandCommentRemoved; [Commands.systemCommentRemoved]: CommandCommentRemoved;
[Commands.systemCommentsUpdated]: unknown; [Commands.systemCommentsUpdated]: unknown;
[Commands.showTracking]: CommandShowTracking; [Commands.showTracking]: CommandShowTracking;
[Commands.refreshTrackingData]: CommandRefreshTrackingData;
[Commands.pingAdded]: CommandPingAdded; [Commands.pingAdded]: CommandPingAdded;
[Commands.pingCancelled]: CommandPingCancelled; [Commands.pingCancelled]: CommandPingCancelled;
[Commands.pingBlocked]: CommandPingBlocked;
} }
export interface MapHandlers { export interface MapHandlers {
@@ -221,6 +238,7 @@ export enum OutCommand {
deleteUserHub = 'delete_user_hub', deleteUserHub = 'delete_user_hub',
getRoutes = 'get_routes', getRoutes = 'get_routes',
getUserRoutes = 'get_user_routes', getUserRoutes = 'get_user_routes',
getRoutesBy = 'get_routes_by',
getCharacterJumps = 'get_character_jumps', getCharacterJumps = 'get_character_jumps',
getStructures = 'get_structures', getStructures = 'get_structures',
getSignatures = 'get_signatures', getSignatures = 'get_signatures',
@@ -6,6 +6,7 @@ import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts'; import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { MapOptions, PingData, UserPermissions } from '@/hooks/Mapper/types'; import { MapOptions, PingData, UserPermissions } from '@/hooks/Mapper/types';
import { SystemSignature } from '@/hooks/Mapper/types/signatures'; import { SystemSignature } from '@/hooks/Mapper/types/signatures';
import { RoutesByCategoryType } from '@/hooks/Mapper/mapRootProvider/types.ts';
export type MapUnionTypes = { export type MapUnionTypes = {
wormholesData: Record<string, WormholeDataRaw>; wormholesData: Record<string, WormholeDataRaw>;
@@ -20,6 +21,8 @@ export type MapUnionTypes = {
systemSignatures: Record<string, SystemSignature[]>; systemSignatures: Record<string, SystemSignature[]>;
routes?: RoutesList; routes?: RoutesList;
userRoutes?: RoutesList; userRoutes?: RoutesList;
routesListBy?: RoutesList;
availableRoutesBy?: RoutesByCategoryType[];
kills: Record<number, number>; kills: Record<number, number>;
connections: SolarSystemConnection[]; connections: SolarSystemConnection[];
userPermissions: Partial<UserPermissions>; userPermissions: Partial<UserPermissions>;
+7
View File
@@ -13,12 +13,19 @@ export type SystemStaticInfoShort = Pick<
type MappedSystem = SolarSystemStaticInfoRaw | undefined; type MappedSystem = SolarSystemStaticInfoRaw | undefined;
export type RouteStationSummary = {
station_id: number;
station_name: string;
special?: boolean;
};
export type Route = { export type Route = {
destination: number; destination: number;
has_connection: boolean; has_connection: boolean;
origin: number; origin: number;
systems?: number[]; systems?: number[];
mapped_systems?: MappedSystem[]; mapped_systems?: MappedSystem[];
stations?: RouteStationSummary[];
success?: boolean; success?: boolean;
}; };
+1 -1
View File
@@ -57,7 +57,7 @@ export default {
}; };
refreshZone.addEventListener('click', handleUpdate); refreshZone.addEventListener('click', handleUpdate);
refreshZone.addEventListener('mouseover', handleUpdate); // refreshZone.addEventListener('mouseover', handleUpdate);
this.updated(); this.updated();
}, },
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+1
View File
@@ -63,6 +63,7 @@ config :wanderer_app, WandererAppWeb.Endpoint,
] ]
config :wanderer_app, config :wanderer_app,
environment: :dev,
dev_routes: true dev_routes: true
# Do not include metadata nor timestamps in development logs # Do not include metadata nor timestamps in development logs
+3
View File
@@ -1,5 +1,8 @@
import Config import Config
# Set environment at compile time for modules using Application.compile_env
config :wanderer_app, environment: :prod
# Note we also include the path to a cache manifest # Note we also include the path to a cache manifest
# containing the digested version of static files. This # containing the digested version of static files. This
# manifest is generated by the `mix assets.deploy` task, # manifest is generated by the `mix assets.deploy` task,
+58 -5
View File
@@ -92,6 +92,31 @@ map_subscription_extra_hubs_10_price =
config_dir config_dir
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_HUBS_10_PRICE", 10_000_000) |> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_HUBS_10_PRICE", 10_000_000)
# Parse promo codes from environment variable
# Format: "CODE1:10,CODE2:20" where numbers are discount percentages
promo_codes =
config_dir
|> get_var_from_path_or_env("WANDERER_PROMO_CODES", "")
|> case do
"" ->
%{}
codes_string ->
codes_string
|> String.split(",")
|> Enum.map(fn entry ->
case String.split(String.trim(entry), ":") do
[code, discount] ->
{String.upcase(String.trim(code)), String.to_integer(String.trim(discount))}
_ ->
nil
end
end)
|> Enum.reject(&is_nil/1)
|> Map.new()
end
map_connection_auto_expire_hours = map_connection_auto_expire_hours =
config_dir config_dir
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_AUTO_EXPIRE_HOURS", 24) |> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_AUTO_EXPIRE_HOURS", 24)
@@ -176,8 +201,36 @@ config :wanderer_app,
} }
], ],
extra_characters_50: map_subscription_extra_characters_50_price, extra_characters_50: map_subscription_extra_characters_50_price,
extra_hubs_10: map_subscription_extra_hubs_10_price extra_hubs_10: map_subscription_extra_hubs_10_price,
} promo_codes: promo_codes
},
# Finch pool configuration - separate pools for different services
# ESI Character Tracking pool - high capacity for bulk character operations
# With 30+ TrackerPools × ~100 concurrent tasks, need large pool
finch_esi_character_pool_size:
System.get_env("WANDERER_FINCH_ESI_CHARACTER_POOL_SIZE", "200") |> String.to_integer(),
finch_esi_character_pool_count:
System.get_env("WANDERER_FINCH_ESI_CHARACTER_POOL_COUNT", "4") |> String.to_integer(),
# ESI General pool - standard capacity for general ESI operations
finch_esi_general_pool_size:
System.get_env("WANDERER_FINCH_ESI_GENERAL_POOL_SIZE", "50") |> String.to_integer(),
finch_esi_general_pool_count:
System.get_env("WANDERER_FINCH_ESI_GENERAL_POOL_COUNT", "4") |> String.to_integer(),
# Webhooks pool - isolated from ESI rate limits
finch_webhooks_pool_size:
System.get_env("WANDERER_FINCH_WEBHOOKS_POOL_SIZE", "25") |> String.to_integer(),
finch_webhooks_pool_count:
System.get_env("WANDERER_FINCH_WEBHOOKS_POOL_COUNT", "2") |> String.to_integer(),
# Default pool - everything else (email, license manager, etc.)
finch_default_pool_size:
System.get_env("WANDERER_FINCH_DEFAULT_POOL_SIZE", "25") |> String.to_integer(),
finch_default_pool_count:
System.get_env("WANDERER_FINCH_DEFAULT_POOL_COUNT", "2") |> String.to_integer(),
# Character tracker concurrency settings
# Location updates need high concurrency for <2s response with 3000+ characters
location_concurrency:
System.get_env("WANDERER_LOCATION_CONCURRENCY", "#{System.schedulers_online() * 12}")
|> String.to_integer()
config :ueberauth, Ueberauth, config :ueberauth, Ueberauth,
providers: [ providers: [
@@ -237,7 +290,7 @@ config :logger,
case config_env() do case config_env() do
:prod -> "info" :prod -> "info"
:dev -> "info" :dev -> "info"
:test -> "debug" :test -> "warning"
end end
) )
) )
@@ -405,7 +458,7 @@ config :wanderer_app, :license_manager,
config :wanderer_app, :sse, config :wanderer_app, :sse,
enabled: enabled:
config_dir config_dir
|> get_var_from_path_or_env("WANDERER_SSE_ENABLED", "true") |> get_var_from_path_or_env("WANDERER_SSE_ENABLED", "false")
|> String.to_existing_atom(), |> String.to_existing_atom(),
max_connections_total: max_connections_total:
config_dir |> get_int_from_path_or_env("WANDERER_SSE_MAX_CONNECTIONS", 1000), config_dir |> get_int_from_path_or_env("WANDERER_SSE_MAX_CONNECTIONS", 1000),
@@ -420,6 +473,6 @@ config :wanderer_app, :sse,
config :wanderer_app, :external_events, config :wanderer_app, :external_events,
webhooks_enabled: webhooks_enabled:
config_dir config_dir
|> get_var_from_path_or_env("WANDERER_WEBHOOKS_ENABLED", "true") |> get_var_from_path_or_env("WANDERER_WEBHOOKS_ENABLED", "false")
|> String.to_existing_atom(), |> String.to_existing_atom(),
webhook_timeout_ms: config_dir |> get_int_from_path_or_env("WANDERER_WEBHOOK_TIMEOUT_MS", 15000) webhook_timeout_ms: config_dir |> get_int_from_path_or_env("WANDERER_WEBHOOK_TIMEOUT_MS", 15000)
+9 -1
View File
@@ -1,5 +1,9 @@
import Config import Config
# Disable Ash async operations in tests to ensure transactional safety
# This prevents Ash from spawning tasks that could bypass the Ecto sandbox
config :ash, :disable_async?, true
# Configure your database # Configure your database
# #
# The MIX_TEST_PARTITION environment variable can be used # The MIX_TEST_PARTITION environment variable can be used
@@ -24,7 +28,11 @@ config :wanderer_app,
pubsub_client: Test.PubSubMock, pubsub_client: Test.PubSubMock,
cached_info: WandererApp.CachedInfo.Mock, cached_info: WandererApp.CachedInfo.Mock,
character_api_disabled: false, character_api_disabled: false,
environment: :test environment: :test,
map_subscriptions_enabled: false,
wanderer_kills_service_enabled: false,
sse: [enabled: false],
external_events: [webhooks_enabled: false]
# We don't run a server during test. If one is required, # We don't run a server during test. If one is required,
# you can enable the server option below. # you can enable the server option below.
+10 -4
View File
@@ -16,6 +16,11 @@ defmodule WandererApp.Api.AccessList do
includes([:owner, :members]) includes([:owner, :members])
default_fields([
:name,
:description
])
derive_filter?(true) derive_filter?(true)
derive_sort?(true) derive_sort?(true)
@@ -60,19 +65,17 @@ defmodule WandererApp.Api.AccessList do
# Added :api_key to the accepted attributes # Added :api_key to the accepted attributes
accept [:name, :description, :owner_id, :api_key] accept [:name, :description, :owner_id, :api_key]
primary?(true) primary?(true)
argument :owner_id, :uuid, allow_nil?: false
change manage_relationship(:owner_id, :owner, on_lookup: :relate, on_no_match: nil)
end end
update :update do update :update do
accept [:name, :description, :owner_id, :api_key] accept [:name, :description, :owner_id, :api_key]
primary?(true) primary?(true)
require_atomic? false
end end
update :assign_owner do update :assign_owner do
accept [:owner_id] accept [:owner_id]
require_atomic? false
end end
end end
@@ -81,12 +84,15 @@ defmodule WandererApp.Api.AccessList do
attribute :name, :string do attribute :name, :string do
allow_nil? false allow_nil? false
public? true
end end
attribute :description, :string do attribute :description, :string do
allow_nil? true allow_nil? true
public? true
end end
# Note: api_key intentionally not public for security
attribute :api_key, :string do attribute :api_key, :string do
allow_nil? true allow_nil? true
end end
+20 -1
View File
@@ -16,6 +16,14 @@ defmodule WandererApp.Api.AccessListMember do
includes([:access_list]) includes([:access_list])
default_fields([
:name,
:eve_character_id,
:eve_corporation_id,
:eve_alliance_id,
:role
])
derive_filter?(true) derive_filter?(true)
derive_sort?(true) derive_sort?(true)
@@ -53,7 +61,11 @@ defmodule WandererApp.Api.AccessListMember do
:role :role
] ]
defaults [:create, :read, :update, :destroy] defaults [:create, :read, :destroy]
update :update do
require_atomic? false
end
read :read_by_access_list do read :read_by_access_list do
argument(:access_list_id, :string, allow_nil?: false) argument(:access_list_id, :string, allow_nil?: false)
@@ -67,12 +79,14 @@ defmodule WandererApp.Api.AccessListMember do
update :block do update :block do
accept([]) accept([])
require_atomic? false
change(set_attribute(:blocked, true)) change(set_attribute(:blocked, true))
end end
update :unblock do update :unblock do
accept([]) accept([])
require_atomic? false
change(set_attribute(:blocked, false)) change(set_attribute(:blocked, false))
end end
@@ -83,22 +97,27 @@ defmodule WandererApp.Api.AccessListMember do
attribute :name, :string do attribute :name, :string do
allow_nil? false allow_nil? false
public? true
end end
attribute :eve_character_id, :string do attribute :eve_character_id, :string do
allow_nil? true allow_nil? true
public? true
end end
attribute :eve_corporation_id, :string do attribute :eve_corporation_id, :string do
allow_nil? true allow_nil? true
public? true
end end
attribute :eve_alliance_id, :string do attribute :eve_alliance_id, :string do
allow_nil? true allow_nil? true
public? true
end end
attribute :role, :atom do attribute :role, :atom do
default "viewer" default "viewer"
public? true
constraints( constraints(
one_of: [ one_of: [
+80
View File
@@ -0,0 +1,80 @@
defmodule WandererApp.Api.ActorHelpers do
@moduledoc """
Utilities for extracting actor information from Ash contexts.
Provides helper functions for working with ActorWithMap and extracting
user, map, and character information from various context formats.
"""
alias WandererApp.Api.ActorWithMap
@doc """
Extract map from actor or context.
Handles various context formats:
- Direct ActorWithMap struct
- Context map with :actor key
- Context map with :map key
- Ash.Resource.Change.Context struct
"""
def get_map(%{actor: %ActorWithMap{map: %{} = map}}), do: map
def get_map(%{map: %{} = map}), do: map
# Handle Ash.Resource.Change.Context struct
def get_map(%Ash.Resource.Change.Context{actor: %ActorWithMap{map: %{} = map}}), do: map
def get_map(%Ash.Resource.Change.Context{actor: _}), do: nil
def get_map(context) when is_map(context) do
# For plain maps, check private.actor
with private when is_map(private) <- Map.get(context, :private),
%ActorWithMap{map: %{} = map} <- Map.get(private, :actor) do
map
else
_ -> nil
end
end
def get_map(_), do: nil
@doc """
Extract user from actor.
Handles:
- ActorWithMap struct
- Direct user struct with :id field
"""
def get_user(%ActorWithMap{user: user}), do: user
def get_user(%{id: _} = user), do: user
def get_user(_), do: nil
@doc """
Get character IDs for the actor.
Used for ACL filtering to determine which resources the user can access.
Returns {:ok, list} or {:ok, []} if no characters found.
"""
def get_character_ids(%ActorWithMap{user: user}), do: get_character_ids(user)
def get_character_ids(%{characters: characters}) when is_list(characters) do
{:ok, Enum.map(characters, & &1.id)}
end
def get_character_ids(%{characters: %Ecto.Association.NotLoaded{}, id: user_id}) do
# Load characters from database
load_characters_by_id(user_id)
end
def get_character_ids(%{id: user_id}) do
# Fallback: load user with characters
load_characters_by_id(user_id)
end
def get_character_ids(_), do: {:ok, []}
defp load_characters_by_id(user_id) do
case WandererApp.Api.User.by_id(user_id, load: [:characters]) do
{:ok, user} -> {:ok, Enum.map(user.characters, & &1.id)}
_ -> {:ok, []}
end
end
end
+15
View File
@@ -0,0 +1,15 @@
defmodule WandererApp.Api.ActorWithMap do
@moduledoc """
Wraps a user and map together as an actor for token-based authentication.
When API requests use Bearer token auth, the token identifies both the user
(map owner) and the map. This struct allows passing both through Ash's actor system.
"""
@enforce_keys [:user, :map]
defstruct [:user, :map]
def new(user, map) do
%__MODULE__{user: user, map: map}
end
end
@@ -0,0 +1,40 @@
defmodule WandererApp.Api.Changes.InjectMapFromActor do
@moduledoc """
Ash change that injects map_id from the authenticated actor.
For token-based auth, the map is determined by the API token.
This change automatically sets map_id, so clients don't need to provide it.
"""
use Ash.Resource.Change
alias WandererApp.Api.ActorHelpers
@impl true
def change(changeset, _opts, context) do
case ActorHelpers.get_map(context) do
%{id: map_id} ->
Ash.Changeset.force_change_attribute(changeset, :map_id, map_id)
_other ->
# nil or unexpected return shape - check for direct map_id
# Check params (input), arguments, and attributes (in that order)
map_id =
Map.get(changeset.params, :map_id) ||
Ash.Changeset.get_argument(changeset, :map_id) ||
Ash.Changeset.get_attribute(changeset, :map_id)
case map_id do
nil ->
Ash.Changeset.add_error(changeset,
field: :map_id,
message: "map_id is required (provide via token or attribute)"
)
_map_id ->
# map_id provided directly (internal calls, tests)
changeset
end
end
end
end
+6 -3
View File
@@ -39,6 +39,8 @@ defmodule WandererApp.Api.Character do
define(:active_by_user, define(:active_by_user,
action: :active_by_user action: :active_by_user
) )
define(:admin_all, action: :admin_all)
end end
actions do actions do
@@ -69,9 +71,8 @@ defmodule WandererApp.Api.Character do
filter(expr(user_id == ^arg(:user_id) and deleted == false)) filter(expr(user_id == ^arg(:user_id) and deleted == false))
end end
read :available_by_map do read :admin_all do
argument(:map_id, :uuid, allow_nil?: false) prepare build(load: [:user])
filter(expr(user_id == ^arg(:user_id) and deleted == false))
end end
read :last_active do read :last_active do
@@ -100,6 +101,7 @@ defmodule WandererApp.Api.Character do
update :mark_as_deleted do update :mark_as_deleted do
accept([]) accept([])
require_atomic? false
change(atomic_update(:deleted, true)) change(atomic_update(:deleted, true))
change(atomic_update(:user_id, nil)) change(atomic_update(:user_id, nil))
@@ -107,6 +109,7 @@ defmodule WandererApp.Api.Character do
update :update_online do update :update_online do
accept([:online]) accept([:online])
require_atomic? false
end end
update :update_location do update :update_location do
@@ -33,7 +33,11 @@ defmodule WandererApp.Api.CorpWalletTransaction do
:ref_type :ref_type
] ]
defaults [:create, :read, :update, :destroy] defaults [:create, :read, :destroy]
update :update do
require_atomic? false
end
create :new do create :new do
accept [ accept [
+7 -1
View File
@@ -36,7 +36,11 @@ defmodule WandererApp.Api.License do
:expire_at :expire_at
] ]
defaults [:read, :update, :destroy] defaults [:read, :destroy]
update :update do
require_atomic? false
end
create :create do create :create do
primary? true primary? true
@@ -58,12 +62,14 @@ defmodule WandererApp.Api.License do
update :invalidate do update :invalidate do
accept([]) accept([])
require_atomic? false
change(set_attribute(:is_valid, false)) change(set_attribute(:is_valid, false))
end end
update :set_valid do update :set_valid do
accept([]) accept([])
require_atomic? false
change(set_attribute(:is_valid, true)) change(set_attribute(:is_valid, true))
end end
+155 -9
View File
@@ -8,9 +8,13 @@ defmodule WandererApp.Api.Map do
alias Ash.Resource.Change.Builtins alias Ash.Resource.Change.Builtins
require Logger
postgres do postgres do
repo(WandererApp.Repo) repo(WandererApp.Repo)
table("maps_v1") table("maps_v1")
migration_defaults scopes: "'{wormholes}'"
end end
json_api do json_api do
@@ -44,6 +48,7 @@ defmodule WandererApp.Api.Map do
code_interface do code_interface do
define(:available, action: :available) define(:available, action: :available)
define(:get_map_by_slug, action: :by_slug, args: [:slug]) define(:get_map_by_slug, action: :by_slug, args: [:slug])
define(:by_api_key, action: :by_api_key, args: [:api_key])
define(:new, action: :new) define(:new, action: :new)
define(:create, action: :create) define(:create, action: :create)
define(:update, action: :update) define(:update, action: :update)
@@ -54,6 +59,7 @@ defmodule WandererApp.Api.Map do
define(:mark_as_deleted, action: :mark_as_deleted) define(:mark_as_deleted, action: :mark_as_deleted)
define(:update_api_key, action: :update_api_key) define(:update_api_key, action: :update_api_key)
define(:toggle_webhooks, action: :toggle_webhooks) define(:toggle_webhooks, action: :toggle_webhooks)
define(:toggle_sse, action: :toggle_sse)
define(:by_id, define(:by_id,
get_by: [:id], get_by: [:id],
@@ -61,6 +67,8 @@ defmodule WandererApp.Api.Map do
) )
define(:duplicate, action: :duplicate) define(:duplicate, action: :duplicate)
define(:admin_all, action: :admin_all)
define(:restore, action: :restore)
end end
calculations do calculations do
@@ -90,22 +98,41 @@ defmodule WandererApp.Api.Map do
filter expr(slug == ^arg(:slug)) filter expr(slug == ^arg(:slug))
end end
read :by_api_key do
get? true
argument :api_key, :string, allow_nil?: false
prepare WandererApp.Api.Preparations.SecureApiKeyLookup
end
read :available do read :available do
prepare WandererApp.Api.Preparations.FilterMapsByRoles prepare WandererApp.Api.Preparations.FilterMapsByRoles
end end
create :new do read :admin_all do
accept [:name, :slug, :description, :scope, :only_tracked_characters, :owner_id] # Admin-only action that bypasses FilterMapsByRoles
primary?(true) # Returns ALL maps including soft-deleted ones with owner and ACLs loaded
prepare build(load: [:owner, :acls])
end
argument :owner_id, :uuid, allow_nil?: false create :new do
accept [
:name,
:slug,
:description,
:scope,
:scopes,
:only_tracked_characters,
:owner_id,
:sse_enabled
]
primary?(true)
argument :create_default_acl, :boolean, allow_nil?: true argument :create_default_acl, :boolean, allow_nil?: true
argument :acls, {:array, :uuid}, allow_nil?: true argument :acls, {:array, :uuid}, allow_nil?: true
argument :acls_text_input, :string, allow_nil?: true argument :acls_text_input, :string, allow_nil?: true
argument :scope_text_input, :string, allow_nil?: true argument :scope_text_input, :string, allow_nil?: true
argument :acls_empty_selection, :string, allow_nil?: true argument :acls_empty_selection, :string, allow_nil?: true
change manage_relationship(:owner_id, :owner, on_lookup: :relate, on_no_match: nil)
change manage_relationship(:acls, type: :append_and_remove) change manage_relationship(:acls, type: :append_and_remove)
change WandererApp.Api.Changes.SlugifyName change WandererApp.Api.Changes.SlugifyName
end end
@@ -113,7 +140,17 @@ defmodule WandererApp.Api.Map do
update :update do update :update do
primary? true primary? true
require_atomic? false require_atomic? false
accept [:name, :slug, :description, :scope, :only_tracked_characters, :owner_id]
accept [
:name,
:slug,
:description,
:scope,
:scopes,
:only_tracked_characters,
:owner_id,
:sse_enabled
]
argument :owner_id_text_input, :string, allow_nil?: true argument :owner_id_text_input, :string, allow_nil?: true
argument :acls_text_input, :string, allow_nil?: true argument :acls_text_input, :string, allow_nil?: true
@@ -128,6 +165,9 @@ defmodule WandererApp.Api.Map do
) )
change WandererApp.Api.Changes.SlugifyName change WandererApp.Api.Changes.SlugifyName
# Validate subscription when enabling SSE
validate &validate_sse_subscription/2
end end
update :update_acls do update :update_acls do
@@ -142,33 +182,64 @@ defmodule WandererApp.Api.Map do
update :assign_owner do update :assign_owner do
accept [:owner_id] accept [:owner_id]
require_atomic? false
end end
update :update_hubs do update :update_hubs do
accept [:hubs] accept [:hubs]
require_atomic? false
end end
update :update_options do update :update_options do
accept [:options] accept [:options]
require_atomic? false
end end
update :mark_as_deleted do update :mark_as_deleted do
accept([]) accept([])
require_atomic? false
change(set_attribute(:deleted, true)) change(set_attribute(:deleted, true))
end end
update :restore do
# Admin-only action to restore a soft-deleted map
accept([])
require_atomic? false
change(set_attribute(:deleted, false))
end
update :update_api_key do update :update_api_key do
accept [:public_api_key] accept [:public_api_key]
require_atomic? false
end end
update :toggle_webhooks do update :toggle_webhooks do
accept [:webhooks_enabled] accept [:webhooks_enabled]
require_atomic? false
change after_action(fn _changeset, record, _context ->
WandererApp.Map.update_webhooks_enabled(record.id, record.webhooks_enabled)
{:ok, record}
end)
end
update :toggle_sse do
require_atomic? false
accept [:sse_enabled]
# Validate subscription when enabling SSE
validate &validate_sse_subscription/2
change after_action(fn _changeset, record, _context ->
WandererApp.Map.update_sse_enabled(record.id, record.sse_enabled)
{:ok, record}
end)
end end
create :duplicate do create :duplicate do
accept [:name, :description, :scope, :only_tracked_characters] accept [:name, :description, :scope, :scopes, :only_tracked_characters]
argument :source_map_id, :uuid, allow_nil?: false argument :source_map_id, :uuid, allow_nil?: false
argument :copy_acls, :boolean, default: true argument :copy_acls, :boolean, default: true
argument :copy_user_settings, :boolean, default: true argument :copy_user_settings, :boolean, default: true
@@ -184,9 +255,14 @@ defmodule WandererApp.Api.Map do
description = description =
Ash.Changeset.get_attribute(changeset, :description) || source_map.description Ash.Changeset.get_attribute(changeset, :description) || source_map.description
# Use provided scopes or fall back to source map scopes
scopes =
Ash.Changeset.get_attribute(changeset, :scopes) || source_map.scopes
changeset changeset
|> Ash.Changeset.change_attribute(:description, description) |> Ash.Changeset.change_attribute(:description, description)
|> Ash.Changeset.change_attribute(:scope, source_map.scope) |> Ash.Changeset.change_attribute(:scope, source_map.scope)
|> Ash.Changeset.change_attribute(:scopes, scopes)
|> Ash.Changeset.change_attribute( |> Ash.Changeset.change_attribute(
:only_tracked_characters, :only_tracked_characters,
source_map.only_tracked_characters source_map.only_tracked_characters
@@ -312,12 +388,37 @@ defmodule WandererApp.Api.Map do
public?(true) public?(true)
end end
attribute :sse_enabled, :boolean do
default(false)
allow_nil?(false)
public?(true)
end
attribute :scopes, {:array, :atom} do
default([:wormholes])
allow_nil?(true)
public?(true)
constraints(
items: [
one_of: [
:wormholes,
:hi,
:low,
:null,
:pochven
]
]
)
end
create_timestamp(:inserted_at) create_timestamp(:inserted_at)
update_timestamp(:updated_at) update_timestamp(:updated_at)
end end
identities do identities do
identity :unique_slug, [:slug] identity :unique_slug, [:slug]
identity :unique_public_api_key, [:public_api_key]
end end
relationships do relationships do
@@ -344,4 +445,49 @@ defmodule WandererApp.Api.Map do
public? false public? false
end end
end end
# SSE Subscription Validation
#
# This validation ensures that SSE can only be enabled when:
# 1. SSE is being disabled (always allowed)
# 2. Map is being created (skip validation, will be checked on first update)
# 3. Community Edition mode (always allowed)
# 4. Enterprise mode with active subscription
defp validate_sse_subscription(changeset, _context) do
sse_enabled = Ash.Changeset.get_attribute(changeset, :sse_enabled)
map_id = changeset.data.id
subscriptions_enabled = WandererApp.Env.map_subscriptions_enabled?()
cond do
# Not enabling SSE - no validation needed
not sse_enabled ->
:ok
# Map creation (no ID yet) - skip validation
is_nil(map_id) ->
:ok
# Community Edition mode - always allow
not subscriptions_enabled ->
:ok
# Enterprise mode - check subscription
true ->
validate_active_subscription(map_id)
end
end
defp validate_active_subscription(map_id) do
case WandererApp.Map.is_subscription_active?(map_id) do
{:ok, true} ->
:ok
{:ok, false} ->
{:error, field: :sse_enabled, message: "Active subscription required to enable SSE"}
{:error, reason} ->
Logger.error("Error checking subscription status: #{inspect(reason)}")
{:error, field: :sse_enabled, message: "Unable to verify subscription status"}
end
end
end end

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