Compare commits

...

189 Commits

Author SHA1 Message Date
CI
00bf620e35 chore: release version v1.84.27 2025-11-17 09:16:32 +00:00
Dmitry Popov
46eef60d86 chore: fixed warnings 2025-11-17 10:15:57 +01:00
Dmitry Popov
fe836442ab fix(core): supported characters_updates for external events 2025-11-17 00:08:08 +01:00
Dmitry Popov
9514806dbb fix(core): improved character tracking 2025-11-16 23:45:39 +01:00
Dmitry Popov
4e6423ebc8 fix(core): improved character tracking 2025-11-16 18:28:58 +01:00
Dmitry Popov
a97e598299 fix(core): improved character location tracking 2025-11-16 16:39:39 +01:00
CI
9c26b50aac chore: [skip ci] 2025-11-16 01:14:41 +00:00
CI
3f2ddf5cc4 chore: release version v1.84.26 2025-11-16 01:14:41 +00:00
Dmitry Popov
233b2bd7a4 fix(core): disable character tracker pausing 2025-11-16 02:14:05 +01:00
CI
0d35268efc chore: [skip ci] 2025-11-16 01:01:35 +00:00
CI
d169220eb2 chore: release version v1.84.25 2025-11-16 01:01:35 +00:00
Dmitry Popov
182d5ec9fb fix(core): used upsert for adding map systems 2025-11-16 02:00:59 +01:00
CI
32958253b7 chore: [skip ci] 2025-11-15 22:50:08 +00:00
CI
c011d56ce7 chore: release version v1.84.24 2025-11-15 22:50:08 +00:00
Dmitry Popov
73d1921d42 Merge pull request #549 from wanderer-industries/redesign-and-fixes
fix(Map): New design and prepared main pages for new patch
2025-11-16 02:49:36 +04:00
Dmitry Popov
7bb810e1e6 chore: update bg image url 2025-11-15 23:35:59 +01:00
CI
c90ac7b1e3 chore: [skip ci] 2025-11-15 22:17:28 +00:00
CI
005e0c2bc6 chore: release version v1.84.23 2025-11-15 22:17:28 +00:00
Dmitry Popov
808acb540e fix(core): fixed map pings cancel errors 2025-11-15 23:16:58 +01:00
DanSylvest
06626f910b fix(Map): Fixed problem related with error if settings was removed and mapper crashed. Fixed settings reset. 2025-11-15 21:30:45 +03:00
CI
812582d955 chore: [skip ci] 2025-11-15 11:38:00 +00:00
CI
f3077c0bf1 chore: release version v1.84.22 2025-11-15 11:38:00 +00:00
Dmitry Popov
32c70cbbad Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-15 12:37:31 +01:00
Dmitry Popov
8934935e10 fix(core): fixed map initialization 2025-11-15 12:37:27 +01:00
CI
20c8a53712 chore: [skip ci] 2025-11-15 08:48:30 +00:00
CI
b22970fef3 chore: release version v1.84.21 2025-11-15 08:48:30 +00:00
Dmitry Popov
cf72394ef9 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-15 09:47:53 +01:00
Dmitry Popov
e6dbba7283 fix(core): fixed map characters adding 2025-11-15 09:47:48 +01:00
CI
843b3b86b2 chore: [skip ci] 2025-11-15 07:29:25 +00:00
CI
bd865b9f64 chore: release version v1.84.20 2025-11-15 07:29:25 +00:00
Dmitry Popov
ae91cd2f92 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-15 08:25:59 +01:00
Dmitry Popov
0be7a5f9d0 fix(core): fixed map start issues 2025-11-15 08:25:55 +01:00
CI
e15bfa426a chore: [skip ci] 2025-11-14 19:28:51 +00:00
CI
4198e4b07a chore: release version v1.84.19 2025-11-14 19:28:51 +00:00
Dmitry Popov
03ee08ff67 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-14 20:28:16 +01:00
Dmitry Popov
ac4dd4c28b fix(core): fixed map start issues 2025-11-14 20:28:12 +01:00
CI
308e81a464 chore: [skip ci] 2025-11-14 18:36:20 +00:00
CI
6f4240d931 chore: release version v1.84.18 2025-11-14 18:36:20 +00:00
Dmitry Popov
847b45a431 fix(core): added gracefull map poll recovery from saved state. added map slug unique checks 2025-11-14 19:35:45 +01:00
CI
5ec97d74ca chore: [skip ci] 2025-11-14 13:43:40 +00:00
CI
74359a5542 chore: release version v1.84.17 2025-11-14 13:43:40 +00:00
Dmitry Popov
0020f46dd8 fix(core): fixed activity tracking issues 2025-11-14 14:42:44 +01:00
CI
a6751b45c6 chore: [skip ci] 2025-11-13 16:20:24 +00:00
CI
f48aeb5cec chore: release version v1.84.16 2025-11-13 16:20:24 +00:00
Dmitry Popov
a5f25646c9 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-13 17:19:47 +01:00
Dmitry Popov
23cf1fd96f fix(core): removed maps auto-start logic 2025-11-13 17:19:44 +01:00
CI
6f15521069 chore: [skip ci] 2025-11-13 14:49:32 +00:00
CI
9d41e57c06 chore: release version v1.84.15 2025-11-13 14:49:32 +00:00
Dmitry Popov
ea9a22df09 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-13 15:49:01 +01:00
Dmitry Popov
0d4fd6f214 fix(core): fixed maps start/stop logic, added server downtime period support 2025-11-13 15:48:56 +01:00
CI
87a6c20545 chore: [skip ci] 2025-11-13 14:46:26 +00:00
CI
c375f4e4ce chore: release version v1.84.14 2025-11-13 14:46:26 +00:00
Dmitry Popov
843a6d7320 Merge pull request #543 from wanderer-industries/fix-error-on-remove-settings
fix(Map): Fixed problem related with error if settings was removed an…
2025-11-13 18:43:13 +04:00
DanSylvest
98c54a3413 fix(Map): Fixed problem related with error if settings was removed and mapper crashed. Fixed settings reset. 2025-11-13 12:53:40 +03:00
CI
0439110938 chore: [skip ci] 2025-11-13 07:52:33 +00:00
CI
8ce1e5fa3e chore: release version v1.84.13 2025-11-13 07:52:33 +00:00
Dmitry Popov
ebaf6bcdc6 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-13 08:52:00 +01:00
Dmitry Popov
40d947bebc chore: updated RELEASE_NODE for server defaults 2025-11-13 08:51:56 +01:00
CI
61d1c3848f chore: [skip ci] 2025-11-13 07:39:29 +00:00
CI
e152ce179f chore: release version v1.84.12 2025-11-13 07:39:29 +00:00
Dmitry Popov
7bbe387183 chore: reduce garbage collection interval 2025-11-13 08:38:52 +01:00
CI
b1555ff03c chore: [skip ci] 2025-11-12 18:53:48 +00:00
CI
e624499244 chore: release version v1.84.11 2025-11-12 18:53:48 +00:00
Dmitry Popov
6a1976dec6 Merge pull request #541 from guarzo/guarzo/apifun2
fix: api and doc updates
2025-11-12 22:53:17 +04:00
Guarzo
3db24c4344 fix: api and doc updates 2025-11-12 18:39:21 +00:00
CI
883c09f255 chore: [skip ci] 2025-11-12 17:28:54 +00:00
CI
ff24d80038 chore: release version v1.84.10 2025-11-12 17:28:54 +00:00
Dmitry Popov
63cbc9c0b9 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-12 18:28:20 +01:00
Dmitry Popov
8056972a27 fix(core): Fixed adding system on character dock 2025-11-12 18:28:16 +01:00
CI
1759d46740 chore: [skip ci] 2025-11-12 13:28:14 +00:00
CI
e4b7d2e45b chore: release version v1.84.9 2025-11-12 13:28:14 +00:00
Dmitry Popov
41573cbee3 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-12 14:27:43 +01:00
Dmitry Popov
24ffc20bb8 chore: added ccp attribution to footer 2025-11-12 14:27:40 +01:00
CI
e077849b66 chore: [skip ci] 2025-11-12 12:42:09 +00:00
CI
375a9ef65b chore: release version v1.84.8 2025-11-12 12:42:08 +00:00
Dmitry Popov
9bf90ab752 fix(core): added cleanup jobs for old system signatures & chain passages 2025-11-12 13:41:33 +01:00
CI
90c3481151 chore: [skip ci] 2025-11-12 10:57:58 +00:00
CI
e36b08a7e5 chore: release version v1.84.7 2025-11-12 10:57:58 +00:00
Dmitry Popov
e1f79170c3 Merge pull request #540 from guarzo/guarzo/apifun
fix: api and search fixes
2025-11-12 14:54:33 +04:00
Guarzo
68b5455e91 bug fix 2025-11-12 07:25:49 +00:00
Guarzo
f28e75c7f4 pr updates 2025-11-12 07:16:21 +00:00
Guarzo
6091adb28e fix: api and structure search fixes 2025-11-12 07:07:39 +00:00
CI
d4657b335f chore: [skip ci] 2025-11-12 00:13:07 +00:00
CI
7fee850902 chore: release version v1.84.6 2025-11-12 00:13:07 +00:00
Dmitry Popov
648c168a66 fix(core): Added map slug uniqness checking while using API 2025-11-12 01:12:13 +01:00
CI
f5c4b2c407 chore: [skip ci] 2025-11-11 12:52:39 +00:00
CI
b592223d52 chore: release version v1.84.5 2025-11-11 12:52:39 +00:00
Dmitry Popov
5cf118c6ee Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-11 13:52:11 +01:00
Dmitry Popov
b25013c652 fix(core): Added tracking for map & character event handling errors 2025-11-11 13:52:07 +01:00
CI
cf43861b11 chore: [skip ci] 2025-11-11 12:27:54 +00:00
CI
b5fe8f8878 chore: release version v1.84.4 2025-11-11 12:27:54 +00:00
Dmitry Popov
5e5068c7de fix(core): fixed issue with updating system signatures 2025-11-11 13:27:17 +01:00
CI
624b51edfb chore: [skip ci] 2025-11-11 09:52:29 +00:00
CI
a72f8e60c4 chore: release version v1.84.3 2025-11-11 09:52:29 +00:00
Dmitry Popov
dec8ae50c9 Merge branch 'develop' 2025-11-11 10:51:55 +01:00
Dmitry Popov
0332d36a8e fix(core): fixed linked signature time status update
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
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-11 10:51:43 +01:00
CI
8444c7f82d chore: [skip ci] 2025-11-10 16:57:53 +00:00
CI
ec3fc7447e chore: release version v1.84.2 2025-11-10 16:57:53 +00:00
Dmitry Popov
20ec2800c9 Merge pull request #538 from wanderer-industries/develop
Develop
2025-11-10 20:56:53 +04:00
Dmitry Popov
6fbf43e860 fix(api): fixed api for get/update map systems
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
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-10 17:23:44 +01:00
Dmitry Popov
697da38020 Merge pull request #537 from guarzo/guarzo/apisystemperf
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
fix: add indexes for map/system
2025-11-09 01:48:01 +04:00
Guarzo
4bc65b43d2 fix: add index for map/systems api 2025-11-08 14:30:19 +00:00
Dmitry Popov
910ec97fd1 chore: refactored map server processes
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-11-06 09:23:19 +01:00
Dmitry Popov
40ed58ee8c Merge pull request #536 from wanderer-industries/refactor-map-servers
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
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
Refactor map servers
2025-11-06 03:03:57 +04:00
Dmitry Popov
c18d241c77 Merge branch 'develop' into refactor-map-servers 2025-11-06 00:01:32 +01:00
Dmitry Popov
8b42908a5c chore: refactored map server processes 2025-11-06 00:01:04 +01:00
Dmitry Popov
6d32505a59 chore: added map cached rtree implementation 2025-11-04 23:40:37 +01:00
Dmitry Popov
fe8a34c77d chore: refactored map state usage 2025-11-04 22:40:04 +01:00
CI
d12cafcca8 chore: [skip ci] 2025-11-01 20:01:52 +00:00
CI
38a9c76ff0 chore: release version v1.84.1 2025-11-01 20:01:52 +00:00
Dmitry Popov
d6c30b4a53 fix(Core): Fixed connection time status update issue 2025-11-01 21:01:18 +01:00
CI
53a81daaf5 chore: [skip ci] 2025-10-29 14:30:52 +00:00
CI
92081c99e3 chore: release version v1.84.0 2025-10-29 14:30:52 +00:00
Dmitry Popov
d78020d2f5 Merge pull request #535 from wanderer-industries/esi-rate-limits
feat(Core): ESI API rate limits support
2025-10-29 18:30:18 +04:00
Dmitry Popov
fb1a9b440d feat(Core): ESI API rate limits support
fixes #534
2025-10-29 15:29:12 +01:00
CI
0141ac46e3 chore: [skip ci] 2025-10-29 09:24:54 +00:00
CI
d2bf6a8f86 chore: release version v1.83.4 2025-10-29 09:24:54 +00:00
Dmitry Popov
1844e4c757 fix(Core): Fixed page reloads 2025-10-29 10:23:54 +01:00
CI
d407efe805 chore: [skip ci] 2025-10-27 23:52:49 +00:00
CI
021e04d87a chore: release version v1.83.3 2025-10-27 23:52:49 +00:00
Dmitry Popov
7844c9db34 fix(Core): Fixed old map API for systems & added small QOL improvements 2025-10-28 00:52:04 +01:00
CI
355beb8394 chore: [skip ci] 2025-10-22 16:09:15 +00:00
CI
d82eeba792 chore: release version v1.83.2 2025-10-22 16:09:15 +00:00
Dmitry Popov
0396b05e58 fix(Connections): Set new connection time status based on to/from system class 2025-10-22 18:08:38 +02:00
CI
9494a9eb37 chore: [skip ci] 2025-10-21 14:13:39 +00:00
CI
8238f84ac7 chore: release version v1.83.1 2025-10-21 14:13:39 +00:00
Dmitry Popov
1cf19b2a50 fix(Kills): Fixed zkb links (added following '/'). 2025-10-21 16:13:08 +02:00
CI
e8543fd2f8 chore: [skip ci] 2025-10-21 07:45:45 +00:00
CI
c7f360e1fa chore: release version v1.83.0 2025-10-21 07:45:45 +00:00
Dmitry Popov
a2b83f7f0c Merge pull request #531 from wanderer-industries/copy-past-roles
Copy past roles
2025-10-21 11:45:13 +04:00
CI
ae5689a403 chore: [skip ci] 2025-10-21 06:52:44 +00:00
CI
c46af1d286 chore: release version v1.82.3 2025-10-21 06:52:44 +00:00
Aleksei Chichenkov
d17ba2168c Merge pull request #533 from wanderer-industries/fix-db
fix(Map): Fix system static info - add source region for U319 from Null-sec
2025-10-21 09:52:17 +03:00
DanSylvest
80c14716eb fix(Map): Fix system static info - add source region for U319 from Null-sec 2025-10-21 09:50:10 +03:00
CI
8541fcd29b chore: [skip ci] 2025-10-21 06:41:33 +00:00
CI
65d6acd7fb chore: release version v1.82.2 2025-10-21 06:41:33 +00:00
Aleksei Chichenkov
8b5f83d6b2 Merge pull request #532 from wanderer-industries/fix-db
fix(Map): Fix system static info - for J012635 add D382; for J015092 …
2025-10-21 09:41:08 +03:00
DanSylvest
5e18891f4b fix(Map): Fix system static info - for J012635 add D382; for J015092 - changed from J244, Z060 to N110, J244; for J000487 removed C008 2025-10-21 09:38:47 +03:00
DanSylvest
74e0b85748 fix(Map): Copy-Paste restriction: support from FE side - fixed problem with incorrect disabling copy and paste buttons 2025-10-21 09:20:41 +03:00
DanSylvest
81d3495b65 fix(Map): Copy-Paste restriction: support from FE side - removed unnecessary constant 2025-10-20 12:51:20 +03:00
CI
d1959ca09f chore: [skip ci] 2025-10-20 09:33:16 +00:00
CI
ec7a5ecf10 chore: release version v1.82.1 2025-10-20 09:33:16 +00:00
DanSylvest
70b9ec99ba Merge remote-tracking branch 'origin/copy-past-roles' into copy-past-roles 2025-10-20 12:32:41 +03:00
Dmitry Popov
7147d79166 Merge branch 'main' into copy-past-roles 2025-10-20 11:33:45 +02:00
Dmitry Popov
1dad9316bd fix(Core): Fixed 'viewer' map access & characters tracking 2025-10-20 11:32:32 +02:00
DanSylvest
872f7dcf48 fix(Map): Copy-Paste restriction: support from FE side 2025-10-20 12:32:07 +03:00
Dmitry Popov
02b450325e fix(Core): Added Eve data downloaded files cleanup logic 2025-10-19 12:37:31 +02:00
Dmitry Popov
136bc4cbb9 feat(Core): Added map roles settings for copy/paste 2025-10-19 12:03:16 +02:00
Dmitry Popov
dab49df9aa Merge branch 'main' into copy-past-roles 2025-10-16 16:01:41 +02:00
Dmitry Popov
6286087f3e feat(Core): Added map roles settings for copy/paste 2025-10-16 16:01:12 +02:00
CI
4ce7160f79 chore: [skip ci] 2025-10-15 19:59:55 +00:00
CI
2913bf19b0 chore: release version v1.82.0 2025-10-15 19:59:55 +00:00
Dmitry Popov
7bd6be6fd0 Merge pull request #528 from wanderer-industries/copy-past-systems-with-connections
Copy past systems with connections
2025-10-15 23:56:31 +04:00
Dmitry Popov
705daa286b Merge branch 'main' into copy-past-systems-with-connections 2025-10-15 21:56:09 +02:00
Dmitry Popov
614d06be66 feat(Core): Added an ability to copy/paste selected map area between maps 2025-10-15 21:55:56 +02:00
CI
dec3e9a7ce chore: [skip ci] 2025-10-15 19:08:54 +00:00
CI
0017ac3373 chore: release version v1.81.15 2025-10-15 19:08:54 +00:00
DanSylvest
ae34744578 Merge remote-tracking branch 'origin/main' 2025-10-15 22:06:53 +03:00
DanSylvest
76885058ef fix(Map): Fixed problem with commit - for correct restore deprecated data - change config key 2025-10-15 22:06:31 +03:00
CI
fccb007036 chore: [skip ci] 2025-10-15 18:56:52 +00:00
CI
a9f8901bd5 chore: release version v1.81.14 2025-10-15 18:56:52 +00:00
DanSylvest
8ae968b5be fix(Map): Fixed problem with commit - for correct restore deprecated data 2025-10-15 21:54:57 +03:00
Dmitry Popov
beffd45e4f Merge branch 'main' into copy-past-systems-with-connections 2025-10-15 16:30:57 +02:00
CI
4488d81e8d chore: [skip ci] 2025-10-15 12:30:56 +00:00
CI
618cc8c5f1 chore: release version v1.81.13 2025-10-15 12:30:56 +00:00
Dmitry Popov
3fb22a877e fix(Core): Fixed system select after tab switch 2025-10-15 14:30:19 +02:00
Dmitry Popov
8759409b82 Merge branch 'main' into copy-past-systems-with-connections 2025-10-15 12:51:25 +02:00
CI
245647ae6a chore: [skip ci] 2025-10-15 10:33:07 +00:00
CI
eb7d33ea07 chore: release version v1.81.12 2025-10-15 10:33:07 +00:00
Dmitry Popov
3575b16def fix(Core): Fixed map events buffering on tab switch 2025-10-15 12:32:29 +02:00
CI
a6fb680be8 chore: [skip ci] 2025-10-15 09:59:48 +00:00
CI
9e17df5544 chore: release version v1.81.11 2025-10-15 09:59:48 +00:00
Dmitry Popov
683fde7be4 fix(Signatures): Fixed EOL indication for un-splashed and signatures list 2025-10-15 11:59:14 +02:00
DanSylvest
ee68ce92a2 fix(Map): Add ability to copy and past systems (UI part) 2025-10-14 14:34:47 +03:00
CI
8b4e38d795 chore: [skip ci] 2025-10-13 22:46:48 +00:00
CI
4995202627 chore: release version v1.81.10 2025-10-13 22:46:48 +00:00
Dmitry Popov
986b997a6a fix(Signatures): Rework for lazy signatures deletion
- if lazy delete enabled, linked connection deleted after signature only
now
- added sort by signature name for info column
- show signature temporary name if set on link signature to system &
signatures widget
2025-10-14 00:46:04 +02:00
CI
9a957af759 chore: [skip ci] 2025-10-12 21:37:58 +00:00
CI
c5a0a96016 chore: release version v1.81.9 2025-10-12 21:37:58 +00:00
Dmitry Popov
8715a6c0ac fix(Signatures): Fixed issue with wrong linked signatures deletions 2025-10-12 23:37:22 +02:00
CI
c9810095aa chore: [skip ci] 2025-10-11 16:12:20 +00:00
CI
69eb888469 chore: release version v1.81.8 2025-10-11 16:12:20 +00:00
DanSylvest
748347df9a fix(Map): Fix problem with restoring settings on widgets 2025-10-11 19:10:27 +03:00
CI
aa4d49027c chore: [skip ci] 2025-10-10 19:12:44 +00:00
CI
a9d7387e40 chore: release version v1.81.7 2025-10-10 19:12:44 +00:00
DanSylvest
dc4d260c9b fix(Map): Fixed problem with rendering dropdown classes in signatures 2025-10-10 22:10:54 +03:00
CI
dc430491bf chore: [skip ci] 2025-10-10 06:47:34 +00:00
CI
42cd261ea7 chore: release version v1.81.6 2025-10-10 06:47:34 +00:00
Aleksei Chichenkov
35af4fdc09 Merge pull request #520 from wanderer-industries/migrations
Migrations
2025-10-10 09:47:08 +03:00
220 changed files with 11721 additions and 135305 deletions

View File

@@ -1,5 +1,7 @@
export WEB_APP_URL="http://localhost:8000"
export RELEASE_COOKIE="PDpbnyo6mEI_0T4ZsHH_ESmi1vT1toQ8PTc0vbfg5FIT4Ih-Lh98mw=="
# Erlang node name for distributed Erlang (optional - defaults to wanderer@hostname)
# export RELEASE_NODE="wanderer@localhost"
export EVE_CLIENT_ID="<EVE_CLIENT_ID>"
export EVE_CLIENT_SECRET="<EVE_CLIENT_SECRET>"
export EVE_CLIENT_WITH_WALLET_ID="<EVE_CLIENT_WITH_WALLET_ID>"

View File

@@ -2,6 +2,457 @@
<!-- changelog -->
## [v1.84.27](https://github.com/wanderer-industries/wanderer/compare/v1.84.26...v1.84.27) (2025-11-17)
### Bug Fixes:
* core: supported characters_updates for external events
* core: improved character tracking
* core: improved character tracking
* core: improved character location tracking
## [v1.84.26](https://github.com/wanderer-industries/wanderer/compare/v1.84.25...v1.84.26) (2025-11-16)
### Bug Fixes:
* core: disable character tracker pausing
## [v1.84.25](https://github.com/wanderer-industries/wanderer/compare/v1.84.24...v1.84.25) (2025-11-16)
### Bug Fixes:
* core: used upsert for adding map systems
## [v1.84.24](https://github.com/wanderer-industries/wanderer/compare/v1.84.23...v1.84.24) (2025-11-15)
### Bug Fixes:
* Map: Fixed problem related with error if settings was removed and mapper crashed. Fixed settings reset.
## [v1.84.23](https://github.com/wanderer-industries/wanderer/compare/v1.84.22...v1.84.23) (2025-11-15)
### Bug Fixes:
* core: fixed map pings cancel errors
## [v1.84.22](https://github.com/wanderer-industries/wanderer/compare/v1.84.21...v1.84.22) (2025-11-15)
### Bug Fixes:
* core: fixed map initialization
## [v1.84.21](https://github.com/wanderer-industries/wanderer/compare/v1.84.20...v1.84.21) (2025-11-15)
### Bug Fixes:
* core: fixed map characters adding
## [v1.84.20](https://github.com/wanderer-industries/wanderer/compare/v1.84.19...v1.84.20) (2025-11-15)
### Bug Fixes:
* core: fixed map start issues
## [v1.84.19](https://github.com/wanderer-industries/wanderer/compare/v1.84.18...v1.84.19) (2025-11-14)
### Bug Fixes:
* core: fixed map start issues
## [v1.84.18](https://github.com/wanderer-industries/wanderer/compare/v1.84.17...v1.84.18) (2025-11-14)
### Bug Fixes:
* core: added gracefull map poll recovery from saved state. added map slug unique checks
## [v1.84.17](https://github.com/wanderer-industries/wanderer/compare/v1.84.16...v1.84.17) (2025-11-14)
### Bug Fixes:
* core: fixed activity tracking issues
## [v1.84.16](https://github.com/wanderer-industries/wanderer/compare/v1.84.15...v1.84.16) (2025-11-13)
### Bug Fixes:
* core: removed maps auto-start logic
## [v1.84.15](https://github.com/wanderer-industries/wanderer/compare/v1.84.14...v1.84.15) (2025-11-13)
### Bug Fixes:
* core: fixed maps start/stop logic, added server downtime period support
## [v1.84.14](https://github.com/wanderer-industries/wanderer/compare/v1.84.13...v1.84.14) (2025-11-13)
### Bug Fixes:
* Map: Fixed problem related with error if settings was removed and mapper crashed. Fixed settings reset.
## [v1.84.13](https://github.com/wanderer-industries/wanderer/compare/v1.84.12...v1.84.13) (2025-11-13)
## [v1.84.12](https://github.com/wanderer-industries/wanderer/compare/v1.84.11...v1.84.12) (2025-11-13)
## [v1.84.11](https://github.com/wanderer-industries/wanderer/compare/v1.84.10...v1.84.11) (2025-11-12)
### Bug Fixes:
* api and doc updates
## [v1.84.10](https://github.com/wanderer-industries/wanderer/compare/v1.84.9...v1.84.10) (2025-11-12)
### Bug Fixes:
* core: Fixed adding system on character dock
## [v1.84.9](https://github.com/wanderer-industries/wanderer/compare/v1.84.8...v1.84.9) (2025-11-12)
## [v1.84.8](https://github.com/wanderer-industries/wanderer/compare/v1.84.7...v1.84.8) (2025-11-12)
### Bug Fixes:
* core: added cleanup jobs for old system signatures & chain passages
## [v1.84.7](https://github.com/wanderer-industries/wanderer/compare/v1.84.6...v1.84.7) (2025-11-12)
### Bug Fixes:
* api and structure search fixes
## [v1.84.6](https://github.com/wanderer-industries/wanderer/compare/v1.84.5...v1.84.6) (2025-11-12)
### Bug Fixes:
* core: Added map slug uniqness checking while using API
## [v1.84.5](https://github.com/wanderer-industries/wanderer/compare/v1.84.4...v1.84.5) (2025-11-11)
### Bug Fixes:
* core: Added tracking for map & character event handling errors
## [v1.84.4](https://github.com/wanderer-industries/wanderer/compare/v1.84.3...v1.84.4) (2025-11-11)
### Bug Fixes:
* core: fixed issue with updating system signatures
## [v1.84.3](https://github.com/wanderer-industries/wanderer/compare/v1.84.2...v1.84.3) (2025-11-11)
### Bug Fixes:
* core: fixed linked signature time status update
## [v1.84.2](https://github.com/wanderer-industries/wanderer/compare/v1.84.1...v1.84.2) (2025-11-10)
### Bug Fixes:
* api: fixed api for get/update map systems
* add index for map/systems api
## [v1.84.1](https://github.com/wanderer-industries/wanderer/compare/v1.84.0...v1.84.1) (2025-11-01)
### Bug Fixes:
* Core: Fixed connection time status update issue
## [v1.84.0](https://github.com/wanderer-industries/wanderer/compare/v1.83.4...v1.84.0) (2025-10-29)
### Features:
* Core: ESI API rate limits support
## [v1.83.4](https://github.com/wanderer-industries/wanderer/compare/v1.83.3...v1.83.4) (2025-10-29)
### Bug Fixes:
* Core: Fixed page reloads
## [v1.83.3](https://github.com/wanderer-industries/wanderer/compare/v1.83.2...v1.83.3) (2025-10-27)
### Bug Fixes:
* Core: Fixed old map API for systems & added small QOL improvements
## [v1.83.2](https://github.com/wanderer-industries/wanderer/compare/v1.83.1...v1.83.2) (2025-10-22)
### Bug Fixes:
* Connections: Set new connection time status based on to/from system class
## [v1.83.1](https://github.com/wanderer-industries/wanderer/compare/v1.83.0...v1.83.1) (2025-10-21)
### Bug Fixes:
* Kills: Fixed zkb links (added following '/').
## [v1.83.0](https://github.com/wanderer-industries/wanderer/compare/v1.82.3...v1.83.0) (2025-10-21)
### Features:
* Core: Added map roles settings for copy/paste
* Core: Added map roles settings for copy/paste
### Bug Fixes:
* Map: Copy-Paste restriction: support from FE side - fixed problem with incorrect disabling copy and paste buttons
* Map: Copy-Paste restriction: support from FE side - removed unnecessary constant
* Map: Copy-Paste restriction: support from FE side
* Core: Added Eve data downloaded files cleanup logic
## [v1.82.3](https://github.com/wanderer-industries/wanderer/compare/v1.82.2...v1.82.3) (2025-10-21)
### Bug Fixes:
* Map: Fix system static info - add source region for U319 from Null-sec
## [v1.82.2](https://github.com/wanderer-industries/wanderer/compare/v1.82.1...v1.82.2) (2025-10-21)
### Bug Fixes:
* Map: Fix system static info - for J012635 add D382; for J015092 - changed from J244, Z060 to N110, J244; for J000487 removed C008
## [v1.82.1](https://github.com/wanderer-industries/wanderer/compare/v1.82.0...v1.82.1) (2025-10-20)
### Bug Fixes:
* Core: Fixed 'viewer' map access & characters tracking
## [v1.82.0](https://github.com/wanderer-industries/wanderer/compare/v1.81.15...v1.82.0) (2025-10-15)
### Features:
* Core: Added an ability to copy/paste selected map area between maps
### Bug Fixes:
* Map: Add ability to copy and past systems (UI part)
## [v1.81.15](https://github.com/wanderer-industries/wanderer/compare/v1.81.14...v1.81.15) (2025-10-15)
### Bug Fixes:
* Map: Fixed problem with commit - for correct restore deprecated data - change config key
## [v1.81.14](https://github.com/wanderer-industries/wanderer/compare/v1.81.13...v1.81.14) (2025-10-15)
### Bug Fixes:
* Map: Fixed problem with commit - for correct restore deprecated data
## [v1.81.13](https://github.com/wanderer-industries/wanderer/compare/v1.81.12...v1.81.13) (2025-10-15)
### Bug Fixes:
* Core: Fixed system select after tab switch
## [v1.81.12](https://github.com/wanderer-industries/wanderer/compare/v1.81.11...v1.81.12) (2025-10-15)
### Bug Fixes:
* Core: Fixed map events buffering on tab switch
## [v1.81.11](https://github.com/wanderer-industries/wanderer/compare/v1.81.10...v1.81.11) (2025-10-15)
### Bug Fixes:
* Signatures: Fixed EOL indication for un-splashed and signatures list
## [v1.81.10](https://github.com/wanderer-industries/wanderer/compare/v1.81.9...v1.81.10) (2025-10-13)
### Bug Fixes:
* Signatures: Rework for lazy signatures deletion
## [v1.81.9](https://github.com/wanderer-industries/wanderer/compare/v1.81.8...v1.81.9) (2025-10-12)
### Bug Fixes:
* Signatures: Fixed issue with wrong linked signatures deletions
## [v1.81.8](https://github.com/wanderer-industries/wanderer/compare/v1.81.7...v1.81.8) (2025-10-11)
### Bug Fixes:
* Map: Fix problem with restoring settings on widgets
## [v1.81.7](https://github.com/wanderer-industries/wanderer/compare/v1.81.6...v1.81.7) (2025-10-10)
### Bug Fixes:
* Map: Fixed problem with rendering dropdown classes in signatures
## [v1.81.6](https://github.com/wanderer-industries/wanderer/compare/v1.81.5...v1.81.6) (2025-10-10)
### Bug Fixes:
* Map: Fixed problem with a lot unnecessary loads zkb data on resize map
* Map: Added ability to see focused element
* Map: Removed unnecessary vertical scroller in Character Tracking dialog. Main always first in list of tracking characters, following next after main, another characters sorting by name
* Map: Added Search tool for systems what on the map
* Map: Added migration mechanism
* Map: Remove settings some default values if migration from very old settings system
* Map: MIGRATION: support from old store settings import
* Map: Add common migration mechanism. ATTENTION! This is a non-reversible stored map settings commit — it means we do not guarantee that settings will work if you check out back. We’ve tried to migrate old settings, but it may not work well or may NOT work at all.
* Map: Add front-end migrations for local store settings
## [v1.81.5](https://github.com/wanderer-industries/wanderer/compare/v1.81.4...v1.81.5) (2025-10-09)

View File

@@ -30,7 +30,7 @@ format f:
mix format
test t:
mix test
MIX_ENV=test mix test
coverage cover co:
mix test --cover
@@ -45,4 +45,3 @@ versions v:
@cat .tool-versions
@cat Aptfile
@echo

View File

@@ -73,7 +73,9 @@ body > div:first-of-type {
}
.maps_bg {
background-image: url('../images/maps_bg.webp');
/* OLD image */
/* background-image: url('../images/maps_bg.webp'); */
background-image: url('https://wanderer-industries.github.io/wanderer-assets/images/eve-screen-catalyst-expansion-bg.jpg');
background-size: cover;
background-position: center;
width: 100%;

View File

@@ -9,6 +9,7 @@ import { useMapperHandlers } from './useMapperHandlers';
import { MapRootContent } from '@/hooks/Mapper/components/mapRootContent/MapRootContent.tsx';
import { MapRootProvider } from '@/hooks/Mapper/mapRootProvider';
import './common-styles/main.scss';
import { ToastProvider } from '@/hooks/Mapper/ToastProvider.tsx';
const ErrorFallback = () => {
return <div className="!z-100 absolute w-screen h-screen bg-transparent"></div>;
@@ -39,13 +40,15 @@ export default function MapRoot({ hooks }) {
return (
<PrimeReactProvider>
<MapRootProvider fwdRef={providerRef} outCommand={handleCommand}>
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
<ReactFlowProvider>
<MapRootContent />
</ReactFlowProvider>
</ErrorBoundary>
</MapRootProvider>
<ToastProvider>
<MapRootProvider fwdRef={providerRef} outCommand={handleCommand}>
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
<ReactFlowProvider>
<MapRootContent />
</ReactFlowProvider>
</ErrorBoundary>
</MapRootProvider>
</ToastProvider>
</PrimeReactProvider>
);
}

View File

@@ -0,0 +1,31 @@
import React, { createContext, useContext, useRef } from 'react';
import { Toast } from 'primereact/toast';
import type { ToastMessage } from 'primereact/toast';
interface ToastContextValue {
toastRef: React.RefObject<Toast>;
show: (message: ToastMessage | ToastMessage[]) => void;
}
const ToastContext = createContext<ToastContextValue | null>(null);
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const toastRef = useRef<Toast>(null);
const show = (message: ToastMessage | ToastMessage[]) => {
toastRef.current?.show(message);
};
return (
<ToastContext.Provider value={{ toastRef, show }}>
<Toast ref={toastRef} position="top-right" />
{children}
</ToastContext.Provider>
);
};
export const useToast = (): ToastContextValue => {
const context = useContext(ToastContext);
if (!context) throw new Error('useToast must be used within a ToastProvider');
return context;
};

View File

@@ -51,20 +51,8 @@ export const Characters = ({ data }: CharactersProps) => {
['border-lime-600/70']: character.online,
},
)}
title={character.tracking_paused ? `${character.name} - Tracking Paused (click to resume)` : character.name}
title={character.name}
>
{character.tracking_paused && (
<>
<span
className={clsx(
'absolute flex flex-col p-[2px] top-[0px] left-[0px] w-[35px] h-[35px]',
'text-yellow-500 text-[9px] z-10 bg-gray-800/40',
'pi',
PrimeIcons.PAUSE,
)}
/>
</>
)}
{mainCharacterEveId === character.eve_id && (
<span
className={clsx(

View File

@@ -118,7 +118,11 @@ export const useContextMenuSystemItems = ({
});
if (isShowPingBtn) {
return <WdMenuItem icon={iconClasses}>{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}</WdMenuItem>;
return (
<WdMenuItem icon={iconClasses} className="!ml-[-2px]">
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
</WdMenuItem>
);
}
return (
@@ -126,7 +130,7 @@ export const useContextMenuSystemItems = ({
infoTitle="Locked. Ping can be set only for one system."
infoClass="pi-lock text-stone-500 mr-[12px]"
>
<WdMenuItem disabled icon={iconClasses}>
<WdMenuItem disabled icon={iconClasses} className="!ml-[-2px]">
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
</WdMenuItem>
</MenuItemWithInfo>

View File

@@ -2,25 +2,60 @@ import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { checkPermissions } from '@/hooks/Mapper/components/map/helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
export interface ContextMenuSystemMultipleProps {
contextMenuRef: RefObject<ContextMenu>;
onDeleteSystems(): void;
onCopySystems(): void;
}
export const ContextMenuSystemMultiple: React.FC<ContextMenuSystemMultipleProps> = ({
contextMenuRef,
onDeleteSystems,
onCopySystems,
}) => {
const {
data: { options, userPermissions },
} = useMapRootState();
const items: MenuItem[] = useMemo(() => {
const allowCopy = checkPermissions(userPermissions, options.allowed_copy_for);
return [
{
label: 'Delete',
icon: PrimeIcons.TRASH,
icon: clsx(PrimeIcons.TRASH, 'text-red-400'),
command: onDeleteSystems,
},
{ separator: true },
{
label: 'Copy',
icon: PrimeIcons.COPY,
command: onCopySystems,
disabled: !allowCopy,
template: () => {
if (allowCopy) {
return <WdMenuItem icon="pi pi-copy">Copy</WdMenuItem>;
}
return (
<MenuItemWithInfo
infoTitle="Action is blocked because you dont have permission to Copy."
infoClass={clsx(PrimeIcons.QUESTION_CIRCLE, 'text-stone-500 mr-[12px]')}
tooltipWrapperClassName="flex"
>
<WdMenuItem disabled icon="pi pi-copy">
Copy
</WdMenuItem>
</MenuItemWithInfo>
);
},
},
];
}, [onDeleteSystems]);
}, [onCopySystems, onDeleteSystems, options, userPermissions]);
return (
<>

View File

@@ -6,27 +6,34 @@ import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { encodeJsonToUriBase64 } from '@/hooks/Mapper/utils';
import { useToast } from '@/hooks/Mapper/ToastProvider.tsx';
export const useContextMenuSystemMultipleHandlers = () => {
const {
data: { pings },
data: { pings, connections },
} = useMapRootState();
const { show } = useToast();
const contextMenuRef = useRef<ContextMenu | null>(null);
const [systems, setSystems] = useState<Node<SolarSystemRawType>[]>();
const { deleteSystems } = useDeleteSystems();
const ping = useMemo(() => (pings.length === 1 ? pings[0] : undefined), [pings]);
const refVars = useRef({ systems, ping, connections, deleteSystems });
refVars.current = { systems, ping, connections, deleteSystems };
const handleSystemMultipleContext: NodeSelectionMouseHandler = (ev, systems_) => {
const handleSystemMultipleContext = useCallback<NodeSelectionMouseHandler>((ev, systems_) => {
setSystems(systems_);
ev.preventDefault();
ctxManager.next('ctxSysMult', contextMenuRef.current);
contextMenuRef.current?.show(ev);
};
}, []);
const onDeleteSystems = useCallback(() => {
const { systems, ping, deleteSystems } = refVars.current;
if (!systems) {
return;
}
@@ -41,11 +48,34 @@ export const useContextMenuSystemMultipleHandlers = () => {
}
deleteSystems(sysToDel);
}, [deleteSystems, systems, ping]);
}, []);
const onCopySystems = useCallback(async () => {
const { systems, connections } = refVars.current;
if (!systems) {
return;
}
const connectionToCopy = connections.filter(
c => systems.filter(s => [c.target, c.source].includes(s.id)).length == 2,
);
await navigator.clipboard.writeText(
encodeJsonToUriBase64({ systems: systems.map(x => x.data), connections: connectionToCopy }),
);
show({
severity: 'success',
summary: 'Copied to clipboard',
detail: `Successfully copied to clipboard - [${systems.length}] systems and [${connectionToCopy.length}] connections`,
life: 3000,
});
}, [show]);
return {
handleSystemMultipleContext,
contextMenuRef,
onDeleteSystems,
onCopySystems,
};
};

View File

@@ -1,10 +1,10 @@
import { useCallback, useRef } from 'react';
import { LayoutEventBlocker, TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { useCallback, useRef } from 'react';
import classes from './FastSystemActions.module.scss';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import classes from './FastSystemActions.module.scss';
export interface FastSystemActionsProps {
systemId: string;
@@ -27,7 +27,7 @@ export const FastSystemActions = ({
ref.current = { systemId, systemName, regionName, isWH };
const handleOpenZKB = useCallback(
() => window.open(`https://zkillboard.com/system/${ref.current.systemId}`, '_blank'),
() => window.open(`https://zkillboard.com/system/${ref.current.systemId}/`, '_blank'),
[],
);

View File

@@ -8,6 +8,4 @@ export type WaypointSetContextHandlerProps = {
destination: string;
};
export type WaypointSetContextHandler = (props: WaypointSetContextHandlerProps) => void;
export type NodeSelectionMouseHandler =
| ((event: React.MouseEvent<Element, MouseEvent>, nodes: Node[]) => void)
| undefined;
export type NodeSelectionMouseHandler = (event: React.MouseEvent<Element, MouseEvent>, nodes: Node[]) => void;

View File

@@ -120,7 +120,7 @@ const MapComp = ({
useMapHandlers(refn, onSelectionChange);
useUpdateNodes(nodes);
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem, onCommand });
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
const { update } = useMapState();
const { variant, gap, size, color } = useBackgroundVars(theme);

View File

@@ -2,22 +2,70 @@ import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { PasteSystemsAndConnections } from '@/hooks/Mapper/components/map/components';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { checkPermissions } from '@/hooks/Mapper/components/map/helpers';
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
export interface ContextMenuRootProps {
contextMenuRef: RefObject<ContextMenu>;
pasteSystemsAndConnections: PasteSystemsAndConnections | undefined;
onAddSystem(): void;
onPasteSystemsAnsConnections(): void;
}
export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({ contextMenuRef, onAddSystem }) => {
export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({
contextMenuRef,
onAddSystem,
onPasteSystemsAnsConnections,
pasteSystemsAndConnections,
}) => {
const {
data: { options, userPermissions },
} = useMapState();
const items: MenuItem[] = useMemo(() => {
const allowPaste = checkPermissions(userPermissions, options.allowed_paste_for);
return [
{
label: 'Add System',
icon: PrimeIcons.PLUS,
command: onAddSystem,
},
...(pasteSystemsAndConnections != null
? [
{
icon: 'pi pi-clipboard',
disabled: !allowPaste,
command: onPasteSystemsAnsConnections,
template: () => {
if (allowPaste) {
return (
<WdMenuItem icon="pi pi-clipboard">
Paste
</WdMenuItem>
);
}
return (
<MenuItemWithInfo
infoTitle="Action is blocked because you dont have permission to Paste."
infoClass={clsx(PrimeIcons.QUESTION_CIRCLE, 'text-stone-500 mr-[12px]')}
tooltipWrapperClassName="flex"
>
<WdMenuItem disabled icon="pi pi-clipboard">
Paste
</WdMenuItem>
</MenuItemWithInfo>
);
},
},
]
: []),
];
}, [onAddSystem]);
}, [userPermissions, options, onAddSystem, pasteSystemsAndConnections, onPasteSystemsAnsConnections]);
return (
<>

View File

@@ -1,36 +1,76 @@
import { useReactFlow, XYPosition } from 'reactflow';
import React, { useCallback, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { OnMapAddSystemCallback } from '@/hooks/Mapper/components/map/map.types.ts';
import { recenterSystemsByBounds } from '@/hooks/Mapper/helpers/recenterSystems.ts';
import { OutCommand, OutCommandHandler, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { decodeUriBase64ToJson } from '@/hooks/Mapper/utils';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { ContextMenu } from 'primereact/contextmenu';
import React, { useCallback, useRef, useState } from 'react';
import { useReactFlow, XYPosition } from 'reactflow';
export type PasteSystemsAndConnections = {
systems: SolarSystemRawType[];
connections: SolarSystemConnection[];
};
type UseContextMenuRootHandlers = {
onAddSystem?: OnMapAddSystemCallback;
onCommand?: OutCommandHandler;
};
export const useContextMenuRootHandlers = ({ onAddSystem }: UseContextMenuRootHandlers = {}) => {
export const useContextMenuRootHandlers = ({ onAddSystem, onCommand }: UseContextMenuRootHandlers = {}) => {
const rf = useReactFlow();
const contextMenuRef = useRef<ContextMenu | null>(null);
const [position, setPosition] = useState<XYPosition | null>(null);
const [pasteSystemsAndConnections, setPasteSystemsAndConnections] = useState<PasteSystemsAndConnections>();
const handleRootContext = (e: React.MouseEvent<HTMLDivElement>) => {
const handleRootContext = async (e: React.MouseEvent<HTMLDivElement>) => {
setPosition(rf.project({ x: e.clientX, y: e.clientY }));
e.preventDefault();
ctxManager.next('ctxRoot', contextMenuRef.current);
contextMenuRef.current?.show(e);
try {
const text = await navigator.clipboard.readText();
const result = decodeUriBase64ToJson(text);
setPasteSystemsAndConnections(result as PasteSystemsAndConnections);
} catch (err) {
setPasteSystemsAndConnections(undefined);
// do nothing
}
};
const ref = useRef({ onAddSystem, position });
ref.current = { onAddSystem, position };
const ref = useRef({ onAddSystem, position, pasteSystemsAndConnections, onCommand });
ref.current = { onAddSystem, position, pasteSystemsAndConnections, onCommand };
const onAddSystemCallback = useCallback(() => {
ref.current.onAddSystem?.({ coordinates: position });
}, [position]);
const onPasteSystemsAnsConnections = useCallback(async () => {
const { pasteSystemsAndConnections, onCommand, position } = ref.current;
if (!position || !onCommand || !pasteSystemsAndConnections) {
return;
}
const { systems } = recenterSystemsByBounds(pasteSystemsAndConnections.systems);
await onCommand({
type: OutCommand.manualPasteSystemsAndConnections,
data: {
systems: systems.map(({ position: srcPos, ...rest }) => ({
position: { x: Math.round(srcPos.x + position.x), y: Math.round(srcPos.y + position.y) },
...rest,
})),
connections: pasteSystemsAndConnections.connections,
},
});
}, []);
return {
handleRootContext,
pasteSystemsAndConnections,
contextMenuRef,
onAddSystem: onAddSystemCallback,
onPasteSystemsAnsConnections,
};
};

View File

@@ -1,6 +1,6 @@
@use "sass:color";
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@import '@/hooks/Mapper/components/map/styles/solar-system-node';
@use '@/hooks/Mapper/components/map/styles/solar-system-node' as v;
@keyframes move-stripes {
from {
@@ -26,8 +26,8 @@
background-color: var(--rf-node-bg-color, #202020) !important;
color: var(--rf-text-color, #ffffff);
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
border: 1px solid color.adjust($pastel-blue, $lightness: -10%);
box-shadow: 0 0 5px rgba(v.$dark-bg, 0.5);
border: 1px solid color.adjust(v.$pastel-blue, $lightness: -10%);
border-radius: 5px;
position: relative;
z-index: 3;
@@ -99,7 +99,7 @@
}
&.selected {
border-color: $pastel-pink;
border-color: v.$pastel-pink;
box-shadow: 0 0 10px #9a1af1c2;
}
@@ -113,11 +113,11 @@
bottom: 0;
z-index: -1;
border-color: $neon-color-1;
border-color: v.$neon-color-1;
background: repeating-linear-gradient(
45deg,
$neon-color-3 0px,
$neon-color-3 8px,
v.$neon-color-3 0px,
v.$neon-color-3 8px,
transparent 8px,
transparent 21px
);
@@ -146,7 +146,7 @@
border: 1px solid var(--eve-solar-system-status-color-lookingFor-dark15);
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
&.selected {
border-color: $pastel-pink;
border-color: v.$pastel-pink;
}
}
@@ -347,13 +347,13 @@
.Handle {
min-width: initial;
min-height: initial;
border: 1px solid $pastel-blue;
border: 1px solid v.$pastel-blue;
width: 5px;
height: 5px;
pointer-events: auto;
&.selected {
border-color: $pastel-pink;
border-color: v.$pastel-pink;
}
&.HandleTop {

View File

@@ -1,15 +1,16 @@
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import classes from './UnsplashedSignature.module.scss';
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
import { useMemo } from 'react';
import clsx from 'clsx';
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { TimeStatus } from '@/hooks/Mapper/types';
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
import clsx from 'clsx';
import { useMemo } from 'react';
import classes from './UnsplashedSignature.module.scss';
interface UnsplashedSignatureProps {
signature: SystemSignature;
@@ -35,7 +36,7 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
}, [customInfo]);
const isEOL = useMemo(() => {
return customInfo?.isEOL;
return customInfo?.time_status === TimeStatus._1h;
}, [customInfo]);
const whClassStyle = useMemo(() => {

View File

@@ -0,0 +1,5 @@
import { UserPermission, UserPermissions } from '@/hooks/Mapper/types';
export const checkPermissions = (permissions: Partial<UserPermissions>, targetPermission: UserPermission) => {
return targetPermission != null && permissions[targetPermission];
};

View File

@@ -4,3 +4,4 @@ export * from './getSystemClassStyles';
export * from './getShapeClass';
export * from './getBackgroundClass';
export * from './prepareUnsplashedChunks';
export * from './checkPermissions';

View File

@@ -14,8 +14,27 @@ export const useCommandsCharacters = () => {
const ref = useRef({ update });
ref.current = { update };
const charactersUpdated = useCallback((characters: CommandCharactersUpdated) => {
ref.current.update(() => ({ characters: characters.slice() }));
const charactersUpdated = useCallback((updatedCharacters: CommandCharactersUpdated) => {
ref.current.update(state => {
const existing = state.characters ?? [];
// Put updatedCharacters into a map keyed by ID
const updatedMap = new Map(updatedCharacters.map(c => [c.eve_id, c]));
// 1. Update existing characters when possible
const merged = existing.map(character => {
const updated = updatedMap.get(character.eve_id);
if (updated) {
updatedMap.delete(character.eve_id); // Mark as processed
return { ...character, ...updated };
}
return character;
});
// 2. Any remaining items in updatedMap are NEW characters → add them
const newCharacters = Array.from(updatedMap.values());
return { characters: [...merged, ...newCharacters] };
});
}, []);
const characterAdded = useCallback((value: CommandCharacterAdded) => {

View File

@@ -38,6 +38,8 @@ export const useMapInit = () => {
user_characters,
present_characters,
hubs,
options,
user_permissions,
}: CommandInit) => {
const { update } = ref.current;
@@ -63,6 +65,14 @@ export const useMapInit = () => {
updateData.hubs = hubs;
}
if (options) {
updateData.options = options;
}
if (options) {
updateData.userPermissions = user_permissions;
}
if (systems) {
updateData.systems = systems;
}

View File

@@ -4,10 +4,13 @@ import { DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constant
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const MapInterface = () => {
// const [items, setItems] = useState<WindowProps[]>(restoreWindowsFromLS);
const { windowsSettings, updateWidgetSettings } = useMapRootState();
const items = useMemo(() => {
if (Object.keys(windowsSettings).length === 0) {
return [];
}
return windowsSettings.windows
.map(x => {
const content = DEFAULT_WIDGETS.find(y => y.id === x.id)?.content;

View File

@@ -9,11 +9,12 @@ import {
} from '@/hooks/Mapper/components/map/constants.ts';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
import { useSystemSignaturesData } from '../../widgets/SystemSignatures/hooks/useSystemSignaturesData';
const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName;
@@ -135,6 +136,11 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
[data, setVisible],
);
const { signatures } = useSystemSignaturesData({
systemId: `${data.solar_system_source}`,
settings: LINK_SIGNTATURE_SETTINGS,
});
useEffect(() => {
if (!targetSystemDynamicInfo) {
handleHide();
@@ -152,10 +158,12 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
>
<SystemSignaturesContent
systemId={`${data.solar_system_source}`}
hideLinkedSignatures
signatures={signatures}
hasUnsupportedLanguage={false}
settings={LINK_SIGNTATURE_SETTINGS}
hideLinkedSignatures
selectable
onSelect={handleSelect}
selectable={true}
filterSignature={filterSignature}
/>
</Dialog>

View File

@@ -1,12 +1,12 @@
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
import { LayoutEventBlocker, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { SystemInfoContent } from './SystemInfoContent';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
import { PrimeIcons } from 'primereact/api';
import { useCallback, useState } from 'react';
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
import { SystemInfoContent } from './SystemInfoContent';
export const SystemInfo = () => {
const [visible, setVisible] = useState(false);
@@ -48,7 +48,7 @@ export const SystemInfo = () => {
</div>
<LayoutEventBlocker className="flex gap-1 items-center">
<a href={`https://zkillboard.com/system/${systemId}`} rel="noreferrer" target="_blank">
<a href={`https://zkillboard.com/system/${systemId}/`} rel="noreferrer" target="_blank">
<img src={ZKB_ICON} width="14" height="14" className="external-icon" />
</a>
<a href={`http://anoik.is/systems/${solarSystemName}`} rel="noreferrer" target="_blank">

View File

@@ -1,123 +1,16 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useMemo, useState } from 'react';
import { useSignatureUndo } from './hooks/useSignatureUndo';
import { useSystemSignaturesData } from './hooks/useSystemSignaturesData';
import { SystemSignaturesHeader } from './SystemSignatureHeader';
import { SystemSignaturesContent } from './SystemSignaturesContent';
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { SystemSignaturesHeader } from './SystemSignatureHeader';
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
/**
* Custom hook for managing pending signature deletions and undo countdown.
*/
function useSignatureUndo(
systemId: string | undefined,
settings: SignatureSettingsType,
outCommand: OutCommandHandler,
) {
const [countdown, setCountdown] = useState<number>(0);
const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
const [deletedSignatures, setDeletedSignatures] = useState<ExtendedSystemSignature[]>([]);
const intervalRef = useRef<number | null>(null);
const addDeleted = useCallback((signatures: ExtendedSystemSignature[]) => {
const newIds = signatures.map(sig => sig.eve_id);
setPendingIds(prev => {
const next = new Set(prev);
newIds.forEach(id => next.add(id));
return next;
});
setDeletedSignatures(prev => [...prev, ...signatures]);
}, []);
// Clear deleted signatures when system changes
useEffect(() => {
if (systemId) {
setDeletedSignatures([]);
setPendingIds(new Set());
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
}, [systemId]);
// kick off or clear countdown whenever pendingIds changes
useEffect(() => {
// clear any existing timer
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (pendingIds.size === 0) {
setCountdown(0);
setDeletedSignatures([]);
return;
}
// determine timeout from settings
const timeoutMs = getDeletionTimeoutMs(settings);
// Ensure a minimum of 1 second for immediate deletion so the UI shows
const effectiveTimeoutMs = timeoutMs === 0 ? 1000 : timeoutMs;
setCountdown(Math.ceil(effectiveTimeoutMs / 1000));
// start new interval
intervalRef.current = window.setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
setPendingIds(new Set());
setDeletedSignatures([]);
return 0;
}
return prev - 1;
});
}, 1000);
return () => {
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [pendingIds, settings]);
// undo handler
const handleUndo = useCallback(async () => {
if (!systemId || pendingIds.size === 0) return;
await outCommand({
type: OutCommand.undoDeleteSignatures,
data: { system_id: systemId, eve_ids: Array.from(pendingIds) },
});
setPendingIds(new Set());
setDeletedSignatures([]);
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [systemId, pendingIds, outCommand]);
return {
pendingIds,
countdown,
deletedSignatures,
addDeleted,
handleUndo,
};
}
export const SystemSignatures = () => {
const [visible, setVisible] = useState(false);
const [sigCount, setSigCount] = useState(0);
const [showSettings, setShowSettings] = useState(false);
const {
data: { selectedSystems },
@@ -127,31 +20,6 @@ export const SystemSignatures = () => {
const [systemId] = selectedSystems;
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
const { pendingIds, countdown, deletedSignatures, addDeleted, handleUndo } = useSignatureUndo(
systemId,
settingsSignatures,
outCommand,
);
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
if (pendingIds.size > 0 && countdown > 0) {
event.preventDefault();
event.stopPropagation();
handleUndo();
}
});
const handleCountChange = useCallback((count: number) => {
setSigCount(count);
}, []);
const handleSettingsSave = useCallback(
(newSettings: SignatureSettingsType) => {
settingsSignaturesUpdate(newSettings);
setVisible(false);
},
[settingsSignaturesUpdate],
);
const handleLazyDeleteToggle = useCallback(
(value: boolean) => {
@@ -163,7 +31,42 @@ export const SystemSignatures = () => {
[settingsSignaturesUpdate],
);
const openSettings = useCallback(() => setVisible(true), []);
const {
signatures,
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,
handleSelectAll,
handlePaste,
hasUnsupportedLanguage,
} = useSystemSignaturesData({
systemId,
settings: settingsSignatures,
onLazyDeleteChange: handleLazyDeleteToggle,
});
const sigCount = useMemo(() => signatures.length, [signatures]);
const deletedSignatures = useMemo(() => signatures.filter(s => s.deleted), [signatures]);
const { countdown, handleUndo } = useSignatureUndo(systemId, settingsSignatures, deletedSignatures, outCommand);
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
if (deletedSignatures.length > 0 && countdown > 0) {
event.preventDefault();
event.stopPropagation();
handleUndo();
}
});
const handleSettingsSave = useCallback(
(newSettings: SignatureSettingsType) => {
settingsSignaturesUpdate(newSettings);
setShowSettings(false);
},
[settingsSignaturesUpdate],
);
const openSettings = useCallback(() => setShowSettings(true), []);
return (
<Widget
@@ -171,7 +74,7 @@ export const SystemSignatures = () => {
<SystemSignaturesHeader
sigCount={sigCount}
lazyDeleteValue={settingsSignatures[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
pendingCount={pendingIds.size}
pendingCount={deletedSignatures.length}
undoCountdown={countdown}
onLazyDeleteChange={handleLazyDeleteToggle}
onUndoClick={handleUndo}
@@ -187,18 +90,21 @@ export const SystemSignatures = () => {
) : (
<SystemSignaturesContent
systemId={systemId}
signatures={signatures}
selectedSignatures={selectedSignatures}
onSelectSignatures={setSelectedSignatures}
onDeleteSelected={handleDeleteSelected}
onSelectAll={handleSelectAll}
onPaste={handlePaste}
hasUnsupportedLanguage={hasUnsupportedLanguage}
settings={settingsSignatures}
deletedSignatures={deletedSignatures}
onLazyDeleteChange={handleLazyDeleteToggle}
onCountChange={handleCountChange}
onSignatureDeleted={addDeleted}
/>
)}
{visible && (
{showSettings && (
<SystemSignatureSettingsDialog
settings={settingsSignatures}
onCancel={() => setVisible(false)}
onCancel={() => setShowSettings(false)}
onSave={handleSettingsSave}
/>
)}

View File

@@ -33,34 +33,39 @@ import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getSignatureRowClass } from '../helpers/rowStyles';
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
interface SystemSignaturesContentProps {
systemId: string;
signatures: ExtendedSystemSignature[];
selectedSignatures?: ExtendedSystemSignature[];
onSelectSignatures?: (s: ExtendedSystemSignature[]) => void;
onDeleteSelected?: () => Promise<void>;
onSelectAll?: () => void;
onPaste?: (clipboardString: string) => void;
settings: SignatureSettingsType;
hideLinkedSignatures?: boolean;
hasUnsupportedLanguage?: boolean;
selectable?: boolean;
onSelect?: (signature: SystemSignature) => void;
onLazyDeleteChange?: (value: boolean) => void;
onCountChange?: (count: number) => void;
filterSignature?: (signature: SystemSignature) => boolean;
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
deletedSignatures?: ExtendedSystemSignature[];
}
export const SystemSignaturesContent = ({
systemId,
signatures,
selectedSignatures,
onSelectSignatures,
onDeleteSelected,
onSelectAll,
onPaste,
settings,
hideLinkedSignatures,
hasUnsupportedLanguage,
selectable,
onSelect,
onLazyDeleteChange,
onCountChange,
filterSignature,
onSignatureDeleted,
deletedSignatures = [],
}: SystemSignaturesContentProps) => {
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
@@ -79,32 +84,18 @@ export const SystemSignaturesContent = ({
const { clipboardContent, setClipboardContent } = useClipboard();
const {
signatures,
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,
handleSelectAll,
handlePaste,
hasUnsupportedLanguage,
} = useSystemSignaturesData({
systemId,
settings,
onCountChange,
onLazyDeleteChange,
onSignatureDeleted,
});
const deletedSignatures = useMemo(() => signatures.filter(s => s.deleted), [signatures]);
useEffect(() => {
if (selectable) return;
if (!clipboardContent?.text) return;
handlePaste(clipboardContent.text);
onPaste?.(clipboardContent.text);
setClipboardContent(null);
}, [selectable, clipboardContent, handlePaste, setClipboardContent]);
}, [selectable, clipboardContent, onPaste, setClipboardContent]);
useHotkey(true, ['a'], handleSelectAll);
useHotkey(true, ['a'], () => onSelectAll?.());
useHotkey(false, ['Backspace', 'Delete'], (event: KeyboardEvent) => {
const targetWindow = (event.target as HTMLHtmlElement)?.closest(`[data-window-id="${SIGNATURE_WINDOW_ID}"]`);
@@ -117,7 +108,7 @@ export const SystemSignaturesContent = ({
event.stopPropagation();
// Delete key should always immediately delete, never show pending deletions
handleDeleteSelected();
onDeleteSelected?.();
});
const handleResize = useCallback(() => {
@@ -152,9 +143,9 @@ export const SystemSignaturesContent = ({
selectable
? onSelect?.(selectableSignatures[0])
: setSelectedSignatures(selectableSignatures as ExtendedSystemSignature[]);
: onSelectSignatures?.(selectableSignatures as ExtendedSystemSignature[]);
},
[onSelect, selectable, setSelectedSignatures, deletedSignatures],
[onSelect, selectable, onSelectSignatures, deletedSignatures],
);
const {
@@ -177,9 +168,6 @@ export const SystemSignaturesContent = ({
);
const filteredSignatures = useMemo<ExtendedSystemSignature[]>(() => {
// Get the set of deleted signature IDs for quick lookup
const deletedIds = new Set(deletedSignatures.map(sig => sig.eve_id));
// Common filter function
const shouldShowSignature = (sig: ExtendedSystemSignature): boolean => {
if (filterSignature && !filterSignature(sig)) {
@@ -213,24 +201,8 @@ export const SystemSignaturesContent = ({
return settings[sig.kind] as boolean;
};
// Filter active signatures, excluding any that are in the deleted list
const activeSignatures = signatures.filter(sig => {
// Skip if this signature is in the deleted list
if (deletedIds.has(sig.eve_id)) {
return false;
}
return shouldShowSignature(sig);
});
// Add deleted signatures with pending deletion flag, applying the same filters
const deletedWithPendingFlag = deletedSignatures.filter(shouldShowSignature).map(sig => ({
...sig,
pendingDeletion: true,
}));
return [...activeSignatures, ...deletedWithPendingFlag];
}, [signatures, hideLinkedSignatures, settings, filterSignature, deletedSignatures]);
return signatures.filter(sig => shouldShowSignature(sig));
}, [signatures, hideLinkedSignatures, settings, filterSignature]);
const onRowMouseEnter = useCallback((e: DataTableRowMouseEvent) => {
setHoveredSignature(e.data as SystemSignature);
@@ -253,20 +225,18 @@ export const SystemSignaturesContent = ({
return getSignatureRowClass(
rowData as ExtendedSystemSignature,
refVars.current.selectedSignatures,
refVars.current.selectedSignatures || [],
refVars.current.settings[SETTINGS_KEYS.COLOR_BY_TYPE] as boolean,
);
}, []);
const handleSortSettings = useCallback(
(e: DataTableStateEvent) =>
refVars.current.settingsSignaturesUpdate({
...refVars.current.settingsSignatures,
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
}),
[],
);
const handleSortSettings = useCallback((e: DataTableStateEvent) => {
refVars.current.settingsSignaturesUpdate({
...refVars.current.settingsSignatures,
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
});
}, []);
return (
<div ref={tableRef} className="h-full">
@@ -287,7 +257,7 @@ export const SystemSignaturesContent = ({
value={filteredSignatures}
size="small"
selectionMode="multiple"
selection={selectedSignatures}
selection={selectedSignatures || []}
metaKeySelection
onSelectionChange={handleSelectSignatures}
dataKey="eve_id"
@@ -336,6 +306,8 @@ export const SystemSignaturesContent = ({
style={{ maxWidth: nameColumnWidth }}
hidden={isCompact || isMedium}
body={renderInfoColumn}
sortable
sortField="name"
/>
{showDescriptionColumn && (
<Column

View File

@@ -1,5 +1,5 @@
import clsx from 'clsx';
import { ExtendedSystemSignature, SignatureGroup } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { getRowBackgroundColor } from './getRowBackgroundColor';
import classes from './rowStyles.module.scss';
@@ -20,7 +20,7 @@ export function getSignatureRowClass(
return clsx([...baseCls, 'bg-violet-400/40 hover:bg-violet-300/40']);
}
if (row.pendingDeletion) {
if (row.deleted) {
return clsx([...baseCls, 'bg-red-400/40 hover:bg-red-400/50']);
}

View File

@@ -1,24 +1,20 @@
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
export interface UseSystemSignaturesDataProps {
systemId: string;
settings: SignatureSettingsType;
hideLinkedSignatures?: boolean;
onCountChange?: (count: number) => void;
onPendingChange?: (
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
undo: () => void,
) => void;
onLazyDeleteChange?: (value: boolean) => void;
deletionTiming?: number;
}
export interface UseFetchingParams {
systemId: string;
settings: SignatureSettingsType;
signaturesRef: React.MutableRefObject<ExtendedSystemSignature[]>;
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
pendingDeletionMapRef: React.MutableRefObject<Record<string, ExtendedSystemSignature>>;
}
export interface UsePendingDeletionParams {

View File

@@ -1,42 +0,0 @@
import { useCallback, useRef } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { prepareUpdatePayload } from '../helpers';
import { UsePendingDeletionParams } from './types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
export function usePendingDeletions({
systemId,
setSignatures,
onPendingChange,
}: Omit<UsePendingDeletionParams, 'deletionTiming'>) {
const { outCommand } = useMapRootState();
const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({});
const processRemovedSignatures = useCallback(
async (
removed: ExtendedSystemSignature[],
added: ExtendedSystemSignature[],
updated: ExtendedSystemSignature[],
) => {
if (!removed.length) return;
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
},
[systemId, outCommand],
);
const clearPendingDeletions = useCallback(() => {
pendingDeletionMapRef.current = {};
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
}, []);
return {
pendingDeletionMapRef,
processRemovedSignatures,
clearPendingDeletions,
};
}

View File

@@ -1,21 +1,27 @@
import { useCallback } from 'react';
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { prepareUpdatePayload, getActualSigs, mergeLocalPending } from '../helpers';
import { useCallback, useMemo } from 'react';
import { getDeletionTimeoutMs } from '../constants';
import { getActualSigs, prepareUpdatePayload } from '../helpers';
import { UseFetchingParams } from './types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const useSignatureFetching = ({
systemId,
signaturesRef,
setSignatures,
pendingDeletionMapRef,
}: UseFetchingParams) => {
export const useSignatureFetching = ({ systemId, settings, signaturesRef, setSignatures }: UseFetchingParams) => {
const {
data: { characters },
outCommand,
} = useMapRootState();
const deleteTimeout = useMemo(() => {
const lazyDelete = settings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean;
if (!lazyDelete) {
return 0;
}
return getDeletionTimeoutMs(settings);
}, [settings]);
const handleGetSignatures = useCallback(async () => {
if (!systemId) {
setSignatures([]);
@@ -32,24 +38,23 @@ export const useSignatureFetching = ({
character_name: characters.find(c => c.eve_id === s.character_eve_id)?.name,
})) as ExtendedSystemSignature[];
setSignatures(() => mergeLocalPending(pendingDeletionMapRef, extended));
setSignatures(() => extended);
}, [characters, systemId, outCommand]);
const handleUpdateSignatures = useCallback(
async (newList: ExtendedSystemSignature[], updateOnly: boolean, skipUpdateUntouched?: boolean) => {
const { added, updated, removed } = getActualSigs(
signaturesRef.current,
newList,
updateOnly,
skipUpdateUntouched,
);
const actualSigs = getActualSigs(signaturesRef.current, newList, updateOnly, skipUpdateUntouched);
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
const { added, updated, removed } = actualSigs;
if (updated.length !== 0 || added.length !== 0 || removed.length !== 0) {
await outCommand({
type: OutCommand.updateSignatures,
data: { ...prepareUpdatePayload(systemId, added, updated, removed), deleteTimeout },
});
}
},
[systemId, outCommand, signaturesRef],
[systemId, deleteTimeout, outCommand, signaturesRef],
);
return {

View File

@@ -0,0 +1,89 @@
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
import { ExtendedSystemSignature, OutCommandHandler } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useEffect, useRef, useState } from 'react';
import { getDeletionTimeoutMs } from '../constants';
/**
* Custom hook for managing pending signature deletions and undo countdown.
*/
export function useSignatureUndo(
systemId: string | undefined,
settings: SignatureSettingsType,
deletedSignatures: ExtendedSystemSignature[],
outCommand: OutCommandHandler,
) {
const [countdown, setCountdown] = useState<number>(0);
const intervalRef = useRef<number | null>(null);
// Clear deleted signatures when system changes
useEffect(() => {
if (systemId) {
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
}, [systemId]);
// kick off or clear countdown whenever pendingIds changes
useEffect(() => {
// clear any existing timer
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (deletedSignatures.length === 0) {
setCountdown(0);
return;
}
// determine timeout from settings
const timeoutMs = getDeletionTimeoutMs(settings);
// Ensure a minimum of 1 second for immediate deletion so the UI shows
const effectiveTimeoutMs = timeoutMs === 0 ? 1000 : timeoutMs;
setCountdown(Math.ceil(effectiveTimeoutMs / 1000));
// start new interval
intervalRef.current = window.setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
return 0;
}
return prev - 1;
});
}, 1000);
return () => {
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [deletedSignatures, settings]);
// undo handler
const handleUndo = useCallback(async () => {
if (!systemId || deletedSignatures.length === 0) return;
await outCommand({
type: OutCommand.undoDeleteSignatures,
data: { system_id: systemId, eve_ids: deletedSignatures.map(s => s.eve_id) },
});
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [systemId, deletedSignatures, outCommand]);
return {
countdown,
handleUndo,
};
}

View File

@@ -1,44 +1,29 @@
import { useMapEventListener } from '@/hooks/Mapper/events';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import { Commands, ExtendedSystemSignature, SignatureKind } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useEffect, useState } from 'react';
import useRefState from 'react-usestateref';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getActualSigs } from '../helpers';
import { UseSystemSignaturesDataProps } from './types';
import { usePendingDeletions } from './usePendingDeletions';
import { useSignatureFetching } from './useSignatureFetching';
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures.ts';
import { UseSystemSignaturesDataProps } from './types';
import { useSignatureFetching } from './useSignatureFetching';
export const useSystemSignaturesData = ({
systemId,
settings,
onCountChange,
onPendingChange,
onLazyDeleteChange,
onSignatureDeleted,
}: Omit<UseSystemSignaturesDataProps, 'deletionTiming'> & {
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
}) => {
const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
const [hasUnsupportedLanguage, setHasUnsupportedLanguage] = useState<boolean>(false);
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
systemId,
setSignatures,
onPendingChange,
});
const { handleGetSignatures, handleUpdateSignatures } = useSignatureFetching({
systemId,
settings,
signaturesRef,
setSignatures,
pendingDeletionMapRef,
});
const handlePaste = useCallback(
@@ -67,40 +52,14 @@ export const useSystemSignaturesData = ({
setHasUnsupportedLanguage(false);
}
const currentNonPending = lazyDeleteValue
? signaturesRef.current.filter(sig => !sig.pendingDeletion)
: signaturesRef.current.filter(sig => !sig.pendingDeletion || !sig.pendingAddition);
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, false);
if (removed.length > 0) {
await processRemovedSignatures(removed, added, updated);
// Show pending deletions if lazy deletion is enabled
// The deletion timing controls how long the countdown lasts, not whether lazy delete is active
if (onSignatureDeleted && lazyDeleteValue) {
onSignatureDeleted(removed);
}
}
if (updated.length !== 0 || added.length !== 0) {
await outCommand({
type: OutCommand.updateSignatures,
data: {
system_id: systemId,
added,
updated,
removed: [],
},
});
}
await handleUpdateSignatures(incomingSignatures, !lazyDeleteValue, false);
const keepLazy = settings[SETTINGS_KEYS.KEEP_LAZY_DELETE] as boolean;
if (lazyDeleteValue && !keepLazy) {
onLazyDeleteChange?.(false);
}
},
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange, onSignatureDeleted],
[settings, handleUpdateSignatures, onLazyDeleteChange],
);
const handleDeleteSelected = useCallback(async () => {
@@ -109,23 +68,15 @@ export const useSystemSignaturesData = ({
const selectedIds = selectedSignatures.map(s => s.eve_id);
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
// IMPORTANT: Send deletion to server BEFORE updating local state
// Otherwise signaturesRef.current will be updated and getActualSigs won't detect removals
await handleUpdateSignatures(finalList, false, true);
// Update local state after server call
setSignatures(finalList);
setSelectedSignatures([]);
}, [handleUpdateSignatures, selectedSignatures, signatures, setSignatures]);
await handleUpdateSignatures(finalList, false, true);
}, [handleUpdateSignatures, selectedSignatures, signatures]);
const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures);
}, [signatures]);
const undoPending = useCallback(() => {
clearPendingDeletions();
}, [clearPendingDeletions]);
useMapEventListener(event => {
if (event.name === Commands.signaturesUpdated && String(event.data) === String(systemId)) {
handleGetSignatures();
@@ -136,18 +87,13 @@ export const useSystemSignaturesData = ({
useEffect(() => {
if (!systemId) {
setSignatures([]);
undoPending();
return;
}
handleGetSignatures();
}, [systemId]);
useEffect(() => {
onCountChange?.(signatures.length);
}, [signatures]);
return {
signatures: signatures.filter(sig => !sig.deleted),
signatures,
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,

View File

@@ -1,14 +1,14 @@
import { PrimeIcons } from 'primereact/api';
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { SystemViewStandalone, TooltipPosition, WHClassView } from '@/hooks/Mapper/components/ui-kit';
import { SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { PrimeIcons } from 'primereact/api';
import { renderK162Type } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import clsx from 'clsx';
import { renderName } from './renderName.tsx';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
import clsx from 'clsx';
import { renderName } from './renderName.tsx';
export const renderInfoColumn = (row: SystemSignature) => {
if (!row.group || row.group === SignatureGroup.Wormhole) {
@@ -18,7 +18,9 @@ export const renderInfoColumn = (row: SystemSignature) => {
return (
<div className="flex justify-start items-center gap-[4px]">
{customInfo.isEOL && (
{row.temporary_name && <span className={clsx('text-[12px]')}>{row.temporary_name}</span>}
{customInfo.time_status === TimeStatus._1h && (
<WdTooltipWrapper offset={5} position={TooltipPosition.top} content="Signature marked as EOL">
<div className="pi pi-clock text-fuchsia-400 text-[11px] mr-[2px]"></div>
</WdTooltipWrapper>

View File

@@ -30,9 +30,6 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
const { outCommand } = useMapRootState();
const [prevQuery, setPrevQuery] = useState('');
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
useEffect(() => {
if (structure) {
setEditData(structure);
@@ -46,34 +43,24 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
// Searching corporation owners via auto-complete
const searchOwners = useCallback(
async (e: { query: string }) => {
const newQuery = e.query.trim();
if (!newQuery) {
const query = e.query.trim();
if (!query) {
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 },
data: { search: query },
});
setOwnerSuggestions(results);
setPrevQuery(newQuery);
setPrevResults(results);
} catch (err) {
console.error('Failed to fetch owners:', err);
setOwnerSuggestions([]);
}
},
[prevQuery, prevResults, outCommand],
[outCommand],
);
const handleChange = (field: keyof StructureItem, val: string | Date) => {
@@ -122,7 +109,6 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
// fetch corporation ticker if we have an ownerId
if (editData.ownerId) {
try {
// TODO fix it
const { ticker } = await outCommand({
type: OutCommand.getCorporationTicker,
data: { corp_id: editData.ownerId },

View File

@@ -10,9 +10,14 @@ import { useCallback } from 'react';
import { TooltipPosition, WdButton, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const CommonSettings = () => {
const { renderSettingItem } = useMapSettings();
const {
storedSettings: { resetSettings },
} = useMapRootState();
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
const renderSettingsList = useCallback(
@@ -22,7 +27,7 @@ export const CommonSettings = () => {
[renderSettingItem],
);
const handleResetSettings = () => {};
const handleResetSettings = useCallback(() => resetSettings(), [resetSettings]);
return (
<div className="flex flex-col h-full gap-1">

View File

@@ -64,7 +64,7 @@ export const ImportExport = () => {
// INFO: WE NOT SUPPORT MIGRATIONS FOR OLD FILES AND Clipboard
const parsed = parseMapUserSettings(text);
if (applySettings(applyMigrations(parsed))) {
if (applySettings(applyMigrations(parsed) || createDefaultStoredSettings())) {
toast.current?.show({
severity: 'success',
summary: 'Import',

View File

@@ -35,7 +35,7 @@ export const ServerSettings = () => {
try {
//INFO: INSTEAD CHECK WE WILL TRY TO APPLY MIGRATION
applySettings(applyMigrations(JSON.parse(res.default_settings)));
applySettings(applyMigrations(JSON.parse(res.default_settings)) || createDefaultStoredSettings());
callToastSuccess(toast.current, 'Settings synchronized successfully');
} catch (error) {
applySettings(createDefaultStoredSettings());

View File

@@ -1,15 +1,15 @@
import { Dialog } from 'primereact/dialog';
import { useCallback, useEffect } from 'react';
import { OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import {
SignatureGroupContent,
SignatureGroupSelect,
} from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components';
import { InputText } from 'primereact/inputtext';
import { SystemsSettingsProvider } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/Provider.tsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { useCallback, useEffect } from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
type SystemSignaturePrepared = Omit<SystemSignature, 'linked_system'> & {
linked_system: string;
@@ -119,6 +119,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
added: [],
updated: [out],
removed: [],
deleteTimeout: 0,
},
});

View File

@@ -1,6 +1,6 @@
@use "sass:color";
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@import '@/hooks/Mapper/components/map/styles/solar-system-node';
@use '@/hooks/Mapper/components/map/styles/solar-system-node' as v;
:root {
--rf-has-user-characters: #ffc75d;
@@ -108,7 +108,7 @@
}
&.selected {
border-color: $pastel-pink;
border-color: v.$pastel-pink;
box-shadow: 0 0 10px #9a1af1c2;
}
@@ -122,11 +122,11 @@
bottom: 0;
z-index: -1;
border-color: $neon-color-1;
border-color: v.$neon-color-1;
background: repeating-linear-gradient(
45deg,
$neon-color-3 0px,
$neon-color-3 8px,
v.$neon-color-3 0px,
v.$neon-color-3 8px,
transparent 8px,
transparent 21px
);
@@ -152,7 +152,7 @@
&.eve-system-status-lookingFor {
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
&.selected {
border-color: $pastel-pink;
border-color: v.$pastel-pink;
}
}

View File

@@ -1,36 +1,36 @@
import { Map, MAP_ROOT_ID } from '@/hooks/Mapper/components/map/Map.tsx';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CommandSelectSystems, OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OnMapAddSystemCallback, OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import isEqual from 'lodash.isequal';
import { ContextMenuSystem, useContextMenuSystemHandlers } from '@/hooks/Mapper/components/contexts';
import { Map, MAP_ROOT_ID } from '@/hooks/Mapper/components/map/Map.tsx';
import { OnMapAddSystemCallback, OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import {
SystemCustomLabelDialog,
SystemLinkSignatureDialog,
SystemSettingsDialog,
} from '@/hooks/Mapper/components/mapInterface/components';
import { Connections } from '@/hooks/Mapper/components/mapRootContent/components/Connections';
import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandSelectSystems, OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import isEqual from 'lodash.isequal';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Node, useReactFlow, Viewport, XYPosition } from 'reactflow';
import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple';
import { useCommandsSystems } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { emitMapEvent, useMapEventListener } from '@/hooks/Mapper/events';
import { useCommandsSystems } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
import { useCommonMapEventProcessor } from '@/hooks/Mapper/components/mapWrapper/hooks/useCommonMapEventProcessor.ts';
import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import { useHotkey } from '../../hooks/useHotkey';
import { PingType } from '@/hooks/Mapper/types/ping.ts';
import { SystemPingDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemPingDialog';
import { MiniMapPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { useCommonMapEventProcessor } from '@/hooks/Mapper/components/mapWrapper/hooks/useCommonMapEventProcessor.ts';
import { MINIMAP_PLACEMENT_MAP } from '@/hooks/Mapper/constants.ts';
import { MiniMapPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { PingType } from '@/hooks/Mapper/types/ping.ts';
import type { PanelPosition } from '@reactflow/core';
import { useHotkey } from '../../hooks/useHotkey';
import { MINI_MAP_PLACEMENT_OFFSETS } from './constants.ts';
// TODO: INFO - this component needs for abstract work with Map instance
@@ -106,7 +106,7 @@ export const MapWrapper = () => {
runCommand({
name: Commands.selectSystems,
data: { systems: selectedSystems } as CommandSelectSystems,
data: { systems: selectedSystems, delay: 200 } as CommandSelectSystems,
});
}
});

View File

@@ -4,8 +4,17 @@ import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrap
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
import clsx from 'clsx';
type MenuItemWithInfoProps = { infoTitle: ReactNode; infoClass?: string } & WithChildren;
export const MenuItemWithInfo = ({ children, infoClass, infoTitle }: MenuItemWithInfoProps) => {
type MenuItemWithInfoProps = {
infoTitle: ReactNode;
infoClass?: string;
tooltipWrapperClassName?: string;
} & WithChildren;
export const MenuItemWithInfo = ({
children,
infoClass,
infoTitle,
tooltipWrapperClassName,
}: MenuItemWithInfoProps) => {
return (
<div className="flex justify-between w-full h-full items-center">
{children}
@@ -13,6 +22,7 @@ export const MenuItemWithInfo = ({ children, infoClass, infoTitle }: MenuItemWit
content={infoTitle}
position={TooltipPosition.top}
className="!opacity-100 !pointer-events-auto"
wrapperClassName={tooltipWrapperClassName}
>
<div className={clsx('pi text-orange-400', infoClass)} />
</WdTooltipWrapper>

View File

@@ -45,40 +45,42 @@ export const WHClassView = ({
const whClass = useMemo(() => WORMHOLES_ADDITIONAL_INFO[whData.dest], [whData.dest]);
const whClassStyle = WORMHOLE_CLASS_STYLES[whClass?.wormholeClassID] ?? '';
return (
<div className={clsx(classes.WHClassViewRoot, className)}>
{!hideTooltip && (
<WdTooltipWrapper
position={TooltipPosition.bottom}
content={
<div className="flex gap-3">
<div className="flex flex-col gap-1">
<InfoDrawer title="Total mass">{prepareMass(whData.total_mass)}</InfoDrawer>
<InfoDrawer title="Jump mass">{prepareMass(whData.max_mass_per_jump)}</InfoDrawer>
</div>
<div className="flex flex-col gap-1">
<InfoDrawer title="Lifetime">{whData.lifetime}h</InfoDrawer>
<InfoDrawer title="Mass regen">{prepareMass(whData.mass_regen)}</InfoDrawer>
</div>
</div>
}
>
<div
className={clsx(
classes.WHClassViewContent,
{ [classes.NoOffset]: noOffset },
'wh-name select-none cursor-help',
)}
>
{!hideWhClassName && <span className={clsx({ [whClassStyle]: highlightName })}>{whClassName}</span>}
{!hideWhClass && whClass && (
<span className={clsx(classes.WHClassName, whClassStyle, classNameWh)}>
{useShortTitle ? whClass.shortTitle : whClass.shortName}
</span>
)}
</div>
</WdTooltipWrapper>
const content = (
<div
className={clsx(classes.WHClassViewContent, { [classes.NoOffset]: noOffset }, 'wh-name select-none cursor-help')}
>
{!hideWhClassName && <span className={clsx({ [whClassStyle]: highlightName })}>{whClassName}</span>}
{!hideWhClass && whClass && (
<span className={clsx(classes.WHClassName, whClassStyle, classNameWh)}>
{useShortTitle ? whClass.shortTitle : whClass.shortName}
</span>
)}
</div>
);
if (hideTooltip) {
return <div className={clsx(classes.WHClassViewRoot, className)}>{content}</div>;
}
return (
<div className={clsx(classes.WHClassViewRoot, className)}>
<WdTooltipWrapper
position={TooltipPosition.bottom}
content={
<div className="flex gap-3">
<div className="flex flex-col gap-1">
<InfoDrawer title="Total mass">{prepareMass(whData.total_mass)}</InfoDrawer>
<InfoDrawer title="Jump mass">{prepareMass(whData.max_mass_per_jump)}</InfoDrawer>
</div>
<div className="flex flex-col gap-1">
<InfoDrawer title="Lifetime">{whData.lifetime}h</InfoDrawer>
<InfoDrawer title="Mass regen">{prepareMass(whData.mass_regen)}</InfoDrawer>
</div>
</div>
}
>
{content}
</WdTooltipWrapper>
</div>
);
};

View File

@@ -1,13 +1,18 @@
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
import clsx from 'clsx';
type WdMenuItemProps = { icon?: string; disabled?: boolean } & WithChildren;
export const WdMenuItem = ({ children, icon, disabled }: WdMenuItemProps) => {
type WdMenuItemProps = { icon?: string; disabled?: boolean } & WithChildren & WithClassName;
export const WdMenuItem = ({ children, icon, disabled, className }: WdMenuItemProps) => {
return (
<a
className={clsx('flex gap-[6px] w-full h-full items-center px-[12px] !py-0 ml-[-2px]', 'p-menuitem-link', {
'p-disabled': disabled,
})}
className={clsx(
'flex gap-[6px] w-full h-full items-center px-[12px] !py-0',
'p-menuitem-link',
{
'p-disabled': disabled,
},
className,
)}
>
{icon && <div className={clsx('min-w-[20px]', icon)}></div>}
<div className="w-full">{children}</div>

View File

@@ -10,6 +10,7 @@ export type WdTooltipWrapperProps = {
interactive?: boolean;
smallPaddings?: boolean;
tooltipClassName?: string;
wrapperClassName?: string;
} & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> &
Omit<TooltipProps, 'content'>;
@@ -26,6 +27,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
smallPaddings,
size,
tooltipClassName,
wrapperClassName,
...props
},
forwardedRef,
@@ -36,7 +38,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
return (
<div className={clsx(classes.WdTooltipWrapperRoot, className)} {...props}>
{targetSelector ? <>{children}</> : <div className={autoClass}>{children}</div>}
{targetSelector ? <>{children}</> : <div className={clsx(autoClass, wrapperClassName)}>{children}</div>}
<WdTooltip
ref={forwardedRef}

View File

@@ -1,12 +1,5 @@
import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
export enum SESSION_KEY {
viewPort = 'viewPort',
windows = 'windows',
windowsVisible = 'windowsVisible',
routes = 'routes',
}
export const SYSTEM_FOCUSED_LIFETIME = 10000;
export const GRADIENT_MENU_ACTIVE_CLASSES = 'bg-gradient-to-br from-transparent/10 to-fuchsia-300/10';

View File

@@ -3,3 +3,4 @@ export * from './parseSignatures';
export * from './getSystemById';
export * from './getEveImageUrl';
export * from './toastHelpers';
export * from './recenterSystems';

View File

@@ -0,0 +1,39 @@
import { XYPosition } from 'reactflow';
export type WithPosition<T = unknown> = T & { position: XYPosition };
export const computeBoundsCenter = (items: Array<WithPosition>): XYPosition => {
if (items.length === 0) return { x: 0, y: 0 };
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
for (const { position } of items) {
if (position.x < minX) minX = position.x;
if (position.x > maxX) maxX = position.x;
if (position.y < minY) minY = position.y;
if (position.y > maxY) maxY = position.y;
}
return {
x: minX + (maxX - minX) / 2,
y: minY + (maxY - minY) / 2,
};
};
/** Смещает все точки так, чтобы центр области стал (0,0) */
export const recenterSystemsByBounds = <T extends WithPosition>(items: T[]): { center: XYPosition; systems: T[] } => {
const center = computeBoundsCenter(items);
const systems = items.map(it => ({
...it,
position: {
x: it.position.x - center.x,
y: it.position.y - center.y,
},
}));
return { center, systems };
};

View File

@@ -28,14 +28,17 @@ export const useEventBuffer = <T>(handler: UseEventBufferHandler<T>) => {
eventTickRef.current = eventTick;
// @ts-ignore
const handleEvent = useCallback(event => {
if (!eventTickRef.current) {
return;
}
const handleEvent = useCallback(
event => {
if (!eventTickRef.current) {
return;
}
eventsBufferRef.current.push(event);
eventTickRef.current();
}, []);
eventsBufferRef.current.push(event);
eventTickRef.current();
},
[eventTickRef.current],
);
return { handleEvent };
};

View File

@@ -6,9 +6,11 @@ import {
MapUnionTypes,
OutCommandHandler,
SolarSystemConnection,
StringBoolean,
TrackingCharacter,
UseCharactersCacheData,
UseCommentsData,
UserPermission,
} from '@/hooks/Mapper/types';
import { useCharactersCache, useComments, useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
@@ -80,7 +82,16 @@ const INITIAL_DATA: MapRootData = {
selectedSystems: [],
selectedConnections: [],
userPermissions: {},
options: {},
options: {
allowed_copy_for: UserPermission.VIEW_SYSTEM,
allowed_paste_for: UserPermission.VIEW_SYSTEM,
layout: '',
restrict_offline_showing: 'false',
show_linked_signature_id: 'false',
show_linked_signature_id_temp_name: 'false',
show_temp_system_name: 'false',
store_custom_labels: 'false',
},
isSubscriptionActive: false,
linkSignatureToSystem: null,
mainCharacterEveId: null,
@@ -135,7 +146,7 @@ export interface MapRootContextProps {
hasOldSettings: boolean;
getSettingsForExport(): string | undefined;
applySettings(settings: MapUserSettings): boolean;
resetSettings(settings: MapUserSettings): void;
resetSettings(): void;
checkOldSettings(): void;
};
}

View File

@@ -19,7 +19,7 @@ export const createWidgetSettings = <T>(settings: T) => {
export const createDefaultStoredSettings = (): MapUserSettings => {
return {
version: STORED_SETTINGS_VERSION,
migratedFromOld: true,
migratedFromOld: false,
killsWidget: createWidgetSettings(DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createWidgetSettings(DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createWidgetSettings(getDefaultWidgetProps()),

View File

@@ -1,5 +1,4 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef } from 'react';
import {
CommandCharacterAdded,
CommandCharacterRemoved,
@@ -7,6 +6,7 @@ import {
CommandCharacterUpdated,
CommandPresentCharacters,
} from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
export const useCommandsCharacters = () => {
const { update } = useMapRootState();
@@ -14,8 +14,27 @@ export const useCommandsCharacters = () => {
const ref = useRef({ update });
ref.current = { update };
const charactersUpdated = useCallback((characters: CommandCharactersUpdated) => {
ref.current.update(() => ({ characters: characters.slice() }));
const charactersUpdated = useCallback((updatedCharacters: CommandCharactersUpdated) => {
ref.current.update(state => {
const existing = state.characters ?? [];
// Put updatedCharacters into a map keyed by ID
const updatedMap = new Map(updatedCharacters.map(c => [c.eve_id, c]));
// 1. Update existing characters when possible
const merged = existing.map(character => {
const updated = updatedMap.get(character.eve_id);
if (updated) {
updatedMap.delete(character.eve_id); // Mark as processed
return { ...character, ...updated };
}
return character;
});
// 2. Any remaining items in updatedMap are NEW characters → add them
const newCharacters = Array.from(updatedMap.values());
return { characters: [...merged, ...newCharacters] };
});
}, []);
const characterAdded = useCallback((value: CommandCharacterAdded) => {

View File

@@ -42,7 +42,7 @@ export const useActualizeRemoteMapSettings = ({
}
try {
applySettings(applyMigrations(JSON.parse(res.default_settings)));
applySettings(applyMigrations(JSON.parse(res.default_settings) || createDefaultStoredSettings()));
} catch (error) {
applySettings(createDefaultStoredSettings());
}

View File

@@ -115,10 +115,15 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
}
try {
// here we try to restore settings
let oldMapData;
if (!currentMapUserSettings.migratedFromOld) {
const allData = extractData(LS_KEY_LEGASY);
oldMapData = allData?.[map_slug];
}
// INFO: after migrations migratedFromOld always will be true
const migratedResult = applyMigrations(
!currentMapUserSettings.migratedFromOld ? extractData(LS_KEY_LEGASY) : currentMapUserSettings,
);
const migratedResult = applyMigrations(oldMapData ? oldMapData : currentMapUserSettings);
if (!migratedResult) {
setIsReady(true);
@@ -143,10 +148,6 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
setHasOldSettings(!!(widgetsOld || interfaceSettings || widgetRoutes || widgetLocal || widgetKills || onTheMapOld));
}, []);
useEffect(() => {
checkOldSettings();
}, [checkOldSettings]);
const getSettingsForExport = useCallback(() => {
const { map_slug } = ref.current;
@@ -161,6 +162,24 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
applySettings(createDefaultStoredSettings());
}, [applySettings]);
useEffect(() => {
checkOldSettings();
}, [checkOldSettings]);
// IN Case if in runtime someone clear settings
useEffect(() => {
if (Object.keys(windowsSettings).length !== 0) {
return;
}
if (!isReady) {
return;
}
resetSettings();
location.reload();
}, [isReady, resetSettings, windowsSettings]);
return {
isReady,
hasOldSettings,

View File

@@ -26,7 +26,7 @@ export const applyMigrations = (mapSettings: any) => {
return { ...currentMapSettings, version: STORED_SETTINGS_VERSION, migratedFromOld: true };
}
return;
return currentMapSettings;
}
const cmVersion = currentMapSettings.version || 0;

View File

@@ -1,4 +1,4 @@
export const STORED_SETTINGS_VERSION = 2;
export const LS_KEY_LEGASY = 'map-user-settings';
export const LS_KEY = 'map-user-settings-v2';
export const LS_KEY = 'map-user-settings-v3';

View File

@@ -33,7 +33,6 @@ export type CharacterTypeRaw = {
corporation_id: number;
corporation_name: string;
corporation_ticker: string;
tracking_paused: boolean;
};
export interface TrackingCharacter {

View File

@@ -9,3 +9,4 @@ export * from './connectionPassages';
export * from './permissions';
export * from './comment';
export * from './ping';
export * from './options';

View File

@@ -1,4 +1,4 @@
import { CommentType, PingData, SystemSignature, UserPermissions } from '@/hooks/Mapper/types';
import { CommentType, MapOptions, PingData, SystemSignature, UserPermissions } from '@/hooks/Mapper/types';
import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Mapper/types/character.ts';
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
@@ -94,7 +94,7 @@ export type CommandInit = {
hubs: string[];
user_hubs: string[];
routes: RoutesList;
options: Record<string, string | boolean>;
options: MapOptions;
reset?: boolean;
is_subscription_active?: boolean;
main_character_eve_id?: string | null;
@@ -247,6 +247,7 @@ export enum OutCommand {
deleteSystems = 'delete_systems',
manualAddSystem = 'manual_add_system',
manualAddConnection = 'manual_add_connection',
manualPasteSystemsAndConnections = 'manual_paste_systems_and_connections',
manualDeleteConnection = 'manual_delete_connection',
setAutopilotWaypoint = 'set_autopilot_waypoint',
addSystem = 'add_system',

View File

@@ -4,7 +4,7 @@ import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts';
import { SolarSystemRawType } from '@/hooks/Mapper/types/system.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { PingData, UserPermissions } from '@/hooks/Mapper/types';
import { MapOptions, PingData, UserPermissions } from '@/hooks/Mapper/types';
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
export type MapUnionTypes = {
@@ -23,7 +23,7 @@ export type MapUnionTypes = {
kills: Record<number, number>;
connections: SolarSystemConnection[];
userPermissions: Partial<UserPermissions>;
options: Record<string, string | boolean>;
options: MapOptions;
isSubscriptionActive: boolean;
mainCharacterEveId: string | null;

View File

@@ -0,0 +1,14 @@
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
export type StringBoolean = 'true' | 'false';
export type MapOptions = {
allowed_copy_for: UserPermission;
allowed_paste_for: UserPermission;
layout: string;
restrict_offline_showing: StringBoolean;
show_linked_signature_id: StringBoolean;
show_linked_signature_id_temp_name: StringBoolean;
show_temp_system_name: StringBoolean;
store_custom_labels: StringBoolean;
};

View File

@@ -29,7 +29,7 @@ export type GroupType = {
export type SignatureCustomInfo = {
k162Type?: string;
isEOL?: boolean;
time_status?: number;
isCrit?: boolean;
};

View File

@@ -1,5 +1,5 @@
import { useEventBuffer } from '@/hooks/Mapper/hooks';
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
import debounce from 'lodash.debounce';
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
import { RefObject, useCallback, useEffect, useRef } from 'react';
@@ -16,23 +16,6 @@ export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRe
const visibleRef = useRef(visible);
visibleRef.current = visible;
// @ts-ignore
const handleBufferedEvent = useCallback(({ type, body }) => {
if (!visibleRef.current) {
return;
}
handlerRefs.forEach(ref => {
if (!ref.current) {
return;
}
ref.current?.command(type, body);
});
}, []);
const { handleEvent: handleMapEvent } = useEventBuffer<any>(handleBufferedEvent);
// TODO - do not delete THIS code it needs for debug
// const [record, setRecord] = useLocalStorageState<boolean>('record', {
// defaultValue: false,
@@ -73,6 +56,52 @@ export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRe
[hooksRef.current],
);
// @ts-ignore
const eventsBufferRef = useRef<{ type; body }[]>([]);
const eventTick = useCallback(
debounce(() => {
if (eventsBufferRef.current.length === 0) {
return;
}
const { type, body } = eventsBufferRef.current.shift()!;
handlerRefs.forEach(ref => {
if (!ref.current) {
return;
}
ref.current?.command(type, body);
});
// TODO - do not delete THIS code it needs for debug
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
if (eventsBufferRef.current.length > 0) {
eventTick();
}
}, 10),
[],
);
const eventTickRef = useRef(eventTick);
eventTickRef.current = eventTick;
// @ts-ignore
const handleMapEvent = useCallback(({ type, body }) => {
// TODO - do not delete THIS code it needs for debug
// const currentTime = +new Date();
// const timeDiff = currentTime - prevEventTime;
// prevEventTime = currentTime;
// console.log('JOipP', `IN [${inIndex++}] [${timeDiff}] ${getFormattedTime()}`, { type, body });
if (!eventTickRef.current || !visibleRef.current) {
return;
}
eventsBufferRef.current.push({ type, body });
eventTickRef.current();
}, []);
useEffect(() => {
if (!visible && !wasHiddenOnce.current) {
wasHiddenOnce.current = true;

View File

@@ -3,3 +3,4 @@ export * from './getQueryVariable';
export * from './loadTextFile';
export * from './saveToFile';
export * from './omit';
export * from './jsonToUriBase64';

View File

@@ -0,0 +1,26 @@
export const encodeJsonToUriBase64 = (value: unknown): string => {
const json = JSON.stringify(value);
const uriEncoded = encodeURIComponent(json);
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
return window.btoa(uriEncoded);
}
// Node.js
// @ts-ignore
return Buffer.from(uriEncoded, 'utf8').toString('base64');
};
export const decodeUriBase64ToJson = <T = unknown>(base64: string): T => {
let uriEncoded: string;
if (typeof window !== 'undefined' && typeof window.atob === 'function') {
uriEncoded = window.atob(base64);
} else {
// Node.js
// @ts-ignore
uriEncoded = Buffer.from(base64, 'base64').toString('utf8');
}
const json = decodeURIComponent(uriEncoded);
return JSON.parse(json) as T;
};

View File

@@ -12,11 +12,11 @@ const animateBg = function (bgCanvas) {
*/
const randomInRange = (max, min) => Math.floor(Math.random() * (max - min + 1)) + min;
const BASE_SIZE = 1;
const VELOCITY_INC = 1.01;
const VELOCITY_INC = 1.002;
const VELOCITY_INIT_INC = 0.525;
const JUMP_VELOCITY_INC = 0.55;
const JUMP_SIZE_INC = 1.15;
const SIZE_INC = 1.01;
const SIZE_INC = 1.002;
const RAD = Math.PI / 180;
const WARP_COLORS = [
[197, 239, 247],

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -25,13 +25,9 @@ config :wanderer_app,
ecto_repos: [WandererApp.Repo],
ash_domains: [WandererApp.Api],
generators: [timestamp_type: :utc_datetime],
ddrt: DDRT,
ddrt: WandererApp.Map.CacheRTree,
logger: Logger,
pubsub_client: Phoenix.PubSub,
wanderer_kills_base_url:
System.get_env("WANDERER_KILLS_BASE_URL", "ws://host.docker.internal:4004"),
wanderer_kills_service_enabled:
System.get_env("WANDERER_KILLS_SERVICE_ENABLED", "false") == "true"
pubsub_client: Phoenix.PubSub
config :wanderer_app, WandererAppWeb.Endpoint,
adapter: Bandit.PhoenixAdapter,

View File

@@ -4,7 +4,7 @@ import Config
config :wanderer_app, WandererApp.Repo,
username: "postgres",
password: "postgres",
hostname: System.get_env("DB_HOST", "localhost"),
hostname: "localhost",
database: "wanderer_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,

View File

@@ -258,7 +258,9 @@ config :wanderer_app, WandererApp.Scheduler,
timezone: :utc,
jobs:
[
{"@daily", {WandererApp.Map.Audit, :archive, []}}
{"@daily", {WandererApp.Map.Audit, :archive, []}},
{"@daily", {WandererApp.Map.GarbageCollector, :cleanup_chain_passages, []}},
{"@daily", {WandererApp.Map.GarbageCollector, :cleanup_system_signatures, []}}
] ++ sheduler_jobs,
timeout: :infinity

View File

@@ -1,7 +1,25 @@
defmodule WandererApp.Api.Changes.SlugifyName do
@moduledoc """
Ensures map slugs are unique by:
1. Slugifying the provided slug/name
2. Checking for existing slugs (optimization)
3. Finding next available slug with numeric suffix if needed
4. Relying on database unique constraint as final arbiter
Race Condition Mitigation:
- Optimistic check reduces DB roundtrips for most cases
- Database unique index ensures no duplicates slip through
- Proper error messages for constraint violations
- Telemetry events for monitoring conflicts
"""
use Ash.Resource.Change
alias Ash.Changeset
require Ash.Query
require Logger
# Maximum number of attempts to find a unique slug
@max_attempts 100
@impl true
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
@@ -12,10 +30,95 @@ defmodule WandererApp.Api.Changes.SlugifyName do
defp maybe_slugify_name(changeset) do
case Changeset.get_attribute(changeset, :slug) do
slug when is_binary(slug) ->
Changeset.force_change_attribute(changeset, :slug, Slug.slugify(slug))
base_slug = Slug.slugify(slug)
unique_slug = ensure_unique_slug(changeset, base_slug)
Changeset.force_change_attribute(changeset, :slug, unique_slug)
_ ->
changeset
end
end
defp ensure_unique_slug(changeset, base_slug) do
# Get the current record ID if this is an update operation
current_id = Changeset.get_attribute(changeset, :id)
# Check if the base slug is available (optimization to avoid numeric suffixes when possible)
if slug_available?(base_slug, current_id) do
base_slug
else
# Find the next available slug with a numeric suffix
find_available_slug(base_slug, current_id, 2)
end
end
defp find_available_slug(base_slug, current_id, n) when n <= @max_attempts do
candidate_slug = "#{base_slug}-#{n}"
if slug_available?(candidate_slug, current_id) do
# Emit telemetry when we had to use a suffix (indicates potential conflict)
:telemetry.execute(
[:wanderer_app, :map, :slug_suffix_used],
%{suffix_number: n},
%{base_slug: base_slug, final_slug: candidate_slug}
)
candidate_slug
else
find_available_slug(base_slug, current_id, n + 1)
end
end
defp find_available_slug(base_slug, _current_id, n) when n > @max_attempts do
# Fallback: use timestamp suffix if we've tried too many numeric suffixes
# This handles edge cases where many maps have similar names
timestamp = System.system_time(:millisecond)
fallback_slug = "#{base_slug}-#{timestamp}"
Logger.warning(
"Slug generation exceeded #{@max_attempts} attempts for '#{base_slug}', using timestamp fallback",
base_slug: base_slug,
fallback_slug: fallback_slug
)
:telemetry.execute(
[:wanderer_app, :map, :slug_fallback_used],
%{attempts: n},
%{base_slug: base_slug, fallback_slug: fallback_slug}
)
fallback_slug
end
defp slug_available?(slug, current_id) do
query =
WandererApp.Api.Map
|> Ash.Query.filter(slug == ^slug)
|> then(fn query ->
# Exclude the current record if this is an update
if current_id do
Ash.Query.filter(query, id != ^current_id)
else
query
end
end)
|> Ash.Query.limit(1)
case Ash.read(query) do
{:ok, []} ->
true
{:ok, _existing} ->
false
{:error, error} ->
# Log error but be conservative - assume slug is not available
Logger.warning("Error checking slug availability",
slug: slug,
error: inspect(error)
)
false
end
end
end

View File

@@ -30,14 +30,14 @@ defmodule WandererApp.Api.Map do
# Routes configuration
routes do
base("/maps")
get(:read)
index :read
get(:by_slug, route: "/:slug")
# index :read
post(:new)
patch(:update)
delete(:destroy)
# Custom action for map duplication
post(:duplicate, route: "/:id/duplicate")
# post(:duplicate, route: "/:id/duplicate")
end
end

View File

@@ -9,6 +9,11 @@ defmodule WandererApp.Api.MapConnection do
postgres do
repo(WandererApp.Repo)
table("map_chain_v1")
custom_indexes do
# Critical index for list_connections query performance
index [:map_id], name: "map_chain_v1_map_id_index"
end
end
json_api do

View File

@@ -65,7 +65,7 @@ defmodule WandererApp.Api.MapSubscription do
defaults [:create, :read, :update, :destroy]
read :all_active do
prepare build(sort: [updated_at: :asc])
prepare build(sort: [updated_at: :asc], load: [:map])
filter(expr(status == :active))
end

View File

@@ -1,6 +1,26 @@
defmodule WandererApp.Api.MapSystem do
@moduledoc false
@derive {Jason.Encoder,
only: [
:id,
:map_id,
:name,
:solar_system_id,
:position_x,
:position_y,
:status,
:visible,
:locked,
:custom_name,
:description,
:tag,
:temporary_name,
:labels,
:added_at,
:linked_sig_eve_id
]}
use Ash.Resource,
domain: WandererApp.Api,
data_layer: AshPostgres.DataLayer,
@@ -9,6 +29,11 @@ defmodule WandererApp.Api.MapSystem do
postgres do
repo(WandererApp.Repo)
table("map_system_v1")
custom_indexes do
# Partial index for efficient visible systems query
index [:map_id], where: "visible = true", name: "map_system_v1_map_id_visible_index"
end
end
json_api do
@@ -16,6 +41,17 @@ defmodule WandererApp.Api.MapSystem do
includes([:map])
default_fields([
:name,
:solar_system_id,
:status,
:custom_name,
:description,
:tag,
:temporary_name,
:labels
])
derive_filter?(true)
derive_sort?(true)
@@ -31,6 +67,7 @@ defmodule WandererApp.Api.MapSystem do
code_interface do
define(:create, action: :create)
define(:upsert, action: :upsert)
define(:destroy, action: :destroy)
define(:by_id,
@@ -93,6 +130,31 @@ defmodule WandererApp.Api.MapSystem do
defaults [:create, :update, :destroy]
create :upsert do
primary? false
upsert? true
upsert_identity :map_solar_system_id
# Update these fields on conflict
upsert_fields [
:position_x,
:position_y,
:visible,
:name
]
accept [
:map_id,
:solar_system_id,
:name,
:position_x,
:position_y,
:visible,
:locked,
:status
]
end
read :read do
primary?(true)

View File

@@ -38,7 +38,12 @@ defmodule WandererApp.Application do
),
Supervisor.child_spec({Cachex, name: :ship_types_cache}, id: :ship_types_cache_worker),
Supervisor.child_spec({Cachex, name: :character_cache}, id: :character_cache_worker),
Supervisor.child_spec({Cachex, name: :acl_cache}, id: :acl_cache_worker),
Supervisor.child_spec({Cachex, name: :map_cache}, id: :map_cache_worker),
Supervisor.child_spec({Cachex, name: :map_pool_cache},
id: :map_pool_cache_worker
),
Supervisor.child_spec({Cachex, name: :map_state_cache}, id: :map_state_cache_worker),
Supervisor.child_spec({Cachex, name: :character_state_cache},
id: :character_state_cache_worker
),
@@ -48,10 +53,7 @@ defmodule WandererApp.Application do
Supervisor.child_spec({Cachex, name: :wanderer_app_cache},
id: :wanderer_app_cache_worker
),
{Registry, keys: :unique, name: WandererApp.MapRegistry},
{Registry, keys: :unique, name: WandererApp.Character.TrackerRegistry},
{PartitionSupervisor,
child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
{PartitionSupervisor,
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
WandererAppWeb.PresenceGracePeriodManager,
@@ -78,6 +80,7 @@ defmodule WandererApp.Application do
WandererApp.Server.ServerStatusTracker,
WandererApp.Server.TheraDataFetcher,
{WandererApp.Character.TrackerPoolSupervisor, []},
{WandererApp.Map.MapPoolSupervisor, []},
WandererApp.Character.TrackerManager,
WandererApp.Map.Manager
] ++ security_audit_children

View File

@@ -73,6 +73,54 @@ defmodule WandererApp.Cache do
def filter_by_attr_in(type, attr, includes), do: type |> get() |> filter_in(attr, includes)
@doc """
Batch lookup multiple keys from cache.
Returns a map of key => value pairs, with `default` used for missing keys.
"""
def lookup_all(keys, default \\ nil) when is_list(keys) do
# Get all values from cache
values = get_all(keys)
# Build result map with defaults for missing keys
result =
keys
|> Enum.map(fn key ->
value = Map.get(values, key, default)
{key, value}
end)
|> Map.new()
{:ok, result}
end
@doc """
Batch insert multiple key-value pairs into cache.
Accepts a map of key => value pairs or a list of {key, value} tuples.
Skips nil values (deletes the key instead).
"""
def insert_all(entries, opts \\ [])
def insert_all(entries, opts) when is_map(entries) do
# Filter out nil values and delete those keys
{to_delete, to_insert} =
entries
|> Enum.split_with(fn {_key, value} -> is_nil(value) end)
# Delete keys with nil values
Enum.each(to_delete, fn {key, _} -> delete(key) end)
# Insert non-nil values
unless Enum.empty?(to_insert) do
put_all(to_insert, opts)
end
:ok
end
def insert_all(entries, opts) when is_list(entries) do
insert_all(Map.new(entries), opts)
end
defp find(list, %{} = attrs, match: match) do
list
|> Enum.find(fn item ->

View File

@@ -116,7 +116,7 @@ defmodule WandererApp.CachedInfo do
def get_solar_system_jumps() do
case WandererApp.Cache.lookup(:solar_system_jumps) do
{:ok, nil} ->
data = WandererApp.EveDataService.get_solar_system_jumps_data()
{:ok, data} = WandererApp.Api.MapSolarSystemJumps.read()
cache_items(data, :solar_system_jumps)

View File

@@ -4,6 +4,8 @@ defmodule WandererApp.Character do
require Logger
alias WandererApp.Cache
@read_character_wallet_scope "esi-wallet.read_character_wallet.v1"
@read_corp_wallet_scope "esi-wallet.read_corporation_wallets.v1"
@@ -16,6 +18,9 @@ defmodule WandererApp.Character do
ship_item_id: nil
}
@present_on_map_ttl :timer.seconds(10)
@not_present_on_map_ttl :timer.minutes(2)
def get_by_eve_id(character_eve_id) when is_binary(character_eve_id) do
WandererApp.Api.Character.by_eve_id(character_eve_id)
end
@@ -28,7 +33,7 @@ defmodule WandererApp.Character do
Cachex.put(:character_cache, character_id, character)
{:ok, character}
error ->
_error ->
{:error, :not_found}
end
@@ -41,7 +46,7 @@ defmodule WandererApp.Character do
def get_character!(character_id) do
case get_character(character_id) do
{:ok, character} ->
{:ok, character} when not is_nil(character) ->
character
_ ->
@@ -50,16 +55,10 @@ defmodule WandererApp.Character do
end
end
def get_map_character(map_id, character_id, opts \\ []) do
def get_map_character(map_id, character_id) do
case get_character(character_id) do
{:ok, character} ->
# If we are forcing the character to not be present, we merge the character state with map settings
character_is_present =
if opts |> Keyword.get(:not_present, false) do
false
else
WandererApp.Character.TrackerManager.Impl.character_is_present(map_id, character_id)
end
{:ok, character} when not is_nil(character) ->
character_is_present = character_is_present?(map_id, character_id)
{:ok,
character
@@ -187,12 +186,16 @@ defmodule WandererApp.Character do
{:ok, result} ->
{:ok, result |> prepare_search_results()}
{:error, error} ->
Logger.warning("#{__MODULE__} failed search: #{inspect(error)}")
{:ok, []}
error ->
Logger.warning("#{__MODULE__} failed search: #{inspect(error)}")
{:ok, []}
end
error ->
_error ->
{:ok, []}
end
end
@@ -263,22 +266,26 @@ defmodule WandererApp.Character do
end
end
defp maybe_merge_map_character_settings(%{id: character_id} = character, _map_id, true) do
{:ok, tracking_paused} =
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)
@decorate cacheable(
cache: Cache,
key: "character-present-#{map_id}-#{character_id}",
opts: [ttl: @present_on_map_ttl]
)
defp character_is_present?(map_id, character_id),
do: WandererApp.Character.TrackerManager.Impl.character_is_present(map_id, character_id)
character
|> Map.merge(%{tracking_paused: tracking_paused})
end
defp maybe_merge_map_character_settings(character, _map_id, true), do: character
@decorate cacheable(
cache: Cache,
key: "not-present-map-character-#{map_id}-#{character_id}",
opts: [ttl: @not_present_on_map_ttl]
)
defp maybe_merge_map_character_settings(
%{id: character_id} = character,
map_id,
_character_is_present
false
) do
{:ok, tracking_paused} =
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)
WandererApp.MapCharacterSettingsRepo.get(map_id, character_id)
|> case do
{:ok, settings} when not is_nil(settings) ->
@@ -296,7 +303,7 @@ defmodule WandererApp.Character do
character
|> Map.merge(@default_character_tracking_data)
end
|> Map.merge(%{online: false, tracking_paused: tracking_paused})
|> Map.merge(%{online: false})
end
defp prepare_search_results(result) do

View File

@@ -36,14 +36,11 @@ defmodule WandererApp.Character.Tracker do
status: binary()
}
@pause_tracking_timeout :timer.minutes(60 * 10)
@offline_timeout :timer.minutes(5)
@online_error_timeout :timer.minutes(10)
@ship_error_timeout :timer.minutes(10)
@location_error_timeout :timer.minutes(10)
@location_error_timeout :timer.seconds(30)
@location_error_threshold 3
@online_forbidden_ttl :timer.seconds(7)
@offline_check_delay_ttl :timer.seconds(15)
@online_limit_ttl :timer.seconds(7)
@forbidden_ttl :timer.seconds(10)
@limit_ttl :timer.seconds(5)
@location_limit_ttl :timer.seconds(1)
@@ -93,81 +90,16 @@ defmodule WandererApp.Character.Tracker do
end
end
def check_online_errors(character_id),
do: check_tracking_errors(character_id, "online", @online_error_timeout)
def check_ship_errors(character_id),
do: check_tracking_errors(character_id, "ship", @ship_error_timeout)
def check_location_errors(character_id),
do: check_tracking_errors(character_id, "location", @location_error_timeout)
defp check_tracking_errors(character_id, type, timeout) do
WandererApp.Cache.lookup!("character:#{character_id}:#{type}_error_time")
|> case do
nil ->
:skip
error_time ->
duration = DateTime.diff(DateTime.utc_now(), error_time, :millisecond)
if duration >= timeout do
pause_tracking(character_id)
WandererApp.Cache.delete("character:#{character_id}:#{type}_error_time")
:ok
else
:skip
end
end
defp increment_location_error_count(character_id) do
cache_key = "character:#{character_id}:location_error_count"
current_count = WandererApp.Cache.lookup!(cache_key) || 0
new_count = current_count + 1
WandererApp.Cache.put(cache_key, new_count)
new_count
end
defp pause_tracking(character_id) do
if WandererApp.Character.can_pause_tracking?(character_id) &&
not WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused") do
# Log character tracking statistics before pausing
Logger.debug(fn ->
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
"CHARACTER_TRACKING_PAUSED: Character tracking paused due to sustained errors: #{inspect(character_id: character_id,
active_maps: length(character_state.active_maps),
is_online: character_state.is_online,
tracking_duration_minutes: get_tracking_duration_minutes(character_id))}"
end)
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
# Original log kept for backward compatibility
Logger.warning("[CharacterTracker] paused for #{character_id}")
WandererApp.Cache.put(
"character:#{character_id}:tracking_paused",
true,
ttl: @pause_tracking_timeout
)
{:ok, %{solar_system_id: solar_system_id}} =
WandererApp.Character.get_character(character_id)
{:ok, %{active_maps: active_maps}} =
WandererApp.Character.get_character_state(character_id)
active_maps
|> Enum.each(fn map_id ->
WandererApp.Cache.put(
"map:#{map_id}:character:#{character_id}:start_solar_system_id",
solar_system_id
)
end)
end
defp reset_location_error_count(character_id) do
WandererApp.Cache.delete("character:#{character_id}:location_error_count")
end
def update_settings(character_id, track_settings) do
@@ -194,8 +126,7 @@ defmodule WandererApp.Character.Tracker do
case WandererApp.Character.get_character(character_id) do
{:ok, %{eve_id: eve_id, access_token: access_token, tracking_pool: tracking_pool}}
when not is_nil(access_token) ->
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden")
|> case do
true ->
{:error, :skipped}
@@ -227,6 +158,7 @@ defmodule WandererApp.Character.Tracker do
if online.online == true && online.online != is_online do
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
WandererApp.Cache.delete("character:#{character_id}:location_error_count")
WandererApp.Cache.delete("character:#{character_id}:info_forbidden")
WandererApp.Cache.delete("character:#{character_id}:ship_forbidden")
WandererApp.Cache.delete("character:#{character_id}:location_forbidden")
@@ -294,12 +226,6 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
reset_seconds =
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
remaining =
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
WandererApp.Cache.put(
"character:#{character_id}:online_forbidden",
true,
@@ -357,8 +283,7 @@ defmodule WandererApp.Character.Tracker do
defp get_reset_timeout(_headers, default_timeout), do: default_timeout
def update_info(character_id) do
(WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden")
|> case do
true ->
{:error, :skipped}
@@ -442,8 +367,7 @@ defmodule WandererApp.Character.Tracker do
{:ok, %{eve_id: eve_id, access_token: access_token, tracking_pool: tracking_pool}}
when not is_nil(access_token) ->
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:ship_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
WandererApp.Cache.has_key?("character:#{character_id}:ship_forbidden"))
|> case do
true ->
{:error, :skipped}
@@ -552,7 +476,7 @@ defmodule WandererApp.Character.Tracker do
case WandererApp.Character.get_character(character_id) do
{:ok, %{eve_id: eve_id, access_token: access_token, tracking_pool: tracking_pool}}
when not is_nil(access_token) ->
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused")
WandererApp.Cache.has_key?("character:#{character_id}:location_forbidden")
|> case do
true ->
{:error, :skipped}
@@ -565,19 +489,33 @@ defmodule WandererApp.Character.Tracker do
character_id: character_id
) do
{:ok, location} when is_map(location) and not is_struct(location) ->
reset_location_error_count(character_id)
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
character_state
|> maybe_update_location(location)
:ok
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
error_count = increment_location_error_count(character_id)
Logger.warning("ESI_ERROR: Character location tracking failed",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
error_count: error_count,
endpoint: "character_location"
)
if error_count >= @location_error_threshold do
WandererApp.Cache.put(
"character:#{character_id}:location_forbidden",
true,
ttl: @location_error_timeout
)
end
if is_nil(
WandererApp.Cache.lookup!("character:#{character_id}:location_error_time")
) do
@@ -601,13 +539,24 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited}
{:error, error} ->
error_count = increment_location_error_count(character_id)
Logger.error("ESI_ERROR: Character location tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
error_count: error_count,
endpoint: "character_location"
)
if error_count >= @location_error_threshold do
WandererApp.Cache.put(
"character:#{character_id}:location_forbidden",
true,
ttl: @location_error_timeout
)
end
if is_nil(
WandererApp.Cache.lookup!("character:#{character_id}:location_error_time")
) do
@@ -620,13 +569,24 @@ defmodule WandererApp.Character.Tracker do
{:error, :skipped}
_ ->
error_count = increment_location_error_count(character_id)
Logger.error("ESI_ERROR: Character location tracking failed - wrong response",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: "wrong_response",
error_count: error_count,
endpoint: "character_location"
)
if error_count >= @location_error_threshold do
WandererApp.Cache.put(
"character:#{character_id}:location_forbidden",
true,
ttl: @location_error_timeout
)
end
if is_nil(
WandererApp.Cache.lookup!("character:#{character_id}:location_error_time")
) do
@@ -662,8 +622,7 @@ defmodule WandererApp.Character.Tracker do
|> case do
true ->
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:wallet_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
WandererApp.Cache.has_key?("character:#{character_id}:wallet_forbidden"))
|> case do
true ->
{:error, :skipped}
@@ -782,8 +741,7 @@ defmodule WandererApp.Character.Tracker do
alliance_id
)
when old_alliance_id != alliance_id do
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden")
|> case do
true ->
state
@@ -829,8 +787,7 @@ defmodule WandererApp.Character.Tracker do
)
when old_corporation_id != corporation_id do
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:corporation_info_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
WandererApp.Cache.has_key?("character:#{character_id}:corporation_info_forbidden"))
|> case do
true ->
state
@@ -1137,19 +1094,6 @@ defmodule WandererApp.Character.Tracker do
defp get_online(_), do: %{online: false}
defp get_tracking_duration_minutes(character_id) do
case WandererApp.Cache.lookup!("character:#{character_id}:map:*:tracking_start_time") do
nil ->
0
start_time when is_struct(start_time, DateTime) ->
DateTime.diff(DateTime.utc_now(), start_time, :minute)
_ ->
0
end
end
# Telemetry handler for database pool monitoring
def handle_pool_query(_event_name, measurements, metadata, _config) do
queue_time = measurements[:queue_time]

View File

@@ -14,8 +14,8 @@ defmodule WandererApp.Character.TrackerManager do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def start_tracking(character_id, opts \\ []),
do: GenServer.cast(__MODULE__, {&Impl.start_tracking/3, [character_id, opts]})
def start_tracking(character_id),
do: GenServer.cast(__MODULE__, {&Impl.start_tracking/2, [character_id]})
def stop_tracking(character_id),
do: GenServer.cast(__MODULE__, {&Impl.stop_tracking/2, [character_id]})

View File

@@ -40,13 +40,13 @@ defmodule WandererApp.Character.TrackerManager.Impl do
tracked_characters
|> Enum.each(fn character_id ->
start_tracking(state, character_id, %{})
start_tracking(state, character_id)
end)
state
end
def start_tracking(state, character_id, opts) do
def start_tracking(state, character_id) do
if not WandererApp.Cache.has_key?("#{character_id}:track_requested") do
WandererApp.Cache.insert(
"#{character_id}:track_requested",

View File

@@ -8,7 +8,7 @@ defmodule WandererApp.Character.TrackerPool do
:tracked_ids,
:uuid,
:characters,
server_online: true
server_online: false
]
@name __MODULE__
@@ -17,11 +17,8 @@ defmodule WandererApp.Character.TrackerPool do
@unique_registry :unique_tracker_pool_registry
@update_location_interval :timer.seconds(1)
@update_online_interval :timer.seconds(5)
@update_online_interval :timer.seconds(30)
@check_offline_characters_interval :timer.minutes(5)
@check_online_errors_interval :timer.minutes(1)
@check_ship_errors_interval :timer.minutes(1)
@check_location_errors_interval :timer.minutes(1)
@update_ship_interval :timer.seconds(2)
@update_info_interval :timer.minutes(2)
@update_wallet_interval :timer.minutes(10)
@@ -46,10 +43,6 @@ defmodule WandererApp.Character.TrackerPool do
{:ok, _} = Registry.register(@unique_registry, Module.concat(__MODULE__, uuid), tracked_ids)
{:ok, _} = Registry.register(@registry, __MODULE__, uuid)
# Cachex.get_and_update(@cache, :tracked_characters, fn ids ->
# {:commit, ids ++ tracked_ids}
# end)
tracked_ids
|> Enum.each(fn id ->
Cachex.put(@cache, id, uuid)
@@ -79,9 +72,6 @@ defmodule WandererApp.Character.TrackerPool do
[tracked_id | r_tracked_ids]
end)
# Cachex.get_and_update(@cache, :tracked_characters, fn ids ->
# {:commit, ids ++ [tracked_id]}
# end)
Cachex.put(@cache, tracked_id, uuid)
{:noreply, %{state | characters: [tracked_id | characters]}}
@@ -96,10 +86,6 @@ defmodule WandererApp.Character.TrackerPool do
r_tracked_ids |> Enum.reject(fn id -> id == tracked_id end)
end)
# Cachex.get_and_update(@cache, :tracked_characters, fn ids ->
# {:commit, ids |> Enum.reject(fn id -> id == tracked_id end)}
# end)
#
Cachex.del(@cache, tracked_id)
{:noreply, %{state | characters: characters |> Enum.reject(fn id -> id == tracked_id end)}}
@@ -121,13 +107,10 @@ defmodule WandererApp.Character.TrackerPool do
)
Process.send_after(self(), :update_online, 100)
Process.send_after(self(), :check_online_errors, :timer.seconds(60))
Process.send_after(self(), :check_ship_errors, :timer.seconds(90))
Process.send_after(self(), :check_location_errors, :timer.seconds(120))
Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
Process.send_after(self(), :update_location, 300)
Process.send_after(self(), :update_ship, 500)
Process.send_after(self(), :update_info, 1500)
Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
if WandererApp.Env.wallet_tracking_enabled?() do
Process.send_after(self(), :update_wallet, 1000)
@@ -191,6 +174,8 @@ defmodule WandererApp.Character.TrackerPool do
[Tracker Pool] update_online => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
ErrorTracker.report(e, __STACKTRACE__)
end
{:noreply, state}
@@ -259,126 +244,6 @@ defmodule WandererApp.Character.TrackerPool do
{:noreply, state}
end
def handle_info(
:check_online_errors,
%{
characters: characters
} =
state
) do
Process.send_after(self(), :check_online_errors, @check_online_errors_interval)
try do
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(
WandererApp.Character.Tracker,
:check_online_errors,
[
character_id
]
)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
error -> @logger.error("Error in check_online_errors: #{inspect(error)}")
end)
rescue
e ->
Logger.error("""
[Tracker Pool] check_online_errors => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
end
{:noreply, state}
end
def handle_info(
:check_ship_errors,
%{
characters: characters
} =
state
) do
Process.send_after(self(), :check_ship_errors, @check_ship_errors_interval)
try do
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(
WandererApp.Character.Tracker,
:check_ship_errors,
[
character_id
]
)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
error -> @logger.error("Error in check_ship_errors: #{inspect(error)}")
end)
rescue
e ->
Logger.error("""
[Tracker Pool] check_ship_errors => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
end
{:noreply, state}
end
def handle_info(
:check_location_errors,
%{
characters: characters
} =
state
) do
Process.send_after(self(), :check_location_errors, @check_location_errors_interval)
try do
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(
WandererApp.Character.Tracker,
:check_location_errors,
[
character_id
]
)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
error -> @logger.error("Error in check_location_errors: #{inspect(error)}")
end)
rescue
e ->
Logger.error("""
[Tracker Pool] check_location_errors => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
end
{:noreply, state}
end
def handle_info(
:update_location,
%{
@@ -581,8 +446,4 @@ defmodule WandererApp.Character.TrackerPool do
Logger.debug("Failed to monitor message queue: #{inspect(error)}")
end
end
defp via_tuple(uuid) do
{:via, Registry, {@unique_registry, Module.concat(__MODULE__, uuid)}}
end
end

View File

@@ -50,14 +50,9 @@ defmodule WandererApp.Character.TrackerPoolDynamicSupervisor do
end
end
def is_not_tracked?(tracked_id) do
{:ok, tracked_ids} = Cachex.get(@cache, :tracked_characters)
tracked_ids |> Enum.member?(tracked_id) |> Kernel.not()
end
defp get_available_pool([]), do: nil
defp get_available_pool([{pid, uuid} | pools]) do
defp get_available_pool([{_pid, uuid} | pools]) do
case Registry.lookup(@unique_registry, Module.concat(WandererApp.Character.TrackerPool, uuid)) do
[] ->
nil
@@ -67,8 +62,8 @@ defmodule WandererApp.Character.TrackerPoolDynamicSupervisor do
nil ->
get_available_pool(pools)
pid ->
pid
pool_pid ->
pool_pid
end
end
end

View File

@@ -173,12 +173,11 @@ defmodule WandererApp.Character.TrackingUtils do
%{
id: character_id,
eve_id: eve_id
},
} = _character,
map_id,
is_track_allowed,
caller_pid
)
when not is_nil(caller_pid) do
) do
WandererAppWeb.Presence.update(caller_pid, map_id, character_id, %{
tracked: is_track_allowed,
from: DateTime.utc_now()
@@ -217,13 +216,16 @@ defmodule WandererApp.Character.TrackingUtils do
end
defp track_character(
_character,
character,
_map_id,
_is_track_allowed,
_caller_pid
) do
Logger.error("caller_pid is required for tracking characters")
{:error, "caller_pid is required"}
Logger.error(
"Invalid character data for tracking - character must have :id and :eve_id fields, got: #{inspect(character)}"
)
{:error, "Invalid character data"}
end
def untrack(characters, map_id, caller_pid) do
@@ -238,30 +240,14 @@ defmodule WandererApp.Character.TrackingUtils do
})
end)
# WandererApp.Map.Server.untrack_characters(map_id, character_ids)
:ok
else
true ->
Logger.error("caller_pid is required for untracking characters")
Logger.error("caller_pid is required for untracking characters 2")
{:error, "caller_pid is required"}
end
end
# def add_characters([], _map_id, _track_character), do: :ok
# def add_characters([character | characters], map_id, track_character) do
# :ok = WandererApp.Map.Server.add_character(map_id, character, track_character)
# add_characters(characters, map_id, track_character)
# end
# def remove_characters([], _map_id), do: :ok
# def remove_characters([character | characters], map_id) do
# :ok = WandererApp.Map.Server.remove_character(map_id, character.id)
# remove_characters(characters, map_id)
# end
def get_main_character(
nil,
current_user_characters,

View File

@@ -12,7 +12,7 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
total_balance: 0,
transactions: [],
retries: 5,
server_online: true,
server_online: false,
status: :started
]
@@ -75,7 +75,7 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
def handle_event(
:update_corp_wallets,
%{character: character} = state
%{character: character, server_online: true} = state
) do
Process.send_after(self(), :update_corp_wallets, @update_interval)
@@ -88,26 +88,26 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
:update_corp_wallets,
state
) do
Process.send_after(self(), :update_corp_wallets, :timer.seconds(15))
Process.send_after(self(), :update_corp_wallets, @update_interval)
state
end
def handle_event(
:check_wallets,
%{wallets: []} = state
%{character: character, wallets: wallets, server_online: true} = state
) do
Process.send_after(self(), :check_wallets, :timer.seconds(5))
Process.send_after(self(), :check_wallets, @update_interval)
state
end
def handle_event(
:check_wallets,
%{character: character, wallets: wallets} = state
) do
check_wallets(wallets, character)
state
end
def handle_event(
:check_wallets,
state
) do
Process.send_after(self(), :check_wallets, @update_interval)
state

View File

@@ -14,8 +14,6 @@ defmodule WandererApp.DatabaseSetup do
alias WandererApp.Repo
alias Ecto.Adapters.SQL
@test_db_name "wanderer_test"
@doc """
Sets up the test database from scratch.
Creates the database, runs migrations, and sets up initial data.

View File

@@ -21,7 +21,8 @@ defmodule WandererApp.Esi do
defdelegate get_character_location(character_eve_id, opts \\ []), to: WandererApp.Esi.ApiClient
defdelegate get_character_online(character_eve_id, opts \\ []), to: WandererApp.Esi.ApiClient
defdelegate get_character_ship(character_eve_id, opts \\ []), to: WandererApp.Esi.ApiClient
defdelegate find_routes(map_id, origin, hubs, routes_settings), to: WandererApp.Esi.ApiClient
defdelegate get_routes_custom(hubs, origin, params), to: WandererApp.Esi.ApiClient
defdelegate get_routes_eve(hubs, origin, params, opts), to: WandererApp.Esi.ApiClient
defdelegate search(character_eve_id, opts \\ []), to: WandererApp.Esi.ApiClient
defdelegate get_killmail(killmail_id, killmail_hash, opts \\ []), to: WandererApp.Esi.ApiClient

View File

@@ -6,35 +6,9 @@ defmodule WandererApp.Esi.ApiClient do
alias WandererApp.Cache
@ttl :timer.hours(1)
@routes_ttl :timer.minutes(15)
@base_url "https://esi.evetech.net/latest"
@wanderrer_user_agent "(wanderer-industries@proton.me; +https://github.com/wanderer-industries/wanderer)"
@req_esi Req.new(base_url: @base_url, finch: WandererApp.Finch)
@get_link_pairs_advanced_params [
:include_mass_crit,
:include_eol,
:include_frig
]
@default_routes_settings %{
path_type: "shortest",
include_mass_crit: true,
include_eol: false,
include_frig: true,
include_cruise: true,
avoid_wormholes: false,
avoid_pochven: false,
avoid_edencom: false,
avoid_triglavian: false,
include_thera: true,
avoid: []
}
@zarzakh_system 30_100_000
@default_avoid_systems [@zarzakh_system]
@req_esi_options [base_url: "https://esi.evetech.net", finch: WandererApp.Finch]
@cache_opts [cache: true]
@retry_opts [retry: false, retry_log_level: :warning]
@@ -43,11 +17,11 @@ defmodule WandererApp.Esi.ApiClient do
@logger Application.compile_env(:wanderer_app, :logger)
def get_server_status, do: get("/status")
def get_server_status, do: do_get("/status", [], @cache_opts)
def set_autopilot_waypoint(add_to_beginning, clear_other_waypoints, destination_id, opts \\ []),
do:
post_esi(
do_post_esi(
"/ui/autopilot/waypoint",
get_auth_opts(opts)
|> Keyword.merge(
@@ -62,7 +36,7 @@ defmodule WandererApp.Esi.ApiClient do
def post_characters_affiliation(character_eve_ids, _opts)
when is_list(character_eve_ids),
do:
post_esi(
do_post_esi(
"/characters/affiliation/",
json: character_eve_ids,
params: %{
@@ -70,168 +44,9 @@ defmodule WandererApp.Esi.ApiClient do
}
)
def find_routes(map_id, origin, hubs, routes_settings) do
origin = origin |> String.to_integer()
hubs = hubs |> Enum.map(&(&1 |> String.to_integer()))
routes_settings = @default_routes_settings |> Map.merge(routes_settings)
connections =
case routes_settings.avoid_wormholes do
false ->
map_chains =
routes_settings
|> Map.take(@get_link_pairs_advanced_params)
|> Map.put_new(:map_id, map_id)
|> WandererApp.Api.MapConnection.get_link_pairs_advanced!()
|> Enum.map(fn %{
solar_system_source: solar_system_source,
solar_system_target: solar_system_target
} ->
%{
first: solar_system_source,
second: solar_system_target
}
end)
|> Enum.uniq()
{:ok, thera_chains} =
case routes_settings.include_thera do
true ->
WandererApp.Server.TheraDataFetcher.get_chain_pairs(routes_settings)
false ->
{:ok, []}
end
chains = remove_intersection([map_chains | thera_chains] |> List.flatten())
chains =
case routes_settings.include_cruise do
false ->
{:ok, wh_class_a_systems} = WandererApp.CachedInfo.get_wh_class_a_systems()
chains
|> Enum.filter(fn x ->
not Enum.member?(wh_class_a_systems, x.first) and
not Enum.member?(wh_class_a_systems, x.second)
end)
_ ->
chains
end
chains
|> Enum.map(fn chain ->
["#{chain.first}|#{chain.second}", "#{chain.second}|#{chain.first}"]
end)
|> List.flatten()
true ->
[]
end
{:ok, trig_systems} = WandererApp.CachedInfo.get_trig_systems()
pochven_solar_systems =
trig_systems
|> Enum.filter(fn s -> s.triglavian_invasion_status == "Final" end)
|> Enum.map(& &1.solar_system_id)
triglavian_solar_systems =
trig_systems
|> Enum.filter(fn s -> s.triglavian_invasion_status == "Triglavian" end)
|> Enum.map(& &1.solar_system_id)
edencom_solar_systems =
trig_systems
|> Enum.filter(fn s -> s.triglavian_invasion_status == "Edencom" end)
|> Enum.map(& &1.solar_system_id)
avoidance_list =
case routes_settings.avoid_edencom do
true ->
edencom_solar_systems
false ->
[]
end
avoidance_list =
case routes_settings.avoid_triglavian do
true ->
[avoidance_list | triglavian_solar_systems]
false ->
avoidance_list
end
avoidance_list =
case routes_settings.avoid_pochven do
true ->
[avoidance_list | pochven_solar_systems]
false ->
avoidance_list
end
avoidance_list =
(@default_avoid_systems ++ [routes_settings.avoid | avoidance_list])
|> List.flatten()
|> Enum.uniq()
params =
%{
datasource: "tranquility",
flag: routes_settings.path_type,
connections: connections,
avoid: avoidance_list
}
{:ok, all_routes} = get_all_routes(hubs, origin, params)
routes =
all_routes
|> Enum.map(fn route_info ->
map_route_info(route_info)
end)
|> Enum.filter(fn route_info -> not is_nil(route_info) end)
{:ok, routes}
end
def get_all_routes(hubs, origin, params, opts \\ []) do
cache_key =
"routes-#{origin}-#{hubs |> Enum.join("-")}-#{:crypto.hash(:sha, :erlang.term_to_binary(params))}"
case WandererApp.Cache.lookup(cache_key) do
{:ok, result} when not is_nil(result) ->
{:ok, result}
_ ->
case get_all_routes_custom(hubs, origin, params) do
{:ok, result} ->
WandererApp.Cache.insert(
cache_key,
result,
ttl: @routes_ttl
)
{:ok, result}
{:error, _error} ->
@logger.error(
"Error getting custom routes for #{inspect(origin)}: #{inspect(params)}"
)
get_all_routes_eve(hubs, origin, params, opts)
end
end
end
defp get_all_routes_custom(hubs, origin, params),
def get_routes_custom(hubs, origin, params),
do:
post(
do_post(
"#{get_custom_route_base_url()}/route/multiple",
[
json: %{
@@ -245,13 +60,20 @@ defmodule WandererApp.Esi.ApiClient do
|> Keyword.merge(@timeout_opts)
)
def get_all_routes_eve(hubs, origin, params, opts),
def get_routes_eve(hubs, origin, params, opts),
do:
{:ok,
hubs
|> Task.async_stream(
fn destination ->
get_routes(origin, destination, params, opts)
%{
"origin" => origin,
"destination" => destination,
"systems" => [],
"success" => false
}
# do_get_routes_eve(origin, destination, params, opts)
end,
max_concurrency: System.schedulers_online() * 4,
timeout: :timer.seconds(30),
@@ -265,8 +87,19 @@ defmodule WandererApp.Esi.ApiClient do
end
end)}
def get_routes(origin, destination, params, opts) do
case _get_routes(origin, destination, params, opts) do
defp do_get_routes_eve(origin, destination, params, opts) do
esi_params =
Map.merge(params, %{
connections: params.connections |> Enum.join(","),
avoid: params.avoid |> Enum.join(",")
})
do_get(
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
opts,
@cache_opts
)
|> case do
{:ok, result} ->
%{
"origin" => origin,
@@ -299,9 +132,8 @@ defmodule WandererApp.Esi.ApiClient do
key: "killmail-#{killmail_id}-#{killmail_hash}",
opts: [ttl: @ttl]
)
def get_killmail(killmail_id, killmail_hash, opts \\ []) do
get("/killmails/#{killmail_id}/#{killmail_hash}/", opts, @cache_opts)
end
def get_killmail(killmail_id, killmail_hash, opts \\ []),
do: do_get("/killmails/#{killmail_id}/#{killmail_hash}/", opts, @cache_opts)
@decorate cacheable(
cache: Cache,
@@ -322,7 +154,7 @@ defmodule WandererApp.Esi.ApiClient do
opts: [ttl: @ttl]
)
def get_character_info(eve_id, opts \\ []) do
case get(
case do_get(
"/characters/#{eve_id}/",
opts,
@cache_opts
@@ -395,48 +227,11 @@ defmodule WandererApp.Esi.ApiClient do
get_character_auth_data(character_eve_id, "search", merged_opts)
end
defp remove_intersection(pairs_arr) do
tuples = pairs_arr |> Enum.map(fn x -> {x.first, x.second} end)
tuples
|> Enum.reduce([], fn {first, second} = x, acc ->
if Enum.member?(tuples, {second, first}) do
acc
else
[x | acc]
end
end)
|> Enum.uniq()
|> Enum.map(fn {first, second} ->
%{
first: first,
second: second
}
end)
end
defp _get_routes(origin, destination, params, opts),
do: get_routes_eve(origin, destination, params, opts)
defp get_routes_eve(origin, destination, params, opts) do
esi_params =
Map.merge(params, %{
connections: params.connections |> Enum.join(","),
avoid: params.avoid |> Enum.join(",")
})
get(
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
opts,
@cache_opts
)
end
defp get_auth_opts(opts), do: [auth: {:bearer, opts[:access_token]}]
defp get_alliance_info(alliance_eve_id, info_path, opts),
do:
get(
do_get(
"/alliances/#{alliance_eve_id}/#{info_path}",
opts,
@cache_opts
@@ -444,7 +239,7 @@ defmodule WandererApp.Esi.ApiClient do
defp get_corporation_info(corporation_eve_id, info_path, opts),
do:
get(
do_get(
"/corporations/#{corporation_eve_id}/#{info_path}",
opts,
@cache_opts
@@ -460,13 +255,13 @@ defmodule WandererApp.Esi.ApiClient do
character_id = opts |> Keyword.get(:character_id, nil)
if not is_access_token_expired?(character_id) do
get(
do_get(
path,
auth_opts,
opts |> with_refresh_token()
)
else
get_retry(path, auth_opts, opts |> with_refresh_token())
do_get_retry(path, auth_opts, opts |> with_refresh_token())
end
end
@@ -481,29 +276,26 @@ defmodule WandererApp.Esi.ApiClient do
defp get_corporation_auth_data(corporation_eve_id, info_path, opts),
do:
get(
do_get(
"/corporations/#{corporation_eve_id}/#{info_path}",
[params: opts[:params] || []] ++
(opts |> get_auth_opts()),
(opts |> with_refresh_token()) ++ @cache_opts
)
defp with_user_agent_opts(opts) do
opts
|> Keyword.merge(
headers: [{:user_agent, "Wanderer/#{WandererApp.Env.vsn()} #{@wanderrer_user_agent}"}]
)
end
defp with_user_agent_opts(opts),
do:
opts
|> Keyword.merge(
headers: [{:user_agent, "Wanderer/#{WandererApp.Env.vsn()} #{@wanderrer_user_agent}"}]
)
defp with_refresh_token(opts) do
opts |> Keyword.merge(refresh_token?: true)
end
defp with_refresh_token(opts), do: opts |> Keyword.merge(refresh_token?: true)
defp with_cache_opts(opts) do
opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
end
defp with_cache_opts(opts),
do: opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
defp get(path, api_opts \\ [], opts \\ []) do
defp do_get(path, api_opts \\ [], opts \\ []) do
case Cachex.get(:api_cache, path) do
{:ok, cached_data} when not is_nil(cached_data) ->
{:ok, cached_data}
@@ -515,15 +307,17 @@ defmodule WandererApp.Esi.ApiClient do
defp do_get_request(path, api_opts \\ [], opts \\ []) do
try do
case Req.get(
@req_esi,
api_opts
|> Keyword.merge(url: path)
|> with_user_agent_opts()
|> with_cache_opts()
|> Keyword.merge(@retry_opts)
|> Keyword.merge(@timeout_opts)
) do
@req_esi_options
|> Req.new()
|> Req.get(
api_opts
|> Keyword.merge(url: path)
|> with_user_agent_opts()
|> with_cache_opts()
|> Keyword.merge(@retry_opts)
|> Keyword.merge(@timeout_opts)
)
|> case do
{:ok, %{status: 200, body: body, headers: headers}} ->
maybe_cache_response(path, body, headers, opts)
@@ -537,8 +331,8 @@ defmodule WandererApp.Esi.ApiClient do
{:ok, %{status: 420, headers: headers} = _error} ->
# Extract rate limit information from headers
reset_seconds = Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
remaining = Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
reset_seconds = Map.get(headers, "x-esi-error-limit-reset", ["0"]) |> List.first()
remaining = Map.get(headers, "x-esi-error-limit-remain", ["0"]) |> List.first()
# Emit telemetry for rate limiting
:telemetry.execute(
@@ -568,10 +362,40 @@ defmodule WandererApp.Esi.ApiClient do
{:error, :error_limited, headers}
{:ok, %{status: status} = _error} when status in [401, 403] ->
get_retry(path, api_opts, opts)
{:ok, %{status: 429, headers: headers} = _error} ->
# Extract rate limit information from headers
reset_seconds = Map.get(headers, "retry-after", ["0"]) |> List.first()
{:ok, %{status: status}} ->
# Emit telemetry for rate limiting
:telemetry.execute(
[:wanderer_app, :esi, :rate_limited],
%{
count: 1,
reset_duration:
case Integer.parse(reset_seconds || "0") do
{seconds, _} -> seconds * 1000
_ -> 0
end
},
%{
method: "GET",
path: path,
reset_seconds: reset_seconds
}
)
Logger.warning("ESI_RATE_LIMITED: GET request rate limited",
method: "GET",
path: path,
reset_seconds: reset_seconds
)
{:error, :error_limited, headers}
{:ok, %{status: status} = _error} when status in [401, 403] ->
do_get_retry(path, api_opts, opts)
{:ok, %{status: status, headers: headers}} ->
{:error, "Unexpected status: #{status}"}
{:error, _reason} ->
@@ -585,7 +409,7 @@ defmodule WandererApp.Esi.ApiClient do
end
end
defp maybe_cache_response(path, body, %{"expires" => [expires]}, opts)
defp maybe_cache_response(path, body, %{"expires" => [expires]} = _headers, opts)
when is_binary(path) and not is_nil(expires) do
try do
if opts |> Keyword.get(:cache, false) do
@@ -609,7 +433,7 @@ defmodule WandererApp.Esi.ApiClient do
defp maybe_cache_response(_path, _body, _headers, _opts), do: :ok
defp post(url, opts) do
defp do_post(url, opts) do
try do
case Req.post("#{url}", opts |> with_user_agent_opts()) do
{:ok, %{status: status, body: body}} when status in [200, 201] ->
@@ -623,8 +447,8 @@ defmodule WandererApp.Esi.ApiClient do
{:ok, %{status: 420, headers: headers} = _error} ->
# Extract rate limit information from headers
reset_seconds = Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
remaining = Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
reset_seconds = Map.get(headers, "x-esi-error-limit-reset", ["0"]) |> List.first()
remaining = Map.get(headers, "x-esi-error-limit-remain", ["0"]) |> List.first()
# Emit telemetry for rate limiting
:telemetry.execute(
@@ -668,16 +492,13 @@ defmodule WandererApp.Esi.ApiClient do
end
end
defp post_esi(url, opts) do
defp do_post_esi(url, opts) do
try do
req_opts =
(opts |> with_user_agent_opts() |> Keyword.merge(@retry_opts)) ++
[params: opts[:params] || []]
Req.new(
[base_url: @base_url, finch: WandererApp.Finch] ++
req_opts
)
Req.new(@req_esi_options ++ req_opts)
|> Req.post(url: url)
|> case do
{:ok, %{status: status, body: body}} when status in [200, 201] ->
@@ -691,8 +512,8 @@ defmodule WandererApp.Esi.ApiClient do
{:ok, %{status: 420, headers: headers} = _error} ->
# Extract rate limit information from headers
reset_seconds = Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
remaining = Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
reset_seconds = Map.get(headers, "x-esi-error-limit-reset", ["0"]) |> List.first()
remaining = Map.get(headers, "x-esi-error-limit-remain", ["0"]) |> List.first()
# Emit telemetry for rate limiting
:telemetry.execute(
@@ -722,6 +543,36 @@ defmodule WandererApp.Esi.ApiClient do
{:error, :error_limited, headers}
{:ok, %{status: 429, headers: headers} = _error} ->
# Extract rate limit information from headers
reset_seconds = Map.get(headers, "retry-after", ["0"]) |> List.first()
# Emit telemetry for rate limiting
:telemetry.execute(
[:wanderer_app, :esi, :rate_limited],
%{
count: 1,
reset_duration:
case Integer.parse(reset_seconds || "0") do
{seconds, _} -> seconds * 1000
_ -> 0
end
},
%{
method: "POST_ESI",
path: url,
reset_seconds: reset_seconds
}
)
Logger.warning("ESI_RATE_LIMITED: POST request rate limited",
method: "POST_ESI",
path: url,
reset_seconds: reset_seconds
)
{:error, :error_limited, headers}
{:ok, %{status: status}} ->
{:error, "Unexpected status: #{status}"}
@@ -736,7 +587,7 @@ defmodule WandererApp.Esi.ApiClient do
end
end
defp get_retry(path, api_opts, opts, status \\ :forbidden) do
defp do_get_retry(path, api_opts, opts, status \\ :forbidden) do
refresh_token? = opts |> Keyword.get(:refresh_token?, false)
retry_count = opts |> Keyword.get(:retry_count, 0)
character_id = opts |> Keyword.get(:character_id, nil)
@@ -748,7 +599,7 @@ defmodule WandererApp.Esi.ApiClient do
{:ok, token} ->
auth_opts = [access_token: token.access_token] |> get_auth_opts()
get(
do_get(
path,
api_opts |> Keyword.merge(auth_opts),
opts |> Keyword.merge(retry_count: retry_count + 1)
@@ -913,44 +764,4 @@ defmodule WandererApp.Esi.ApiClient do
:character_token_invalid
)
end
defp map_route_info(
%{
"origin" => origin,
"destination" => destination,
"systems" => result_systems,
"success" => success
} = _route_info
),
do:
map_route_info(%{
origin: origin,
destination: destination,
systems: result_systems,
success: success
})
defp map_route_info(
%{origin: origin, destination: destination, systems: result_systems, success: success} =
_route_info
) do
systems =
case result_systems do
[] ->
[]
_ ->
result_systems |> Enum.reject(fn system_id -> system_id == origin end)
end
%{
has_connection: result_systems != [],
systems: systems,
origin: origin,
destination: destination,
success: success
}
end
defp map_route_info(_), do: nil
end

View File

@@ -38,32 +38,8 @@ defmodule WandererApp.EveDataService do
|> Ash.bulk_create(WandererApp.Api.MapSolarSystemJumps, :create)
Logger.info("MapSolarSystemJumps updated!")
end
def download_files() do
tasks =
@dump_file_names
|> Enum.map(fn file_name ->
Task.async(fn ->
download_file(file_name)
end)
end)
Task.await_many(tasks, :timer.minutes(30))
end
def download_file(file_name) do
url = "#{@eve_db_dump_url}/#{file_name}"
Logger.info("Downloading file from #{url}")
download_path = Path.join([:code.priv_dir(:wanderer_app), "repo", "data", file_name])
Req.get!(url, raw: true, into: File.stream!(download_path, [:write])).body
|> Stream.run()
Logger.info("File downloaded successfully to #{download_path}")
:ok
cleanup_files()
end
def load_wormhole_types() do
@@ -163,7 +139,57 @@ defmodule WandererApp.EveDataService do
data
end
def load_map_constellations() do
defp cleanup_files() do
tasks =
@dump_file_names
|> Enum.map(fn file_name ->
Task.async(fn ->
cleanup_file(file_name)
end)
end)
Task.await_many(tasks, :timer.minutes(30))
end
defp cleanup_file(file_name) do
Logger.info("Cleaning file: #{file_name}")
download_path = Path.join([:code.priv_dir(:wanderer_app), "repo", "data", file_name])
:ok = File.rm(download_path)
Logger.info("File removed successfully to #{download_path}")
:ok
end
defp download_files() do
tasks =
@dump_file_names
|> Enum.map(fn file_name ->
Task.async(fn ->
download_file(file_name)
end)
end)
Task.await_many(tasks, :timer.minutes(30))
end
defp download_file(file_name) do
url = "#{@eve_db_dump_url}/#{file_name}"
Logger.info("Downloading file from #{url}")
download_path = Path.join([:code.priv_dir(:wanderer_app), "repo", "data", file_name])
Req.get!(url, raw: true, into: File.stream!(download_path, [:write])).body
|> Stream.run()
Logger.info("File downloaded successfully to #{download_path}")
:ok
end
defp load_map_constellations() do
WandererApp.Utils.CSVUtil.csv_row_to_table_record(
"#{:code.priv_dir(:wanderer_app)}/repo/data/mapConstellations.csv",
fn row ->
@@ -175,7 +201,7 @@ defmodule WandererApp.EveDataService do
)
end
def load_map_regions() do
defp load_map_regions() do
WandererApp.Utils.CSVUtil.csv_row_to_table_record(
"#{:code.priv_dir(:wanderer_app)}/repo/data/mapRegions.csv",
fn row ->
@@ -187,7 +213,7 @@ defmodule WandererApp.EveDataService do
)
end
def load_map_location_wormhole_classes() do
defp load_map_location_wormhole_classes() do
WandererApp.Utils.CSVUtil.csv_row_to_table_record(
"#{:code.priv_dir(:wanderer_app)}/repo/data/mapLocationWormholeClasses.csv",
fn row ->
@@ -199,7 +225,7 @@ defmodule WandererApp.EveDataService do
)
end
def load_inv_groups() do
defp load_inv_groups() do
WandererApp.Utils.CSVUtil.csv_row_to_table_record(
"#{:code.priv_dir(:wanderer_app)}/repo/data/invGroups.csv",
fn row ->
@@ -212,7 +238,7 @@ defmodule WandererApp.EveDataService do
)
end
def get_db_data() do
defp get_db_data() do
map_constellations = load_map_constellations()
map_regions = load_map_regions()
map_location_wormhole_classes = load_map_location_wormhole_classes()
@@ -296,7 +322,7 @@ defmodule WandererApp.EveDataService do
)
end
def get_ship_types_data() do
defp get_ship_types_data() do
inv_groups = load_inv_groups()
ship_type_groups =
@@ -331,7 +357,7 @@ defmodule WandererApp.EveDataService do
|> Enum.filter(fn t -> t.group_id in ship_type_groups end)
end
def get_solar_system_jumps_data() do
defp get_solar_system_jumps_data() do
WandererApp.Utils.CSVUtil.csv_row_to_table_record(
"#{:code.priv_dir(:wanderer_app)}/repo/data/mapSolarSystemJumps.csv",
fn row ->

View File

@@ -72,20 +72,12 @@ defmodule WandererApp.ExternalEvents do
# Check if MapEventRelay is alive before sending
if Process.whereis(MapEventRelay) do
try do
# Use call with timeout instead of cast for better error handling
GenServer.call(MapEventRelay, {:deliver_event, event}, 5000)
:ok
catch
:exit, {:timeout, _} ->
Logger.error("Timeout delivering event to MapEventRelay for map #{map_id}")
{:error, :timeout}
:exit, reason ->
Logger.error("Failed to deliver event to MapEventRelay: #{inspect(reason)}")
{:error, reason}
end
# Use cast for async delivery to avoid blocking the caller
# This is critical for performance in hot paths (character updates)
GenServer.cast(MapEventRelay, {:deliver_event, event})
:ok
else
Logger.warning("MapEventRelay not available for event delivery (map: #{map_id})")
{:error, :relay_not_available}
end
else

View File

@@ -20,6 +20,7 @@ defmodule WandererApp.ExternalEvents.Event do
| :character_added
| :character_removed
| :character_updated
| :characters_updated
| :map_kill
| :acl_member_added
| :acl_member_removed
@@ -42,50 +43,6 @@ defmodule WandererApp.ExternalEvents.Event do
defstruct [:id, :map_id, :type, :payload, :timestamp]
@doc """
Creates a new external event with ULID for ordering.
Validates that the event_type is supported before creating the event.
"""
@spec new(String.t(), event_type(), map()) :: t() | {:error, :invalid_event_type}
def new(map_id, event_type, payload) when is_binary(map_id) and is_map(payload) do
if valid_event_type?(event_type) do
%__MODULE__{
id: Ecto.ULID.generate(System.system_time(:millisecond)),
map_id: map_id,
type: event_type,
payload: payload,
timestamp: DateTime.utc_now()
}
else
raise ArgumentError,
"Invalid event type: #{inspect(event_type)}. Must be one of: #{supported_event_types() |> Enum.map(&to_string/1) |> Enum.join(", ")}"
end
end
@doc """
Converts an event to JSON format for delivery.
"""
@spec to_json(t()) :: map()
def to_json(%__MODULE__{} = event) do
%{
"id" => event.id,
"type" => to_string(event.type),
"map_id" => event.map_id,
"timestamp" => DateTime.to_iso8601(event.timestamp),
"payload" => serialize_payload(event.payload)
}
end
# Convert Ash structs and other complex types to plain maps
defp serialize_payload(payload) when is_struct(payload) do
serialize_payload(payload, MapSet.new())
end
defp serialize_payload(payload) when is_map(payload) do
serialize_payload(payload, MapSet.new())
end
# Define allowlisted fields for different struct types
@system_fields [
:id,
@@ -133,6 +90,73 @@ defmodule WandererApp.ExternalEvents.Event do
]
@signature_fields [:id, :signature_id, :name, :type, :group]
@supported_event_types [
:add_system,
:deleted_system,
:system_renamed,
:system_metadata_changed,
:signatures_updated,
:signature_added,
:signature_removed,
:connection_added,
:connection_removed,
:connection_updated,
:character_added,
:character_removed,
:character_updated,
:characters_updated,
:map_kill,
:acl_member_added,
:acl_member_removed,
:acl_member_updated,
:rally_point_added,
:rally_point_removed
]
@doc """
Creates a new external event with ULID for ordering.
Validates that the event_type is supported before creating the event.
"""
@spec new(String.t(), event_type(), map()) :: t() | {:error, :invalid_event_type}
def new(map_id, event_type, payload) when is_binary(map_id) and is_map(payload) do
if valid_event_type?(event_type) do
%__MODULE__{
id: Ecto.ULID.generate(System.system_time(:millisecond)),
map_id: map_id,
type: event_type,
payload: payload,
timestamp: DateTime.utc_now()
}
else
raise ArgumentError,
"Invalid event type: #{inspect(event_type)}. Must be one of: #{supported_event_types() |> Enum.map(&to_string/1) |> Enum.join(", ")}"
end
end
@doc """
Converts an event to JSON format for delivery.
"""
@spec to_json(t()) :: map()
def to_json(%__MODULE__{} = event) do
%{
"id" => event.id,
"type" => to_string(event.type),
"map_id" => event.map_id,
"timestamp" => DateTime.to_iso8601(event.timestamp),
"payload" => serialize_payload(event.payload)
}
end
# Convert Ash structs and other complex types to plain maps
defp serialize_payload(payload) when is_struct(payload) do
serialize_payload(payload, MapSet.new())
end
defp serialize_payload(payload) when is_map(payload) do
serialize_payload(payload, MapSet.new())
end
# Overloaded versions with visited tracking
defp serialize_payload(payload, visited) when is_struct(payload) do
# Check for circular reference
@@ -193,29 +217,7 @@ defmodule WandererApp.ExternalEvents.Event do
Returns all supported event types.
"""
@spec supported_event_types() :: [event_type()]
def supported_event_types do
[
:add_system,
:deleted_system,
:system_renamed,
:system_metadata_changed,
:signatures_updated,
:signature_added,
:signature_removed,
:connection_added,
:connection_removed,
:connection_updated,
:character_added,
:character_removed,
:character_updated,
:map_kill,
:acl_member_added,
:acl_member_removed,
:acl_member_updated,
:rally_point_added,
:rally_point_removed
]
end
def supported_event_types, do: @supported_event_types
@doc """
Validates an event type.

View File

@@ -212,6 +212,7 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
"time_status" => payload["time_status"] || payload[:time_status],
"mass_status" => payload["mass_status"] || payload[:mass_status],
"ship_size_type" => payload["ship_size_type"] || payload[:ship_size_type],
"locked" => payload["locked"] || payload[:locked],
"updated_at" => event.timestamp
},
"relationships" => %{

View File

@@ -82,16 +82,9 @@ defmodule WandererApp.ExternalEvents.MapEventRelay do
@impl true
def handle_call({:deliver_event, %Event{} = event}, _from, state) do
# Log ACL events at info level for debugging
if event.type in [:acl_member_added, :acl_member_removed, :acl_member_updated] do
Logger.debug(fn ->
"MapEventRelay received :deliver_event (call) for map #{event.map_id}, type: #{event.type}"
end)
else
Logger.debug(fn ->
"MapEventRelay received :deliver_event (call) for map #{event.map_id}, type: #{event.type}"
end)
end
Logger.debug(fn ->
"MapEventRelay received :deliver_event (call) for map #{event.map_id}, type: #{event.type}"
end)
new_state = deliver_single_event(event, state)
{:reply, :ok, new_state}

View File

@@ -31,7 +31,7 @@ defmodule WandererApp.StartCorpWalletTrackerTask do
if not is_nil(admin_character) do
:ok =
WandererApp.Character.TrackerManager.start_tracking(admin_character.id, keep_alive: true)
WandererApp.Character.TrackerManager.start_tracking(admin_character.id)
{:ok, _pid} =
WandererApp.Character.TrackerManager.start_transaction_tracker(admin_character.id)

View File

@@ -546,7 +546,7 @@ defmodule WandererApp.Kills.Client do
end
end
defp check_health(%{socket_pid: pid, last_message_time: last_msg_time} = state)
defp check_health(%{socket_pid: pid, last_message_time: last_msg_time} = _state)
when not is_nil(pid) and not is_nil(last_msg_time) do
cond do
not socket_alive?(pid) ->

View File

@@ -109,8 +109,8 @@ defmodule WandererApp.Kills.MapEventListener do
# Handle re-subscription attempt
def handle_info(:resubscribe_to_maps, state) do
running_maps = WandererApp.Map.RegistryHelper.list_all_maps()
current_running_map_ids = MapSet.new(Enum.map(running_maps, & &1.id))
{:ok, started_maps} = WandererApp.Cache.lookup("started_maps", [])
current_running_map_ids = MapSet.new(started_maps)
Logger.debug(fn ->
"[MapEventListener] Resubscribing to maps. Running maps: #{MapSet.size(current_running_map_ids)}"
@@ -229,7 +229,7 @@ defmodule WandererApp.Kills.MapEventListener do
{:error, :not_running} ->
{:error, :not_running}
{:ok, status} ->
{:ok, _status} ->
{:error, :not_connected}
error ->

View File

@@ -88,13 +88,13 @@ defmodule WandererApp.Kills.Subscription.MapIntegration do
def get_tracked_system_ids do
try do
# Get systems from currently running maps
active_maps = WandererApp.Map.RegistryHelper.list_all_maps()
{:ok, started_maps_ids} = WandererApp.Cache.lookup("started_maps", [])
Logger.debug("[MapIntegration] Found #{length(active_maps)} active maps")
Logger.debug("[MapIntegration] Found #{length(started_maps_ids)} active maps")
map_systems =
active_maps
|> Enum.map(fn %{id: map_id} ->
started_maps_ids
|> Enum.map(fn map_id ->
case WandererApp.MapSystemRepo.get_visible_by_map(map_id) do
{:ok, systems} ->
system_ids = Enum.map(systems, & &1.solar_system_id)
@@ -114,7 +114,7 @@ defmodule WandererApp.Kills.Subscription.MapIntegration do
|> Enum.uniq()
Logger.debug(fn ->
"[MapIntegration] Total tracked systems: #{length(system_ids)} across #{length(active_maps)} maps"
"[MapIntegration] Total tracked systems: #{length(system_ids)} across #{length(started_maps_ids)} maps"
end)
{:ok, system_ids}

View File

@@ -136,9 +136,6 @@ defmodule WandererApp.License.LicenseManager do
end
end
@doc """
Updates a license's expiration date based on the map's subscription.
"""
def update_license_expiration_from_subscription(map_id) do
with {:ok, license} <- get_license_by_map_id(map_id),
{:ok, subscription} <- SubscriptionManager.get_active_map_subscription(map_id) do
@@ -146,43 +143,15 @@ defmodule WandererApp.License.LicenseManager do
end
end
@doc """
Checks if a license is expired.
"""
defp expired?(license) do
case license.expire_at do
nil -> false
expire_at -> DateTime.compare(expire_at, DateTime.utc_now()) == :lt
end
end
@doc """
Generates a random string of specified length.
"""
defp generate_random_string(length) do
:crypto.strong_rand_bytes(length)
|> Base.encode16(case: :upper)
|> binary_part(0, length)
end
@doc """
Formats a datetime as YYYY-MM-DD.
"""
defp format_date(datetime) do
Calendar.strftime(datetime, "%Y-%m-%d")
end
@doc """
Generates a link to the map.
"""
defp generate_map_link(map_slug) do
base_url = Application.get_env(:wanderer_app, :web_app_url)
"#{base_url}/#{map_slug}"
end
@doc """
Gets the map owner's data.
"""
defp get_map_owner_email(map) do
{:ok, %{owner: owner}} = map |> Ash.load([:owner])
"#{owner.name}(#{owner.eve_id})"

View File

@@ -135,7 +135,7 @@ defmodule WandererApp.License.LicenseManagerClient do
Application.get_env(:wanderer_app, :license_manager)[:auth_key]
end
defp parse_error_response(status, %{"error" => error_message}) do
defp parse_error_response(_status, %{"error" => error_message}) do
{:error, error_message}
end

View File

@@ -7,6 +7,8 @@ defmodule WandererApp.Map do
require Logger
@map_state_cache :map_state_cache
defstruct map_id: nil,
name: nil,
scope: :none,
@@ -51,8 +53,8 @@ defmodule WandererApp.Map do
{:ok, map} ->
map
_ ->
Logger.error(fn -> "Failed to get map #{map_id}" end)
error ->
Logger.error("Failed to get map #{map_id}: #{inspect(error)}")
%{}
end
end
@@ -69,6 +71,50 @@ defmodule WandererApp.Map do
end)
end
def get_map_state(map_id, init_if_empty? \\ true) do
case Cachex.get(@map_state_cache, map_id) do
{:ok, nil} ->
case init_if_empty? do
true ->
map_state = WandererApp.Map.Server.Impl.do_init_state(map_id: map_id)
Cachex.put(@map_state_cache, map_id, map_state)
{:ok, map_state}
_ ->
{:ok, nil}
end
{:ok, map_state} ->
{:ok, map_state}
end
end
def get_map_state!(map_id) do
case get_map_state(map_id) do
{:ok, map_state} ->
map_state
_ ->
Logger.error("Failed to get map_state #{map_id}")
throw("Failed to get map_state #{map_id}")
end
end
def update_map_state(map_id, state_update),
do:
Cachex.get_and_update(@map_state_cache, map_id, fn map_state ->
case map_state do
nil ->
new_state = WandererApp.Map.Server.Impl.do_init_state(map_id: map_id)
{:commit, Map.merge(new_state, state_update)}
_ ->
{:commit, Map.merge(map_state, state_update)}
end
end)
def delete_map_state(map_id), do: Cachex.del(@map_state_cache, map_id)
def get_characters_limit(map_id),
do: {:ok, map_id |> get_map!() |> Map.get(:characters_limit, 50)}
@@ -137,9 +183,31 @@ defmodule WandererApp.Map do
def add_characters!(map, []), do: map
def add_characters!(%{map_id: map_id} = map, [character | rest]) do
add_character(map_id, character)
add_characters!(map, rest)
def add_characters!(%{map_id: map_id} = map, characters) when is_list(characters) do
# Get current characters list once
current_characters = Map.get(map, :characters, [])
characters_ids =
characters
|> Enum.map(fn %{id: char_id} -> char_id end)
# Filter out characters that already exist
new_character_ids =
characters_ids
|> Enum.reject(fn char_id -> char_id in current_characters end)
# If all characters already exist, return early
if new_character_ids == [] do
map
else
case update_map(map_id, %{characters: new_character_ids ++ current_characters}) do
{:commit, map} ->
map
_ ->
map
end
end
end
def add_character(
@@ -152,61 +220,10 @@ defmodule WandererApp.Map do
case not (characters |> Enum.member?(character_id)) do
true ->
WandererApp.Character.get_map_character(map_id, character_id)
|> case do
{:ok,
%{
alliance_id: alliance_id,
corporation_id: corporation_id,
solar_system_id: solar_system_id,
structure_id: structure_id,
station_id: station_id,
ship: ship_type_id,
ship_name: ship_name
}} ->
map_id
|> update_map(%{characters: [character_id | characters]})
map_id
|> update_map(%{characters: [character_id | characters]})
# WandererApp.Cache.insert(
# "map:#{map_id}:character:#{character_id}:alliance_id",
# alliance_id
# )
# WandererApp.Cache.insert(
# "map:#{map_id}:character:#{character_id}:corporation_id",
# corporation_id
# )
# WandererApp.Cache.insert(
# "map:#{map_id}:character:#{character_id}:solar_system_id",
# solar_system_id
# )
# WandererApp.Cache.insert(
# "map:#{map_id}:character:#{character_id}:structure_id",
# structure_id
# )
# WandererApp.Cache.insert(
# "map:#{map_id}:character:#{character_id}:station_id",
# station_id
# )
# WandererApp.Cache.insert(
# "map:#{map_id}:character:#{character_id}:ship_type_id",
# ship_type_id
# )
# WandererApp.Cache.insert(
# "map:#{map_id}:character:#{character_id}:ship_name",
# ship_name
# )
:ok
error ->
error
end
:ok
_ ->
{:error, :already_exists}
@@ -486,15 +503,16 @@ defmodule WandererApp.Map do
solar_system_source,
solar_system_target
) do
case map_id
|> get_map!()
|> Map.get(:connections, Map.new())
connections =
map_id
|> get_map!()
|> Map.get(:connections, Map.new())
case connections
|> Map.get("#{solar_system_source}_#{solar_system_target}") do
nil ->
{:ok,
map_id
|> get_map!()
|> Map.get(:connections, Map.new())
connections
|> Map.get("#{solar_system_target}_#{solar_system_source}")}
connection ->

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