mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-10 17:55:43 +00:00
Compare commits
189 Commits
migrations
...
v1.84.27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00bf620e35 | ||
|
|
46eef60d86 | ||
|
|
fe836442ab | ||
|
|
9514806dbb | ||
|
|
4e6423ebc8 | ||
|
|
a97e598299 | ||
|
|
9c26b50aac | ||
|
|
3f2ddf5cc4 | ||
|
|
233b2bd7a4 | ||
|
|
0d35268efc | ||
|
|
d169220eb2 | ||
|
|
182d5ec9fb | ||
|
|
32958253b7 | ||
|
|
c011d56ce7 | ||
|
|
73d1921d42 | ||
|
|
7bb810e1e6 | ||
|
|
c90ac7b1e3 | ||
|
|
005e0c2bc6 | ||
|
|
808acb540e | ||
|
|
06626f910b | ||
|
|
812582d955 | ||
|
|
f3077c0bf1 | ||
|
|
32c70cbbad | ||
|
|
8934935e10 | ||
|
|
20c8a53712 | ||
|
|
b22970fef3 | ||
|
|
cf72394ef9 | ||
|
|
e6dbba7283 | ||
|
|
843b3b86b2 | ||
|
|
bd865b9f64 | ||
|
|
ae91cd2f92 | ||
|
|
0be7a5f9d0 | ||
|
|
e15bfa426a | ||
|
|
4198e4b07a | ||
|
|
03ee08ff67 | ||
|
|
ac4dd4c28b | ||
|
|
308e81a464 | ||
|
|
6f4240d931 | ||
|
|
847b45a431 | ||
|
|
5ec97d74ca | ||
|
|
74359a5542 | ||
|
|
0020f46dd8 | ||
|
|
a6751b45c6 | ||
|
|
f48aeb5cec | ||
|
|
a5f25646c9 | ||
|
|
23cf1fd96f | ||
|
|
6f15521069 | ||
|
|
9d41e57c06 | ||
|
|
ea9a22df09 | ||
|
|
0d4fd6f214 | ||
|
|
87a6c20545 | ||
|
|
c375f4e4ce | ||
|
|
843a6d7320 | ||
|
|
98c54a3413 | ||
|
|
0439110938 | ||
|
|
8ce1e5fa3e | ||
|
|
ebaf6bcdc6 | ||
|
|
40d947bebc | ||
|
|
61d1c3848f | ||
|
|
e152ce179f | ||
|
|
7bbe387183 | ||
|
|
b1555ff03c | ||
|
|
e624499244 | ||
|
|
6a1976dec6 | ||
|
|
3db24c4344 | ||
|
|
883c09f255 | ||
|
|
ff24d80038 | ||
|
|
63cbc9c0b9 | ||
|
|
8056972a27 | ||
|
|
1759d46740 | ||
|
|
e4b7d2e45b | ||
|
|
41573cbee3 | ||
|
|
24ffc20bb8 | ||
|
|
e077849b66 | ||
|
|
375a9ef65b | ||
|
|
9bf90ab752 | ||
|
|
90c3481151 | ||
|
|
e36b08a7e5 | ||
|
|
e1f79170c3 | ||
|
|
68b5455e91 | ||
|
|
f28e75c7f4 | ||
|
|
6091adb28e | ||
|
|
d4657b335f | ||
|
|
7fee850902 | ||
|
|
648c168a66 | ||
|
|
f5c4b2c407 | ||
|
|
b592223d52 | ||
|
|
5cf118c6ee | ||
|
|
b25013c652 | ||
|
|
cf43861b11 | ||
|
|
b5fe8f8878 | ||
|
|
5e5068c7de | ||
|
|
624b51edfb | ||
|
|
a72f8e60c4 | ||
|
|
dec8ae50c9 | ||
|
|
0332d36a8e | ||
|
|
8444c7f82d | ||
|
|
ec3fc7447e | ||
|
|
20ec2800c9 | ||
|
|
6fbf43e860 | ||
|
|
697da38020 | ||
|
|
4bc65b43d2 | ||
|
|
910ec97fd1 | ||
|
|
40ed58ee8c | ||
|
|
c18d241c77 | ||
|
|
8b42908a5c | ||
|
|
6d32505a59 | ||
|
|
fe8a34c77d | ||
|
|
d12cafcca8 | ||
|
|
38a9c76ff0 | ||
|
|
d6c30b4a53 | ||
|
|
53a81daaf5 | ||
|
|
92081c99e3 | ||
|
|
d78020d2f5 | ||
|
|
fb1a9b440d | ||
|
|
0141ac46e3 | ||
|
|
d2bf6a8f86 | ||
|
|
1844e4c757 | ||
|
|
d407efe805 | ||
|
|
021e04d87a | ||
|
|
7844c9db34 | ||
|
|
355beb8394 | ||
|
|
d82eeba792 | ||
|
|
0396b05e58 | ||
|
|
9494a9eb37 | ||
|
|
8238f84ac7 | ||
|
|
1cf19b2a50 | ||
|
|
e8543fd2f8 | ||
|
|
c7f360e1fa | ||
|
|
a2b83f7f0c | ||
|
|
ae5689a403 | ||
|
|
c46af1d286 | ||
|
|
d17ba2168c | ||
|
|
80c14716eb | ||
|
|
8541fcd29b | ||
|
|
65d6acd7fb | ||
|
|
8b5f83d6b2 | ||
|
|
5e18891f4b | ||
|
|
74e0b85748 | ||
|
|
81d3495b65 | ||
|
|
d1959ca09f | ||
|
|
ec7a5ecf10 | ||
|
|
70b9ec99ba | ||
|
|
7147d79166 | ||
|
|
1dad9316bd | ||
|
|
872f7dcf48 | ||
|
|
02b450325e | ||
|
|
136bc4cbb9 | ||
|
|
dab49df9aa | ||
|
|
6286087f3e | ||
|
|
4ce7160f79 | ||
|
|
2913bf19b0 | ||
|
|
7bd6be6fd0 | ||
|
|
705daa286b | ||
|
|
614d06be66 | ||
|
|
dec3e9a7ce | ||
|
|
0017ac3373 | ||
|
|
ae34744578 | ||
|
|
76885058ef | ||
|
|
fccb007036 | ||
|
|
a9f8901bd5 | ||
|
|
8ae968b5be | ||
|
|
beffd45e4f | ||
|
|
4488d81e8d | ||
|
|
618cc8c5f1 | ||
|
|
3fb22a877e | ||
|
|
8759409b82 | ||
|
|
245647ae6a | ||
|
|
eb7d33ea07 | ||
|
|
3575b16def | ||
|
|
a6fb680be8 | ||
|
|
9e17df5544 | ||
|
|
683fde7be4 | ||
|
|
ee68ce92a2 | ||
|
|
8b4e38d795 | ||
|
|
4995202627 | ||
|
|
986b997a6a | ||
|
|
9a957af759 | ||
|
|
c5a0a96016 | ||
|
|
8715a6c0ac | ||
|
|
c9810095aa | ||
|
|
69eb888469 | ||
|
|
748347df9a | ||
|
|
aa4d49027c | ||
|
|
a9d7387e40 | ||
|
|
dc4d260c9b | ||
|
|
dc430491bf | ||
|
|
42cd261ea7 | ||
|
|
35af4fdc09 |
@@ -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>"
|
||||
|
||||
451
CHANGELOG.md
451
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
3
Makefile
3
Makefile
@@ -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
|
||||
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
31
assets/js/hooks/Mapper/ToastProvider.tsx
Normal file
31
assets/js/hooks/Mapper/ToastProvider.tsx
Normal 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;
|
||||
};
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 don’t 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 (
|
||||
<>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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'),
|
||||
[],
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 don’t 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 (
|
||||
<>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { UserPermission, UserPermissions } from '@/hooks/Mapper/types';
|
||||
|
||||
export const checkPermissions = (permissions: Partial<UserPermissions>, targetPermission: UserPermission) => {
|
||||
return targetPermission != null && permissions[targetPermission];
|
||||
};
|
||||
@@ -4,3 +4,4 @@ export * from './getSystemClassStyles';
|
||||
export * from './getShapeClass';
|
||||
export * from './getBackgroundClass';
|
||||
export * from './prepareUnsplashedChunks';
|
||||
export * from './checkPermissions';
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './parseSignatures';
|
||||
export * from './getSystemById';
|
||||
export * from './getEveImageUrl';
|
||||
export * from './toastHelpers';
|
||||
export * from './recenterSystems';
|
||||
|
||||
39
assets/js/hooks/Mapper/helpers/recenterSystems.ts
Normal file
39
assets/js/hooks/Mapper/helpers/recenterSystems.ts
Normal 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 };
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -33,7 +33,6 @@ export type CharacterTypeRaw = {
|
||||
corporation_id: number;
|
||||
corporation_name: string;
|
||||
corporation_ticker: string;
|
||||
tracking_paused: boolean;
|
||||
};
|
||||
|
||||
export interface TrackingCharacter {
|
||||
|
||||
@@ -9,3 +9,4 @@ export * from './connectionPassages';
|
||||
export * from './permissions';
|
||||
export * from './comment';
|
||||
export * from './ping';
|
||||
export * from './options';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
14
assets/js/hooks/Mapper/types/options.ts
Normal file
14
assets/js/hooks/Mapper/types/options.ts
Normal 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;
|
||||
};
|
||||
@@ -29,7 +29,7 @@ export type GroupType = {
|
||||
|
||||
export type SignatureCustomInfo = {
|
||||
k162Type?: string;
|
||||
isEOL?: boolean;
|
||||
time_status?: number;
|
||||
isCrit?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './getQueryVariable';
|
||||
export * from './loadTextFile';
|
||||
export * from './saveToFile';
|
||||
export * from './omit';
|
||||
export * from './jsonToUriBase64';
|
||||
|
||||
26
assets/js/hooks/Mapper/utils/jsonToUriBase64.ts
Normal file
26
assets/js/hooks/Mapper/utils/jsonToUriBase64.ts
Normal 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;
|
||||
};
|
||||
@@ -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],
|
||||
|
||||
BIN
assets/static/images/eo_pp.png
Normal file
BIN
assets/static/images/eo_pp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" => %{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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})"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user