Compare commits

...

194 Commits

Author SHA1 Message Date
DanSylvest
18d50329bc fix: Connection context menu and wormhole. Change UI for select wormhole mass state. Change UI for select ship-size for wormhole. Add ability to set mass for signatures 2026-03-10 15:35:24 +03:00
CI
d8fb980a3b chore: [skip ci] 2026-02-27 17:48:31 +00:00
CI
b8b3bc60ad chore: release version v1.96.5 2026-02-27 17:48:31 +00:00
Dmitry Popov
80d5dd1eb1 Merge branch 'main' of github.com:wanderer-industries/wanderer 2026-02-27 18:47:51 +01:00
Dmitry Popov
1ab0e96cbb fix(core): Fixed access token refresh issues 2026-02-27 18:46:56 +01:00
CI
e3a13b9554 chore: [skip ci] 2026-02-17 19:54:43 +00:00
CI
1f3387e4ff chore: release version v1.96.4 2026-02-17 19:54:43 +00:00
Dmitry Popov
95d2fa232a Merge pull request #596 from wanderer-industries/expired-characters
Expired characters
2026-02-17 20:54:11 +01:00
DanSylvest
eed1d8bc27 fix: Change character token validity status. Now we will see red frame and icon for tracked characters which token was expired. 2026-02-17 22:32:43 +03:00
Dmitry Popov
c451735559 chore: Updated character expired token handler 2026-02-17 16:19:36 +01:00
CI
aa586b7994 chore: [skip ci] 2026-02-15 10:07:08 +00:00
CI
39317831f9 chore: release version v1.96.3 2026-02-15 10:07:08 +00:00
Dmitry Popov
b71bc94d4f fix(tracking): Fixed character tracking issues 2026-02-15 11:06:35 +01:00
CI
0e920a58e6 chore: [skip ci] 2026-02-13 09:01:38 +00:00
CI
9385751332 chore: release version v1.96.2 2026-02-13 09:01:38 +00:00
Aleksei Chichenkov
ffaa48ff9e Merge pull request #593 from wanderer-industries/routes-by-icons
fix: Added icons for RoutesBy
2026-02-13 12:01:07 +03:00
DanSylvest
94665f4e68 fix: Added icons for RoutesBy 2026-02-13 11:57:18 +03:00
CI
e9fd0665c8 chore: [skip ci] 2026-02-12 16:05:16 +00:00
CI
9a0271f711 chore: release version v1.96.1 2026-02-12 16:05:16 +00:00
Dmitry Popov
0c68535656 Merge branch 'main' of github.com:wanderer-industries/wanderer 2026-02-12 17:04:19 +01:00
Dmitry Popov
9ed350befa chore: Added news for awards nomination 2026-02-12 17:04:15 +01:00
CI
c410f5f37d chore: [skip ci] 2026-02-12 15:16:33 +00:00
CI
8559be00f0 chore: release version v1.96.0 2026-02-12 15:16:33 +00:00
Dmitry Popov
1a24ee4c74 Merge branch 'develop' 2026-02-12 16:16:02 +01:00
Dmitry Popov
35ea4e5f1e feat(signatures): Fixed creator visibility issues. Added 4.5 hour color for unsplashed
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2026-02-12 16:15:44 +01:00
CI
de86703737 chore: [skip ci] 2026-02-11 17:12:09 +00:00
CI
c5af43dca1 chore: release version v1.95.0 2026-02-11 17:12:09 +00:00
Dmitry Popov
549fa1d2cf Merge pull request #592 from wanderer-industries/develop
Develop
2026-02-11 21:11:07 +04:00
Dmitry Popov
34a4d5dc9f feat(subscriptions): Added top map donators support
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2026-02-11 17:09:54 +01:00
Dmitry Popov
15142f188b Merge branch 'main' into develop 2026-02-11 10:35:32 +01:00
Dmitry Popov
daf4a81568 Merge pull request #586 from wanderer-industries/routes-by
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
Add Routes By widget. Allow to find nearest blue loot and red l…
2026-02-09 02:49:29 +04:00
DanSylvest
8c5340e911 chore: add placeholder for no found destinations 2026-02-08 21:47:04 +03:00
DanSylvest
6b0f636964 chore: removed unnecessary comments 2026-02-08 19:45:36 +03:00
DanSylvest
09ebd29eb4 feat: Added lost files 2026-02-08 19:28:29 +03:00
DanSylvest
35bd5645bf feat: Added paywall for RoutesBy widget 2026-02-08 19:25:50 +03:00
DanSylvest
a6948ee1da feat: removed unnecessary env variable for routes 2026-02-08 17:58:03 +03:00
Dmitry Popov
98b3f5855c Merge pull request #587 from wanderer-industries/multiple-structure-owners
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Multiple structure owners
2026-02-08 16:44:51 +04:00
CI
11ad48b40a chore: [skip ci] 2026-02-08 12:03:11 +00:00
CI
ecd018abfe chore: release version v1.94.0 2026-02-08 12:03:11 +00:00
Dmitry Popov
f430f74e98 feat(administration): Added registered characters admin view with cort/ally info, sort and filter options 2026-02-08 13:02:35 +01:00
CI
9e146d1117 chore: [skip ci] 2026-02-08 09:08:10 +00:00
CI
0a707fb423 chore: release version v1.93.0 2026-02-08 09:08:10 +00:00
Dmitry Popov
8cda76cc43 feat(subscriptions): Added an ability to withdraw from map to user balance 2026-02-08 10:04:03 +01:00
Dmitry Popov
2005e6f3dd fix(signatures): Fixed back linked sigs data sync and leading to system override issues 2026-02-07 17:18:29 +01:00
Dmitry Popov
ab066a342f Merge branch 'develop' into multiple-structure-owners 2026-02-07 15:18:25 +01:00
Dmitry Popov
82b4a5f35a fix(signatures): Moved C1/C2/C3 and C4/C5 to the bottom of the available list
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2026-02-07 14:39:34 +01:00
Dmitry Popov
ca3a25b836 Merge pull request #589 from guarzo/guarzo/ssecache
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
fix: use cache for sse
2026-02-07 04:20:13 +04:00
Guarzo
8e46c01a8a fix: use cache for sse 2026-02-06 22:48:13 +00:00
DanSylvest
9d9fa3c6b5 feat: Add systems with Security Status cleaning. Add trade hubs. Add ability to store data for this widget 2026-02-04 21:12:40 +03:00
Dmitry Popov
0e24501225 chore: Fixed review comments 2026-02-02 09:35:32 +01:00
DanSylvest
25a3d8951e feat: Add Routes By widget. Allow to find nearest blue loot and red loot stations. Added ability to set waypoint to station. 2026-01-31 12:29:25 +03:00
Dmitry Popov
f4ddc8dc8b Merge pull request #530 from s-no1ukno/main
feat(map): Update Owners on Multiple Structures
2026-01-29 19:37:27 +04:00
Dmitry Popov
ac9b46e24d Merge pull request #585 from guarzo/guarzo/addsysfromapi
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2026-01-27 12:21:35 +04:00
Guarzo
40d0a0777a fix: adding system when linked signature is provided 2026-01-27 03:10:33 +00:00
Dmitry Popov
608792d99a Merge pull request #584 from guarzo/guarzo/autoadd
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
feat: auto add system on sig addition
2026-01-26 22:45:57 +04:00
Guarzo
dc9e0c821e feat: auto add system on sig addition 2026-01-26 13:47:37 +00:00
Dmitry Popov
79d4fd0e43 Merge pull request #582 from guarzo/guarzo/evenmoredev
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
fix: saving updates to unknown sigs
2026-01-25 15:20:19 +04:00
Guarzo
5d03c1ecc7 fix: saving updates to unknown sigs 2026-01-25 01:50:14 +00:00
Dmitry Popov
2eef05495e Merge pull request #580 from guarzo/guarzo/moreapidev
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
fix: wh position and sig type change
2026-01-24 02:07:53 +04:00
Guarzo
f724455a1e fix: wh position and sig type change 2026-01-23 16:01:52 +00:00
Dmitry Popov
33bbb3425c Merge pull request #579 from guarzo/guarzo/apidev
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
fix: api updates and linked sig addition
2026-01-21 00:04:01 +04:00
Guarzo
a919bd9038 fix: api updates and linked sig addition 2026-01-20 17:55:30 +00:00
Dmitry Popov
8ae34cd94a Merge pull request #577 from guarzo/guarzo/apisigfixes
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
fix: api fixes and format
2026-01-16 16:06:34 +04:00
Guarzo
2f38da52e8 fix: api fixes and format 2026-01-16 08:39:19 +00:00
CI
89d7df0ba2 chore: [skip ci] 2026-01-14 22:29:39 +00:00
CI
ba0c10d2e4 chore: release version v1.92.0 2026-01-14 22:29:39 +00:00
Dmitry Popov
996c88d839 Merge pull request #575 from wanderer-industries/k162-selector
K162 selector
2026-01-15 02:29:09 +04:00
Dmitry Popov
80e998cf79 fix(core): Show c1/c2/c3 or c4/c5 or link signature modal 2026-01-14 23:28:47 +01:00
Dmitry Popov
d2bcb89fa1 Merge branch 'main' into k162-selector 2026-01-13 20:27:48 +01:00
CI
922f296f17 chore: [skip ci] 2026-01-13 00:16:39 +00:00
CI
71dc20c933 chore: release version v1.91.11 2026-01-13 00:16:39 +00:00
Dmitry Popov
80f7d34d3d Merge pull request #573 from guarzo/guarzo/maprelayreturn
fix: allow sig api when map relay is off
2026-01-13 04:16:06 +04:00
Guarzo
113fe1c695 fix: allow sig api when map relay is off 2026-01-12 23:59:20 +00:00
DanSylvest
5550844912 feat: Added ability to select a range of wh classes for k162. 2026-01-12 12:39:53 +03:00
CI
0228e68a1d chore: [skip ci] 2026-01-07 12:35:19 +00:00
CI
3424667af1 chore: release version v1.91.10 2026-01-07 12:35:19 +00:00
Dmitry Popov
6c7b28a6c1 Merge pull request #571 from guarzo/guarzo/sigapi2
fix: remove actor context requirement from sig api
2026-01-07 16:34:34 +04:00
Guarzo
3988079cd3 fix: remove actor context requirement from sig api 2026-01-07 04:24:15 +00:00
CI
f5d407fee0 chore: [skip ci] 2026-01-06 15:38:03 +00:00
CI
a857422c46 chore: release version v1.91.9 2026-01-06 15:38:02 +00:00
Dmitry Popov
ec6717d0ef Merge branch 'main' of github.com:wanderer-industries/wanderer 2026-01-06 16:37:32 +01:00
Dmitry Popov
56dacdcbbd fix(core): fixed rally point cancel logic 2026-01-06 16:37:29 +01:00
CI
c8e17b1691 chore: [skip ci] 2026-01-06 14:07:08 +00:00
CI
19c7fe59ee chore: release version v1.91.8 2026-01-06 14:07:08 +00:00
Dmitry Popov
682100c231 Merge branch 'main' of github.com:wanderer-industries/wanderer 2026-01-06 15:06:34 +01:00
Dmitry Popov
f9ac79cdcc fix(core): fixed rally point cancel logic 2026-01-06 15:06:31 +01:00
CI
f09f220645 chore: [skip ci] 2026-01-05 20:29:10 +00:00
CI
e585cdfd20 chore: release version v1.91.7 2026-01-05 20:29:10 +00:00
Dmitry Popov
3a3180f7b3 Merge branch 'main' of github.com:wanderer-industries/wanderer 2026-01-05 21:28:38 +01:00
Dmitry Popov
53abc580e5 chore: added promo on characters page 2026-01-05 21:28:35 +01:00
CI
8710d172a0 chore: [skip ci] 2026-01-04 23:49:15 +00:00
CI
301a380a4b chore: release version v1.91.6 2026-01-04 23:49:15 +00:00
Dmitry Popov
8c911f89e0 fix(core): fixed new connections got deleted after linked signature cleanup 2026-01-05 00:48:38 +01:00
CI
d7e09fc94e chore: [skip ci] 2025-12-30 10:49:35 +00:00
CI
3b7e191898 chore: release version v1.91.5 2025-12-30 10:49:35 +00:00
Dmitry Popov
f351fbaf20 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-30 11:49:02 +01:00
Dmitry Popov
016e793ba7 chore: Added 2026 roadmap blog post 2025-12-30 11:48:59 +01:00
CI
db483fd253 chore: [skip ci] 2025-12-30 09:27:37 +00:00
CI
911ba231cd chore: release version v1.91.4 2025-12-30 09:27:37 +00:00
Dmitry Popov
b3053f325d Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-30 10:27:06 +01:00
Dmitry Popov
4ab47334fc fix(core): fixed connections create between k-space systems (considered as wh connection) 2025-12-30 10:27:03 +01:00
CI
e163f02526 chore: [skip ci] 2025-12-28 17:02:12 +00:00
CI
9e22dba8f1 chore: release version v1.91.3 2025-12-28 17:02:12 +00:00
Dmitry Popov
9631406def Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-28 18:01:43 +01:00
Dmitry Popov
f6ae448c3b chore: update event post 2025-12-28 18:01:39 +01:00
CI
46345ef596 chore: [skip ci] 2025-12-27 22:11:03 +00:00
CI
1625f16c8f chore: release version v1.91.2 2025-12-27 22:11:03 +00:00
Dmitry Popov
b4ef9ae983 fix(core): fixed map scopes updates & logic 2025-12-27 23:10:26 +01:00
CI
3b9c2dd996 chore: [skip ci] 2025-12-25 18:20:20 +00:00
CI
8a0f9a58d0 chore: release version v1.91.1 2025-12-25 18:20:20 +00:00
Dmitry Popov
5fe8caac0d Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-25 19:19:47 +01:00
Dmitry Popov
f18f567727 chore: fix blog link styles 2025-12-25 19:19:44 +01:00
CI
91acc49980 chore: [skip ci] 2025-12-24 15:09:40 +00:00
CI
ae3873a225 chore: release version v1.91.0 2025-12-24 15:09:40 +00:00
Dmitry Popov
b351c6cc26 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-24 16:09:06 +01:00
Dmitry Popov
698244d945 feat(admin): added maps administration view with basic info, search, restore/delete, acls view and edit options 2025-12-24 16:09:03 +01:00
CI
2c7dd9dc5b chore: [skip ci] 2025-12-19 12:33:26 +00:00
CI
36934cce0b chore: release version v1.90.13 2025-12-19 12:33:26 +00:00
Dmitry Popov
b7da7e4ecb Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-19 13:32:46 +01:00
Dmitry Popov
6471ea5590 fix(core): fixed welcome page 2025-12-19 13:32:44 +01:00
CI
b46bcac642 chore: [skip ci] 2025-12-19 09:38:36 +00:00
CI
52d90361e9 chore: release version v1.90.12 2025-12-19 09:38:36 +00:00
Dmitry Popov
1c902d3319 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-19 10:38:02 +01:00
Dmitry Popov
8f671a359b fix(core): fixed permissions update after character corp updates 2025-12-19 10:37:59 +01:00
CI
840c416684 chore: [skip ci] 2025-12-18 21:47:59 +00:00
CI
56e29ad30a chore: release version v1.90.11 2025-12-18 21:47:59 +00:00
Dmitry Popov
cd8f8b5801 chore: added promo codes support for map subs 2025-12-18 22:19:50 +01:00
Dmitry Popov
70e013fa3d Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-18 22:19:38 +01:00
Dmitry Popov
d6bfaf8008 chore: added promo codes support for map subs 2025-12-18 22:19:26 +01:00
CI
95944199a0 chore: [skip ci] 2025-12-18 18:05:48 +00:00
CI
3bd5db8cf3 chore: release version v1.90.10 2025-12-18 18:05:48 +00:00
Dmitry Popov
a245330ada Merge branch 'advent-challenge' 2025-12-18 19:05:10 +01:00
Dmitry Popov
1226b6abf3 chore: added advent challenge 2025-12-18 19:04:43 +01:00
Dmitry Popov
7a1f5c0966 chore: [skip ci] 2025-12-17 19:32:37 +01:00
CI
e5afa1d5bc chore: [skip ci] 2025-12-15 11:46:40 +00:00
CI
1473fe8646 chore: release version v1.90.9 2025-12-15 11:46:40 +00:00
Dmitry Popov
7039ced11e fix(core): reduce chracters untrack grace period to 15 mins (after change/close/disconnect from map)
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-12-15 12:46:02 +01:00
CI
42b5bb337f chore: [skip ci] 2025-12-15 11:35:24 +00:00
CI
1dbb24f6ec chore: release version v1.90.8 2025-12-15 11:35:24 +00:00
Dmitry Popov
c242f510e0 fix(core): skip systems or connections cleanup for not started maps 2025-12-15 12:34:55 +01:00
CI
c59d51636e chore: [skip ci] 2025-12-15 00:36:18 +00:00
CI
c5a8aa1b4d chore: release version v1.90.7 2025-12-15 00:36:18 +00:00
Dmitry Popov
cba050a9e7 fix(core): fixed scopes 2025-12-15 01:35:41 +01:00
CI
59fcbef3b1 chore: [skip ci] 2025-12-12 18:49:02 +00:00
CI
2f1eb6eeaa chore: release version v1.90.6 2025-12-12 18:49:02 +00:00
Dmitry Popov
71ae326cf7 fix(core): fixed map scopes 2025-12-12 19:48:26 +01:00
CI
07829caf0f chore: [skip ci] 2025-12-12 18:36:03 +00:00
CI
a5850b5a8d chore: release version v1.90.5 2025-12-12 18:36:03 +00:00
Dmitry Popov
9f6849209b fix(core): fixed map scopes 2025-12-12 19:35:26 +01:00
CI
7bd295cbad chore: [skip ci] 2025-12-12 17:07:55 +00:00
CI
078e5fc19e chore: release version v1.90.4 2025-12-12 17:07:55 +00:00
Dmitry Popov
3877e121c3 fix(core): fixed map scopes & signatures clean up behaviour 2025-12-12 18:07:18 +01:00
CI
dcb2a0cdb2 chore: [skip ci] 2025-12-11 00:17:06 +00:00
CI
f5294eee84 chore: release version v1.90.3 2025-12-11 00:17:06 +00:00
Dmitry Popov
a5c87b6fa4 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-11 01:16:27 +01:00
Dmitry Popov
eae275f515 fix(core): added pagination for long ACL lists 2025-12-11 01:16:24 +01:00
CI
68ae6706dd chore: [skip ci] 2025-12-10 23:56:28 +00:00
CI
a34b30af15 chore: release version v1.90.2 2025-12-10 23:56:28 +00:00
Dmitry Popov
38b49266ed fix(core): added system position updates to SSE
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-12-11 00:55:52 +01:00
CI
049884bb4c chore: [skip ci] 2025-12-08 21:56:20 +00:00
CI
3c75b2b59f chore: release version v1.90.1 2025-12-08 21:56:20 +00:00
Dmitry Popov
4ad5d191a3 fix(core): fixed connections and signatures remove issues, added comprehensive audit log for auto removed connections and signatures 2025-12-08 22:55:39 +01:00
CI
2499c24cc1 chore: [skip ci] 2025-12-06 10:58:14 +00:00
CI
6f0043205c chore: release version v1.90.0 2025-12-06 10:58:14 +00:00
Dmitry Popov
597741fa60 Merge pull request #567 from wanderer-industries/develop
Develop
2025-12-06 14:57:27 +04:00
Dmitry Popov
d313ae8cd2 fix(core): fixed clean up for linked signatures
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-12-04 11:33:42 +01:00
Dmitry Popov
06d5d8072e fix(core): fixed issue with default select mode
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-12-03 12:21:40 +01:00
CI
f2d112df5c chore: [skip ci] 2025-12-02 23:44:54 +00:00
CI
716604fa84 chore: release version v1.89.6 2025-12-02 23:44:54 +00:00
Dmitry Popov
cae958a1e6 Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-12-03 00:44:31 +01:00
Dmitry Popov
283b36c882 fix(kills): fixed zkb links (added "allow-popups-to-escape-sandbox" to CSP) 2025-12-03 00:44:23 +01:00
Dmitry Popov
051e71f1a6 Merge pull request #566 from guarzo/guarzo/sigapi
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
fix: apiV1 default fields updates
2025-12-03 00:20:13 +04:00
Guarzo
20a50e8db0 fix: apiV1 default fields updates 2025-12-02 17:55:05 +00:00
CI
79d7f7ce7d chore: [skip ci] 2025-12-02 12:46:26 +00:00
CI
6c4b65c446 chore: release version v1.89.5 2025-12-02 12:46:26 +00:00
Dmitry Popov
2b07af5e12 Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-12-02 13:45:57 +01:00
Dmitry Popov
d0901eecb4 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-02 13:45:51 +01:00
Dmitry Popov
ee85d29c54 chore: added tests 2025-12-02 13:45:47 +01:00
Dmitry Popov
a237d6513d Merge branch 'main' into develop 2025-12-02 13:37:13 +01:00
CI
02979588c1 chore: [skip ci] 2025-12-02 12:35:31 +00:00
CI
3abe40855f chore: release version v1.89.4 2025-12-02 12:35:30 +00:00
Dmitry Popov
d0d9418a89 fix(core): fixed acl character update issues 2025-12-02 13:34:55 +01:00
CI
3ce742eb01 chore: [skip ci] 2025-11-30 22:26:08 +00:00
Dmitry Popov
fa32c62f63 Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-11-30 23:25:48 +01:00
Dmitry Popov
2cb2dc526c Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-11-30 18:51:58 +01:00
Dmitry Popov
c3de3c4e35 Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-11-29 20:17:14 +01:00
Dmitry Popov
4585c3a94b feat(core): Added several map scopes support (Wh, Hi, Low, Null, Pochven) 2025-11-29 14:36:45 +01:00
Dmitry Popov
46a1898be9 Merge branch 'fixed-warinings' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-11-29 12:35:36 +01:00
Dmitry Popov
e7219e0eec chore: fixed compile warnings 2025-11-29 12:34:28 +01:00
Jordan Snow
a7d6b06332 feat(map): Reviewed changes
Adding the changes from first review of PR #530. This includes cleanup,
wrapping callbacks in a `useCallback()` hook, and inclusion of clsx
wrapper for styling.
2025-10-23 22:06:42 -06:00
Jordan Snow
8f6da817db Fix: Wrong file added to commits
This file should not have been added to previous commits, and was only
changed to allow for a fix in my local dev environment.
2025-10-19 12:26:28 -06:00
Jordan Snow
378f22a1ef feat(map): Logic for multiple owner updates
Finished all the logic for updating owners on multiple structures in a
single system.
2025-10-18 21:43:44 -06:00
Jordan Snow
14730097b2 feat(map) Adding all the things to the modal
Added a bunch of text and formatting to the system structures owners
dialog box
2025-10-18 20:26:28 -06:00
Jordan Snow
e8bff3098a feat(map): wip New Dialog for Structure Owners
Added the new modal to be able to update all structures within a system
in a single update.
2025-10-18 19:24:19 -06:00
246 changed files with 13154 additions and 1066 deletions

View File

@@ -16,3 +16,8 @@ export WANDERER_SSE_ENABLED="true"
export WANDERER_WEBHOOKS_ENABLED="true"
export WANDERER_SSE_MAX_CONNECTIONS="1000"
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
# Promo codes for map subscriptions (optional)
# Format: CODE:DISCOUNT_PERCENT,CODE2:DISCOUNT_PERCENT2
# Codes are case-insensitive, discounts stack with period discounts
# export WANDERER_PROMO_CODES="PROMO2025:10,NEWUSER:20"

3
.gitignore vendored
View File

@@ -17,6 +17,9 @@ repomix*
/priv/static/images/
/priv/static/*.js
/priv/static/*.css
/priv/static/*-*.png
/priv/static/*-*.webp
/priv/static/*-*.webmanifest
# Dialyzer PLT files
/priv/plts/

View File

@@ -2,6 +2,375 @@
<!-- changelog -->
## [v1.96.5](https://github.com/wanderer-industries/wanderer/compare/v1.96.4...v1.96.5) (2026-02-27)
### Bug Fixes:
* core: Fixed access token refresh issues
## [v1.96.4](https://github.com/wanderer-industries/wanderer/compare/v1.96.3...v1.96.4) (2026-02-17)
### Bug Fixes:
* Change character token validity status. Now we will see red frame and icon for tracked characters which token was expired.
## [v1.96.3](https://github.com/wanderer-industries/wanderer/compare/v1.96.2...v1.96.3) (2026-02-15)
### Bug Fixes:
* tracking: Fixed character tracking issues
## [v1.96.2](https://github.com/wanderer-industries/wanderer/compare/v1.96.1...v1.96.2) (2026-02-13)
### Bug Fixes:
* Added icons for RoutesBy
## [v1.96.1](https://github.com/wanderer-industries/wanderer/compare/v1.96.0...v1.96.1) (2026-02-12)
## [v1.96.0](https://github.com/wanderer-industries/wanderer/compare/v1.95.0...v1.96.0) (2026-02-12)
### Features:
* signatures: Fixed creator visibility issues. Added 4.5 hour color for unsplashed
## [v1.95.0](https://github.com/wanderer-industries/wanderer/compare/v1.94.0...v1.95.0) (2026-02-11)
### Features:
* subscriptions: Added top map donators support
* Added lost files
* Added paywall for RoutesBy widget
* removed unnecessary env variable for routes
* Add systems with Security Status cleaning. Add trade hubs. Add ability to store data for this widget
* Add Routes By widget. Allow to find nearest blue loot and red loot stations. Added ability to set waypoint to station.
* auto add system on sig addition
* map: Reviewed changes
* map: Logic for multiple owner updates
* map: wip New Dialog for Structure Owners
### Bug Fixes:
* signatures: Fixed back linked sigs data sync and leading to system override issues
* signatures: Moved C1/C2/C3 and C4/C5 to the bottom of the available list
* use cache for sse
* adding system when linked signature is provided
* saving updates to unknown sigs
* wh position and sig type change
* api updates and linked sig addition
* api fixes and format
* Wrong file added to commits
## [v1.94.0](https://github.com/wanderer-industries/wanderer/compare/v1.93.0...v1.94.0) (2026-02-08)
### Features:
* administration: Added registered characters admin view with cort/ally info, sort and filter options
## [v1.93.0](https://github.com/wanderer-industries/wanderer/compare/v1.92.0...v1.93.0) (2026-02-08)
### Features:
* subscriptions: Added an ability to withdraw from map to user balance
## [v1.92.0](https://github.com/wanderer-industries/wanderer/compare/v1.91.11...v1.92.0) (2026-01-14)
### Features:
* Added ability to select a range of wh classes for k162.
### Bug Fixes:
* core: Show c1/c2/c3 or c4/c5 or link signature modal
## [v1.91.11](https://github.com/wanderer-industries/wanderer/compare/v1.91.10...v1.91.11) (2026-01-13)
### Bug Fixes:
* allow sig api when map relay is off
## [v1.91.10](https://github.com/wanderer-industries/wanderer/compare/v1.91.9...v1.91.10) (2026-01-07)
### Bug Fixes:
* remove actor context requirement from sig api
## [v1.91.9](https://github.com/wanderer-industries/wanderer/compare/v1.91.8...v1.91.9) (2026-01-06)
### Bug Fixes:
* core: fixed rally point cancel logic
## [v1.91.8](https://github.com/wanderer-industries/wanderer/compare/v1.91.7...v1.91.8) (2026-01-06)
### Bug Fixes:
* core: fixed rally point cancel logic
## [v1.91.7](https://github.com/wanderer-industries/wanderer/compare/v1.91.6...v1.91.7) (2026-01-05)
## [v1.91.6](https://github.com/wanderer-industries/wanderer/compare/v1.91.5...v1.91.6) (2026-01-04)
### Bug Fixes:
* core: fixed new connections got deleted after linked signature cleanup
## [v1.91.5](https://github.com/wanderer-industries/wanderer/compare/v1.91.4...v1.91.5) (2025-12-30)
## [v1.91.4](https://github.com/wanderer-industries/wanderer/compare/v1.91.3...v1.91.4) (2025-12-30)
### Bug Fixes:
* core: fixed connections create between k-space systems (considered as wh connection)
## [v1.91.3](https://github.com/wanderer-industries/wanderer/compare/v1.91.2...v1.91.3) (2025-12-28)
## [v1.91.2](https://github.com/wanderer-industries/wanderer/compare/v1.91.1...v1.91.2) (2025-12-27)
### Bug Fixes:
* core: fixed map scopes updates & logic
## [v1.91.1](https://github.com/wanderer-industries/wanderer/compare/v1.91.0...v1.91.1) (2025-12-25)
## [v1.91.0](https://github.com/wanderer-industries/wanderer/compare/v1.90.13...v1.91.0) (2025-12-24)
### Features:
* admin: added maps administration view with basic info, search, restore/delete, acls view and edit options
## [v1.90.13](https://github.com/wanderer-industries/wanderer/compare/v1.90.12...v1.90.13) (2025-12-19)
### Bug Fixes:
* core: fixed welcome page
## [v1.90.12](https://github.com/wanderer-industries/wanderer/compare/v1.90.11...v1.90.12) (2025-12-19)
### Bug Fixes:
* core: fixed permissions update after character corp updates
## [v1.90.11](https://github.com/wanderer-industries/wanderer/compare/v1.90.10...v1.90.11) (2025-12-18)
## [v1.90.10](https://github.com/wanderer-industries/wanderer/compare/v1.90.9...v1.90.10) (2025-12-18)
## [v1.90.9](https://github.com/wanderer-industries/wanderer/compare/v1.90.8...v1.90.9) (2025-12-15)
### Bug Fixes:
* core: reduce chracters untrack grace period to 15 mins (after change/close/disconnect from map)
## [v1.90.8](https://github.com/wanderer-industries/wanderer/compare/v1.90.7...v1.90.8) (2025-12-15)
### Bug Fixes:
* core: skip systems or connections cleanup for not started maps
## [v1.90.7](https://github.com/wanderer-industries/wanderer/compare/v1.90.6...v1.90.7) (2025-12-15)
### Bug Fixes:
* core: fixed scopes
## [v1.90.6](https://github.com/wanderer-industries/wanderer/compare/v1.90.5...v1.90.6) (2025-12-12)
### Bug Fixes:
* core: fixed map scopes
## [v1.90.5](https://github.com/wanderer-industries/wanderer/compare/v1.90.4...v1.90.5) (2025-12-12)
### Bug Fixes:
* core: fixed map scopes
## [v1.90.4](https://github.com/wanderer-industries/wanderer/compare/v1.90.3...v1.90.4) (2025-12-12)
### Bug Fixes:
* core: fixed map scopes & signatures clean up behaviour
## [v1.90.3](https://github.com/wanderer-industries/wanderer/compare/v1.90.2...v1.90.3) (2025-12-11)
### Bug Fixes:
* core: added pagination for long ACL lists
## [v1.90.2](https://github.com/wanderer-industries/wanderer/compare/v1.90.1...v1.90.2) (2025-12-10)
### Bug Fixes:
* core: added system position updates to SSE
## [v1.90.1](https://github.com/wanderer-industries/wanderer/compare/v1.90.0...v1.90.1) (2025-12-08)
### Bug Fixes:
* core: fixed connections and signatures remove issues, added comprehensive audit log for auto removed connections and signatures
## [v1.90.0](https://github.com/wanderer-industries/wanderer/compare/v1.89.6...v1.90.0) (2025-12-06)
### Features:
* core: Added several map scopes support (Wh, Hi, Low, Null, Pochven)
### Bug Fixes:
* core: fixed clean up for linked signatures
* core: fixed issue with default select mode
* apiV1 default fields updates
## [v1.89.6](https://github.com/wanderer-industries/wanderer/compare/v1.89.5...v1.89.6) (2025-12-02)
### Bug Fixes:
* kills: fixed zkb links (added "allow-popups-to-escape-sandbox" to CSP)
## [v1.89.5](https://github.com/wanderer-industries/wanderer/compare/v1.89.4...v1.89.5) (2025-12-02)
## [v1.89.4](https://github.com/wanderer-industries/wanderer/compare/v1.89.3...v1.89.4) (2025-12-02)
### Bug Fixes:
* core: fixed acl character update issues
## [v1.89.3](https://github.com/wanderer-industries/wanderer/compare/v1.89.2...v1.89.3) (2025-11-30)

View File

@@ -32,6 +32,56 @@ format f:
test t:
MIX_ENV=test mix test
# Run tests in 4 parallel partitions (useful for CI or faster local runs)
test-parallel tp:
@echo "Running tests in 4 parallel partitions..."
@mkdir -p /tmp/wanderer_test_results
@rm -f /tmp/wanderer_test_results/partition_*.txt /tmp/wanderer_test_results/exit_*.txt
@for i in 1 2 3 4; do \
(MIX_TEST_PARTITION=$$i MIX_ENV=test mix test --partitions 4 2>&1; echo $$? > /tmp/wanderer_test_results/exit_$$i.txt) | \
tee /tmp/wanderer_test_results/partition_$$i.txt | sed "s/^/[P$$i] /" & \
done; \
wait
@echo ""
@echo "========================================"
@echo " TEST RESULTS SUMMARY"
@echo "========================================"
@total_tests=0; total_failures=0; total_excluded=0; all_passed=true; \
for i in 1 2 3 4; do \
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
if [ "$$exit_code" != "0" ]; then all_passed=false; fi; \
summary=$$(grep -E "^[0-9]+ (tests?|doctest)" /tmp/wanderer_test_results/partition_$$i.txt | tail -1 || echo "No results"); \
tests=$$(echo "$$summary" | grep -oE "^[0-9]+" || echo "0"); \
failures=$$(echo "$$summary" | grep -oE "[0-9]+ failures?" | grep -oE "^[0-9]+" || echo "0"); \
excluded=$$(echo "$$summary" | grep -oE "[0-9]+ excluded" | grep -oE "^[0-9]+" || echo "0"); \
total_tests=$$((total_tests + tests)); \
total_failures=$$((total_failures + failures)); \
total_excluded=$$((total_excluded + excluded)); \
if [ "$$exit_code" = "0" ]; then \
echo "Partition $$i: ✓ $$summary"; \
else \
echo "Partition $$i: ✗ $$summary (exit code: $$exit_code)"; \
fi; \
done; \
echo "========================================"; \
echo "TOTAL: $$total_tests tests, $$total_failures failures, $$total_excluded excluded"; \
echo "========================================"; \
if [ "$$all_passed" = "true" ]; then \
echo "✓ All partitions passed!"; \
else \
echo "✗ Some partitions failed. Details below:"; \
echo ""; \
for i in 1 2 3 4; do \
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
if [ "$$exit_code" != "0" ]; then \
echo "======== PARTITION $$i FAILURES ========"; \
grep -A 50 "Failures:" /tmp/wanderer_test_results/partition_$$i.txt 2>/dev/null || cat /tmp/wanderer_test_results/partition_$$i.txt; \
echo ""; \
fi; \
done; \
exit 1; \
fi
coverage cover co:
MIX_ENV=test mix test --cover

View File

@@ -1001,3 +1001,27 @@ body > div:first-of-type {
.verticalTabsContainer .p-tabview-panel {
flex-grow: 1;
}
/* Blog post CTA links - only in main post content */
.post-content a {
display: inline-block;
background: linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);
color: white !important;
padding: 0.5rem 1.25rem;
border-radius: 0.5rem;
text-decoration: none !important;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(236, 72, 153, 0.3);
}
.post-content a:hover {
background: linear-gradient(135deg, #db2777 0%, #7c3aed 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(236, 72, 153, 0.4);
}
.post-content a:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(236, 72, 153, 0.3);
}

View File

@@ -1,13 +1,18 @@
import { emitMapEvent } from '@/hooks/Mapper/events';
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { useCallback } from 'react';
import classes from './Characters.module.scss';
import {
TooltipPosition,
WdEveEntityPortrait,
WdEveEntityPortraitSize,
WdTooltipWrapper,
} from '@/hooks/Mapper/components/ui-kit';
import { WdCharStateWrapper } from '@/hooks/Mapper/components/characters/components/WdCharStateWrapper.tsx';
interface CharactersProps {
data: CharacterTypeRaw[];
}
@@ -17,7 +22,7 @@ export const Characters = ({ data }: CharactersProps) => {
const {
outCommand,
data: { mainCharacterEveId, followingCharacterEveId },
data: { mainCharacterEveId, followingCharacterEveId, expiredCharacters },
} = useMapRootState();
const handleSelect = useCallback(async (character: CharacterTypeRaw) => {
@@ -35,61 +40,48 @@ export const Characters = ({ data }: CharactersProps) => {
});
}, []);
const items = data.map(character => (
<li
key={character.eve_id}
className="flex flex-col items-center justify-center"
onClick={() => handleSelect(character)}
>
<div
className={clsx(
'overflow-hidden relative',
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
'transition-colors duration-250 hover:bg-stone-300/90',
{
['border-stone-800/90']: !character.online,
['border-lime-600/70']: character.online,
},
)}
title={character.name}
>
{mainCharacterEveId === character.eve_id && (
<span
className={clsx(
'absolute top-[2px] left-[22px] w-[9px] h-[9px]',
'text-yellow-500 text-[9px] rounded-[1px] z-10',
'pi',
PrimeIcons.STAR_FILL,
)}
/>
)}
const items = data.map(character => {
const isExpired = expiredCharacters.includes(character.eve_id);
{followingCharacterEveId === character.eve_id && (
<span
className={clsx(
'absolute top-[23px] left-[22px] w-[10px] h-[10px]',
'text-sky-300 text-[10px] rounded-[1px] z-10',
'pi pi-angle-double-right',
)}
/>
)}
{isDocked(character.location) && <div className={classes.Docked} />}
<div
className={clsx(
'flex w-full h-full bg-transparent cursor-pointer',
'bg-center bg-no-repeat bg-[length:100%]',
'transition-opacity',
'shadow-[inset_0_1px_6px_1px_#000000]',
{
['opacity-60']: !character.online,
['opacity-100']: character.online,
},
)}
style={{ backgroundImage: `url(https://images.evetech.net/characters/${character.eve_id}/portrait)` }}
></div>
</div>
</li>
));
return (
<li
key={character.eve_id}
className="flex flex-col items-center justify-center"
onClick={() => handleSelect(character)}
>
<WdTooltipWrapper
position={TooltipPosition.bottom}
content={isExpired ? `Token is expired for ${character.name}` : character.name}
>
<WdCharStateWrapper
eve_id={character.eve_id}
location={character.location}
isExpired={isExpired}
isMain={mainCharacterEveId === character.eve_id}
isFollowing={followingCharacterEveId === character.eve_id}
isOnline={character.online}
>
<WdEveEntityPortrait
eveId={character.eve_id}
size={WdEveEntityPortraitSize.w33}
className={clsx(
'flex w-full h-full bg-transparent cursor-pointer',
'bg-center bg-no-repeat bg-[length:100%]',
'transition-opacity',
'shadow-[inset_0_1px_6px_1px_#000000]',
{
['opacity-60']: !isExpired && !character.online,
['opacity-100']: !isExpired && character.online,
['opacity-50']: isExpired,
},
'!border-0',
)}
/>
</WdCharStateWrapper>
</WdTooltipWrapper>
</li>
);
});
return (
<ul className="flex gap-1 characters" id="characters" ref={parent}>

View File

@@ -0,0 +1,71 @@
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
import classes from './WdCharStateWrapper.module.scss';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
import { LocationRaw } from '@/hooks/Mapper/types';
type WdCharStateWrapperProps = {
eve_id: string;
isExpired?: boolean;
isMain?: boolean;
isFollowing?: boolean;
location: LocationRaw | null;
isOnline: boolean;
} & WithChildren;
export const WdCharStateWrapper = ({
location,
isOnline,
isMain,
isFollowing,
isExpired,
children,
}: WdCharStateWrapperProps) => {
return (
<div
className={clsx(
'overflow-hidden relative',
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
'transition-colors duration-250 hover:bg-stone-300/90',
{
['border-stone-800/90']: !isExpired && !isOnline,
['border-lime-600/70']: !isExpired && isOnline,
['border-red-600/70']: isExpired,
},
)}
>
{isMain && (
<span
className={clsx(
'absolute top-[2px] left-[22px] w-[9px] h-[9px]',
'text-yellow-500 text-[9px] rounded-[1px] z-10',
'pi',
PrimeIcons.STAR_FILL,
)}
/>
)}
{isFollowing && (
<span
className={clsx(
'absolute top-[23px] left-[22px] w-[10px] h-[10px]',
'text-sky-300 text-[10px] rounded-[1px] z-10',
'pi pi-angle-double-right',
)}
/>
)}
{isDocked(location) && <div className={classes.Docked} />}
{isExpired && (
<span
className={clsx(
'absolute top-[4px] left-[4px] w-[10px] h-[10px]',
'text-red-400 text-[10px] rounded-[1px] z-10',
'pi pi-exclamation-triangle',
)}
/>
)}
{children}
</div>
);
};

View File

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

View File

@@ -8,3 +8,15 @@
}
}
}
.ContextMenu {
width: max-content;
min-width: unset;
:global {
.p-submenu-list {
width: max-content;
min-width: unset !important;
}
}
}

View File

@@ -1,21 +1,23 @@
import React, { RefObject, useMemo } from 'react';
import React, { RefObject, useCallback, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
import { CharacterTypeRaw, SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
import classes from './ContextMenuSystemInfo.module.scss';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components';
import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks';
import { Route } from '@/hooks/Mapper/types/routes.ts';
import { Route, RouteStationSummary } from '@/hooks/Mapper/types/routes.ts';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
import { useGetOwnOnlineCharacters } from '@/hooks/Mapper/components/hooks/useGetOwnOnlineCharacters.ts';
import { sortStationsByDistance } from './sortStationsByDistance.ts';
export interface ContextMenuSystemInfoProps {
systemStatics: Map<number, SolarSystemStaticInfoRaw>;
hubs: string[];
contextMenuRef: RefObject<ContextMenu>;
systemId: string | undefined;
systemIdFrom?: string | undefined;
@@ -37,11 +39,106 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
onWaypointSet,
systemId,
systemIdFrom,
hubs,
routes,
}) => {
const getWaypointMenu = useWaypointMenu(onWaypointSet);
const getJumpPlannerMenu = useJumpPlannerMenu(systems, systemIdFrom);
const { toggleHubCommand, hubs } = useRouteProvider();
const getOwnOnlineCharacters = useGetOwnOnlineCharacters();
const getStationWaypointItems = useCallback(
(destinationId: string, chars: CharacterTypeRaw[]): MenuItem[] => [
{
label: 'Set Destination',
icon: PrimeIcons.SEND,
command: () => {
onWaypointSet({
fromBeginning: true,
clearWay: true,
destination: destinationId,
charIds: chars.map(char => char.eve_id),
});
},
},
{
label: 'Add Waypoint',
icon: PrimeIcons.DIRECTIONS_ALT,
command: () => {
onWaypointSet({
fromBeginning: false,
clearWay: false,
destination: destinationId,
charIds: chars.map(char => char.eve_id),
});
},
},
{
label: 'Add Waypoint Front',
icon: PrimeIcons.DIRECTIONS,
command: () => {
onWaypointSet({
fromBeginning: true,
clearWay: false,
destination: destinationId,
charIds: chars.map(char => char.eve_id),
});
},
},
],
[onWaypointSet],
);
const getStationsMenu = useCallback(
(stations: RouteStationSummary[]) => {
const chars = getOwnOnlineCharacters().filter(x => x.online);
const sortedStations = sortStationsByDistance(stations);
return [
{
label: 'Stations',
icon: PrimeIcons.MAP_MARKER,
items: sortedStations.map(station => {
const destinationId = station.station_id.toString();
const specialClass = station.special ? '[&_.p-menuitem-text]:text-orange-400' : '';
if (chars.length === 0) {
return {
label: station.station_name,
className: specialClass || undefined,
items: [{ label: 'No online characters', disabled: true }],
};
}
if (chars.length === 1) {
return {
label: station.station_name,
className: specialClass || undefined,
items: getStationWaypointItems(destinationId, chars.slice(0, 1)),
};
}
return {
label: station.station_name,
className: `${specialClass} w-[500px]`.trim(),
items: [
{
label: 'All',
icon: PrimeIcons.USERS,
items: getStationWaypointItems(destinationId, chars),
},
...chars.map(char => ({
label: char.name,
icon: PrimeIcons.USER,
items: getStationWaypointItems(destinationId, [char]),
})),
],
};
}),
},
];
},
[getOwnOnlineCharacters, getStationWaypointItems],
);
const items: MenuItem[] = useMemo(() => {
const system = systemId ? systemStatics.get(parseInt(systemId)) : undefined;
@@ -50,6 +147,10 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
if (!systemId || !system) {
return [];
}
const route = routes.find(x => x.destination?.toString() === systemId);
const stationItems = route?.stations?.length ? getStationsMenu(route.stations) : [];
return [
{
className: classes.FastActions,
@@ -69,15 +170,20 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
{ separator: true },
...getJumpPlannerMenu(system, routes),
...getWaypointMenu(systemId, system.system_class),
{
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
icon: !hubs.includes(systemId) ? (
<MapAddIcon className="mr-1 relative left-[-2px]" />
) : (
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
),
command: onHubToggle,
},
...stationItems,
...(toggleHubCommand
? [
{
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
icon: !hubs.includes(systemId) ? (
<MapAddIcon className="mr-1 relative left-[-2px]" />
) : (
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
),
command: onHubToggle,
},
]
: []),
...(!systemOnMap
? [
{
@@ -94,15 +200,18 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
systems,
getJumpPlannerMenu,
getWaypointMenu,
getStationsMenu,
hubs,
onHubToggle,
onAddSystem,
onOpenSettings,
toggleHubCommand,
routes,
]);
return (
<>
<ContextMenu model={items} ref={contextMenuRef} breakpoint="767px" />
<ContextMenu className={classes.ContextMenu} model={items} ref={contextMenuRef} breakpoint="767px" />
</>
);
};

View File

@@ -0,0 +1,90 @@
import { RouteStationSummary } from '@/hooks/Mapper/types/routes.ts';
const ROMAN_VALUES: Record<string, number> = {
I: 1,
V: 5,
X: 10,
L: 50,
C: 100,
D: 500,
M: 1000,
};
const MAX_DISTANCE = Number.MAX_SAFE_INTEGER;
const romanToInt = (value: string): number | null => {
const chars = value.toUpperCase().split('');
if (chars.length === 0 || chars.some(char => ROMAN_VALUES[char] === undefined)) {
return null;
}
let total = 0;
let prev = 0;
for (let i = chars.length - 1; i >= 0; i--) {
const current = ROMAN_VALUES[chars[i]];
if (current < prev) {
total -= current;
} else {
total += current;
prev = current;
}
}
return total;
};
const parseOrbitIndex = (value: string | undefined): number | null => {
if (!value) {
return null;
}
const trimmed = value.trim();
const asInt = Number.parseInt(trimmed, 10);
if (!Number.isNaN(asInt) && `${asInt}` === trimmed) {
return asInt;
}
return romanToInt(trimmed);
};
const extractPlanetOrbit = (name: string): number | null => {
const firstPart = name.split(' - ')[0] ?? '';
const match = firstPart.match(/([IVXLCDM]+|\d+)(?:\s*\([^)]*\))?$/i);
return parseOrbitIndex(match?.[1]);
};
const extractMoonOrbit = (name: string): number | null => {
const match = name.match(/\bMoon\s+([IVXLCDM]+|\d+)\b/i);
return parseOrbitIndex(match?.[1]);
};
const stationSortKey = (station: RouteStationSummary): [number, number, string, number] => {
return [
extractPlanetOrbit(station.station_name) ?? MAX_DISTANCE,
// If there is no moon in the station name, treat it as closer than moon orbits.
extractMoonOrbit(station.station_name) ?? 0,
station.station_name.toLowerCase(),
station.station_id,
];
};
export const sortStationsByDistance = (stations: RouteStationSummary[]): RouteStationSummary[] => {
return [...stations].sort((a, b) => {
const aKey = stationSortKey(a);
const bKey = stationSortKey(b);
for (let i = 0; i < aKey.length; i++) {
if (aKey[i] < bKey[i]) {
return -1;
}
if (aKey[i] > bKey[i]) {
return 1;
}
}
return 0;
});
};

View File

@@ -38,7 +38,7 @@ export const useContextMenuSystemInfoHandlers = () => {
return;
}
ref.current.toggleHubCommand(system);
ref.current.toggleHubCommand?.(system);
setSystem(undefined);
}, []);

View File

@@ -6,6 +6,7 @@ export const useDetectSettingsChanged = () => {
storedSettings: {
interfaceSettings,
settingsRoutes,
settingsRoutesBy,
settingsLocal,
settingsSignatures,
settingsOnTheMap,
@@ -16,7 +17,15 @@ export const useDetectSettingsChanged = () => {
useEffect(
() => setCounter(x => x + 1),
[interfaceSettings, settingsRoutes, settingsLocal, settingsSignatures, settingsOnTheMap, settingsKills],
[
interfaceSettings,
settingsRoutes,
settingsRoutesBy,
settingsLocal,
settingsSignatures,
settingsOnTheMap,
settingsKills,
],
);
return counter;

View File

@@ -1,11 +1,3 @@
import {
MASS_STATE_NAMES,
MASS_STATE_NAMES_ORDER,
SHIP_SIZES_NAMES,
SHIP_SIZES_NAMES_ORDER,
SHIP_SIZES_NAMES_SHORT,
SHIP_SIZES_SIZE,
} from '@/hooks/Mapper/components/map/constants.ts';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
@@ -14,6 +6,8 @@ import { MenuItem } from 'primereact/menuitem';
import React, { RefObject, useMemo } from 'react';
import { Edge } from 'reactflow';
import { LifetimeActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/LifetimeActionsWrapper.tsx';
import { MassStatusActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/MassStatusActionsWrapper.tsx';
import { ShipSizeActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/ShipSizeActionsWrapper.tsx';
import classes from './ContextMenuConnection.module.scss';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { isNullsecSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
@@ -86,16 +80,28 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
return <LifetimeActionsWrapper lifetime={edge.data?.time_status} onChangeLifetime={onChangeTimeState} />;
},
},
...(!isFrigateSize
? [
{
className: clsx(classes.FastActions, '!h-[54px]'),
template: () => {
return (
<MassStatusActionsWrapper
massStatus={edge.data?.mass_status}
onChangeMassStatus={onChangeMassState}
/>
);
},
},
]
: []),
{
label: `Frigate`,
className: clsx({
[classes.ConnectionFrigate]: isFrigateSize,
}),
icon: PrimeIcons.CLOUD,
command: () =>
onChangeShipSizeStatus(
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
),
className: clsx(classes.FastActions, '!h-[64px]'),
template: () => {
return (
<ShipSizeActionsWrapper shipSize={edge.data?.ship_size_type} onChangeShipSize={onChangeShipSizeStatus} />
);
},
},
{
label: `Save mass`,
@@ -105,41 +111,6 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
icon: PrimeIcons.LOCK,
command: () => onToggleMassSave(!edge.data?.locked),
},
...(!isFrigateSize
? [
{
label: `Mass status`,
icon: PrimeIcons.CHART_PIE,
items: MASS_STATE_NAMES_ORDER.map(x => ({
label: MASS_STATE_NAMES[x],
className: clsx({
[classes.SelectedItem]: edge.data?.mass_status === x,
}),
command: () => onChangeMassState(x),
})),
},
]
: []),
{
label: `Ship Size`,
icon: PrimeIcons.CLOUD,
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
label: (
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
<div>{SHIP_SIZES_NAMES[x]}</div>
<div></div>
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
{SHIP_SIZES_SIZE[x]} t.
</div>
</div>
) as unknown as string, // TODO my lovely kostyl
className: clsx({
[classes.SelectedItem]: edge.data?.ship_size_type === x,
}),
command: () => onChangeShipSizeStatus(x),
})),
},
...(bothNullsec
? [
{

View File

@@ -0,0 +1,15 @@
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
import {
WdMassStatusSelector,
WdMassStatusSelectorProps,
} from '@/hooks/Mapper/components/ui-kit/WdMassStatusSelector.tsx';
export const MassStatusActionsWrapper = (props: WdMassStatusSelectorProps) => {
return (
<LayoutEventBlocker className="flex flex-col gap-1 w-[100%] h-full px-2 pt-[4px]">
<div className="text-[12px] text-stone-500 font-semibold">Mass status:</div>
<WdMassStatusSelector {...props} />
</LayoutEventBlocker>
);
};

View File

@@ -0,0 +1,12 @@
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
import { WdShipSizeSelector, WdShipSizeSelectorProps } from '@/hooks/Mapper/components/ui-kit/WdShipSizeSelector.tsx';
export const ShipSizeActionsWrapper = (props: WdShipSizeSelectorProps) => {
return (
<LayoutEventBlocker className="flex flex-col gap-1 w-[100%] h-full px-2 pt-[4px]">
<div className="text-[12px] text-stone-500 font-semibold">Ship size:</div>
<WdShipSizeSelector {...props} />
</LayoutEventBlocker>
);
};

View File

@@ -39,6 +39,10 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
return customInfo?.time_status === TimeStatus._1h;
}, [customInfo]);
const is4H = useMemo(() => {
return customInfo?.time_status === TimeStatus._4h;
}, [customInfo]);
const whClassStyle = useMemo(() => {
if (signature.type === 'K162' && k162TypeOption) {
const k162Data = wormholesData[k162TypeOption.whClassName];
@@ -65,6 +69,7 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">
<rect y="1" width="13" height="4" rx="2" className={whClassStyle} fill="currentColor" />
{isEOL && <rect x="4" width="5" height="6" rx="1" className={clsx(classes.Eol)} fill="#a153ac" />}
{is4H && <rect x="4" width="5" height="6" rx="1" className={clsx(classes.Eol)} fill="#d8b4fe" />}
</svg>
</div>
</WdTooltipWrapper>

View File

@@ -72,7 +72,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const {
storedSettings: { interfaceSettings },
data: { systemSignatures: mapSystemSignatures },
data: { systemSignatures: mapSystemSignatures, pings },
} = useMapRootState();
const systemStaticInfo = useMemo(() => {
@@ -108,7 +108,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
visibleNodes,
showKSpaceBG,
isThickConnections,
pings,
systemHighlighted,
},
outCommand,

View File

@@ -121,6 +121,7 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
useEffect(() => {
if (!ping) {
setIsShow(false);
return;
}
@@ -161,27 +162,26 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
};
}, [interfaceSettings]);
if (!ping) {
return null;
}
const isShowSelectedSystem = selectedSystem != null && selectedSystem !== ping.solar_system_id;
const isShowSelectedSystem = ping && selectedSystem != null && selectedSystem !== ping.solar_system_id;
// Only render Toast when there's a ping
return (
<>
<Toast
position={placement as never}
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
ref={toast}
content={({ message }) => (
<section
className={clsx(
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
)}
>
<div className="flex gap-3">
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
{ping && (
<Toast
key={ping.id}
position={placement as never}
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
ref={toast}
content={({ message }) => (
<section
className={clsx(
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
)}
>
<div className="flex gap-3">
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
<div className="flex flex-col gap-1 w-full">
<div className="flex justify-between">
<div>
@@ -253,28 +253,33 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
{/*/>*/}
</div>
</section>
)}
></Toast>
)}
></Toast>
)}
<WdButton
icon="pi pi-bell"
severity="warning"
aria-label="Notification"
size="small"
className="w-[33px] h-[33px]"
outlined
onClick={handleClickShow}
disabled={isShow}
/>
{ping && (
<>
<WdButton
icon="pi pi-bell"
severity="warning"
aria-label="Notification"
size="small"
className="w-[33px] h-[33px]"
outlined
onClick={handleClickShow}
disabled={isShow}
/>
<ConfirmPopup
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
message="Are you sure you want to delete ping?"
icon="pi pi-exclamation-triangle text-orange-400"
accept={removePing}
/>
<ConfirmPopup
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
message="Are you sure you want to delete ping?"
icon="pi pi-exclamation-triangle text-orange-400"
accept={removePing}
/>
</>
)}
</>
);
};

View File

@@ -3,9 +3,9 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useSystemInfo } from '@/hooks/Mapper/components/hooks';
import {
SOLAR_SYSTEM_CLASS_IDS,
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
SOLAR_SYSTEM_CLASS_IDS,
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
} 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';
@@ -91,7 +91,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
if (k162TypeInfo) {
// Check if the k162Type matches our target system class
return customInfo.k162Type === targetSystemClassGroup;
return k162TypeInfo.value.includes(targetSystemClassGroup);
}
}

View File

@@ -7,6 +7,7 @@ import {
SystemStructures,
WRoutesPublic,
WRoutesUser,
WRoutesBy,
WSystemKills,
} from '@/hooks/Mapper/components/mapInterface/widgets';
@@ -18,6 +19,7 @@ export enum WidgetsIds {
signatures = 'signatures',
local = 'local',
routes = 'routes',
routesBy = 'routesBy',
structures = 'structures',
kills = 'kills',
comments = 'comments',
@@ -60,6 +62,13 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
zIndex: 0,
content: () => <WRoutesPublic />,
},
{
id: WidgetsIds.routesBy,
position: { x: 10, y: 740 },
size: { width: 510, height: 200 },
zIndex: 0,
content: () => <WRoutesBy />,
},
{
id: WidgetsIds.userRoutes,
position: { x: 10, y: 10 },
@@ -112,6 +121,10 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
id: WidgetsIds.routes,
label: 'Routes',
},
{
id: WidgetsIds.routesBy,
label: 'Routes By',
},
{
id: WidgetsIds.userRoutes,
label: 'User Routes',

View File

@@ -41,7 +41,7 @@ export const RoutesWidgetContent = () => {
const {
data: { selectedSystems, systems, isSubscriptionActive },
} = useMapRootState();
const { hubs = [], routesList, isRestricted, loading } = useRouteProvider();
const { hubs = [], routesList, isRestricted, loading, nohubsPlaceholder } = useRouteProvider();
const [systemId] = selectedSystems;
@@ -105,7 +105,11 @@ export const RoutesWidgetContent = () => {
}
if (hubs.length === 0) {
return <div className="w-full h-full flex justify-center items-center select-none">Routes not set</div>;
return (
<div className="w-full h-full flex justify-center items-center select-none">
{nohubsPlaceholder ?? 'Routes not set'}
</div>
);
}
return (
@@ -129,7 +133,6 @@ export const RoutesWidgetContent = () => {
offset: 10,
}}
/>
<SystemView
systemId={route.destination.toString()}
className={clsx('select-none text-center cursor-context-menu')}
@@ -138,7 +141,7 @@ export const RoutesWidgetContent = () => {
showCustomName
/>
</div>
<div className="text-right pl-1">{route.has_connection ? route.systems?.length ?? 2 : ''}</div>
<div className="text-right pl-1">{route.has_connection ? (route.systems?.length ?? 2) : ''}</div>
<div className="pl-2 pb-0.5">
<RoutesList data={route} onContextMenu={handleContextMenu} />
</div>
@@ -147,9 +150,7 @@ export const RoutesWidgetContent = () => {
})}
</div>
</LoadingWrapper>
<ContextMenuSystemInfo
hubs={hubs}
routes={preparedRoutes}
systems={systems}
systemStatics={systemStatics}
@@ -162,9 +163,10 @@ export const RoutesWidgetContent = () => {
type RoutesWidgetCompProps = {
title: ReactNode | string;
renderContent?: (content: ReactNode, compact: boolean) => ReactNode;
};
export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps) => {
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
const { data, update, addHubCommand } = useRouteProvider();
@@ -183,7 +185,7 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => addHubCommand(item.value.toString()),
async item => addHubCommand?.(item.value.toString()),
[addHubCommand],
);
@@ -191,15 +193,17 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
<Widget
label={
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
<span className="select-none">{title}</span>
<div className="select-none flex items-center gap-2">{title}</div>
<LayoutEventBlocker className="flex items-center gap-2">
<WdImgButton
className={PrimeIcons.PLUS_CIRCLE}
onClick={onAddSystem}
tooltip={{
content: 'Click here to add new system to routes',
}}
/>
{addHubCommand && (
<WdImgButton
className={PrimeIcons.PLUS_CIRCLE}
onClick={onAddSystem}
tooltip={{
content: 'Click here to add new system to routes',
}}
/>
)}
<WdTooltipWrapper content="Show shortest route" position={TooltipPosition.top}>
<WdCheckbox
@@ -223,24 +227,38 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
</div>
}
>
<RoutesWidgetContent />
{renderContent ? (
renderContent(
<div className="h-full overflow-auto bg-opacity-5 custom-scrollbar">
<RoutesWidgetContent />
</div>,
compact,
)
) : (
<div className="h-full overflow-auto bg-opacity-5 custom-scrollbar">
<RoutesWidgetContent />
</div>
)}
<RoutesSettingsDialog visible={routeSettingsVisible} setVisible={setRouteSettingsVisible} />
<AddSystemDialog
title="Add system to routes"
visible={openAddSystem}
setVisible={() => setOpenAddSystem(false)}
onSubmit={handleSubmitAddSystem}
/>
{addHubCommand && (
<AddSystemDialog
title="Add system to routes"
visible={openAddSystem}
setVisible={() => setOpenAddSystem(false)}
onSubmit={handleSubmitAddSystem}
/>
)}
</Widget>
);
};
export const RoutesWidget = forwardRef<RoutesImperativeHandle, RoutesWidgetProps & RoutesWidgetCompProps>(
({ title, ...props }, ref) => {
({ title, renderContent, ...props }, ref) => {
return (
<RoutesProvider {...props} ref={ref}>
<RoutesWidgetComp title={title} />
<RoutesWidgetComp title={title} renderContent={renderContent} />
</RoutesProvider>
);
},

View File

@@ -1 +1,2 @@
export * from './useLoadRoutes';
export * from './useLoadRoutesBy';

View File

@@ -0,0 +1,71 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { flattenValues } from '@/hooks/Mapper/utils/flattenValues.ts';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types';
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
type UseLoadRoutesByProps = {
loadRoutesCommand: LoadRoutesCommand;
routesList: RoutesList | undefined;
data: RoutesType;
deps?: unknown[];
};
export const useLoadRoutesBy = ({
data: routesSettings,
loadRoutesCommand,
routesList,
deps = [],
}: UseLoadRoutesByProps) => {
const [loading, setLoading] = useState(false);
const {
data: { selectedSystems },
} = useMapRootState();
const prevSys = usePrevious(selectedSystems);
const ref = useRef({ prevSys, selectedSystems });
ref.current = { prevSys, selectedSystems };
const loadRoutes = useCallback(
(systemId: string, settings: RoutesType) => {
loadRoutesCommand(systemId, settings);
setLoading(true);
},
[loadRoutesCommand],
);
useMapEventListener(event => {
if (event.name === Commands.routesListBy) {
setLoading(false);
}
});
useEffect(() => {
setLoading(false);
}, [routesList]);
useEffect(() => {
if (selectedSystems.length !== 1) {
return;
}
const [systemId] = selectedSystems;
loadRoutes(systemId, routesSettings);
}, [loadRoutes, selectedSystems, ...flattenValues(routesSettings), ...deps]);
return { loading, loadRoutes, setLoading };
};

View File

@@ -12,9 +12,10 @@ export type RoutesWidgetProps = {
routesList: RoutesList | undefined;
loading: boolean;
addHubCommand: AddHubCommand;
toggleHubCommand: ToggleHubCommand;
addHubCommand?: AddHubCommand;
toggleHubCommand?: ToggleHubCommand;
isRestricted?: boolean;
nohubsPlaceholder?: string;
};
export type RoutesProviderInnerProps = RoutesWidgetProps;

View File

@@ -1,6 +1,16 @@
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
import { FINAL_DURATION_MS } from '../constants';
// Strip frontend-only fields that should never be sent to the backend.
// "linked_system" is an object the frontend uses; the backend expects "linked_system_id" (integer)
// which is set via a separate linkSignatureToSystem call.
function stripFrontendFields(s: ExtendedSystemSignature) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { linked_system, pendingDeletion, pendingAddition, pendingUntil, finalTimeoutId, character_name, ...rest } =
s as any;
return rest;
}
export function prepareUpdatePayload(
systemId: string,
added: ExtendedSystemSignature[],
@@ -9,9 +19,9 @@ export function prepareUpdatePayload(
) {
return {
system_id: systemId,
added: added.map(s => ({ ...s })),
updated: updated.map(s => ({ ...s })),
removed: removed.map(s => ({ ...s })),
added: added.map(stripFrontendFields),
updated: updated.map(stripFrontendFields),
removed: removed.map(stripFrontendFields),
};
}

View File

@@ -35,7 +35,7 @@ export const useSignatureFetching = ({ systemId, settings, signaturesRef, setSig
const extended = serverSigs.map(s => ({
...s,
character_name: characters.find(c => c.eve_id === s.character_eve_id)?.name,
character_name: s.character_name ?? characters.find(c => c.eve_id === s.character_eve_id)?.name,
})) as ExtendedSystemSignature[];
setSignatures(() => extended);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, ClipboardEvent, useRef } from 'react';
import React, { useCallback, ClipboardEvent, useRef, useState } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import {
@@ -13,7 +13,9 @@ import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemStructuresContent } from './SystemStructuresContent/SystemStructuresContent';
import { useSystemStructures } from './hooks/useSystemStructures';
import { processSnippetText } from './helpers';
import { processSnippetText, StructureItem } from './helpers';
import { SystemStructuresOwnersDialog } from './SystemStructuresOwnersDialog/SystemStructuresOwnersDialog';
import clsx from 'clsx';
export const SystemStructures: React.FC = () => {
const {
@@ -24,6 +26,7 @@ export const SystemStructures: React.FC = () => {
const isNotSelectedSystem = selectedSystems.length !== 1;
const { structures, handleUpdateStructures } = useSystemStructures({ systemId, outCommand });
const [showEditDialog, setShowEditDialog] = useState(false);
const labelRef = useRef<HTMLDivElement>(null);
const isCompact = useMaxWidth(labelRef, 260);
@@ -48,6 +51,18 @@ export const SystemStructures: React.FC = () => {
[processClipboard],
);
const handleSave = (updatedStructures: StructureItem[]) => {
handleUpdateStructures(updatedStructures)
}
const handleOpenDialog = useCallback(() => {
setShowEditDialog(true)
}, [])
const handleCloseDialog = useCallback(() => {
setShowEditDialog(false)
}, [])
const handlePasteTimer = useCallback(async () => {
try {
const text = await navigator.clipboard.readText();
@@ -71,8 +86,19 @@ export const SystemStructures: React.FC = () => {
</div>
<LayoutEventBlocker className="flex gap-2.5">
{structures.length > 1 && (
<WdImgButton
className={clsx(PrimeIcons.USER_EDIT, 'text-sky-400 hover:text-sky-200 transition duration-300')}
onClick={handleOpenDialog}
tooltip={{
position: TooltipPosition.left,
// @ts-ignore
content: 'Update all structure owners',
}}
/>
)}
<WdImgButton
className={`${PrimeIcons.CLOCK} text-sky-400 hover:text-sky-200 transition duration-300`}
className={clsx(PrimeIcons.CLOCK, 'text-sky-400 hover:text-sky-200 transition duration-300')}
onClick={handlePasteTimer}
tooltip={{
position: TooltipPosition.left,
@@ -117,6 +143,15 @@ export const SystemStructures: React.FC = () => {
<SystemStructuresContent structures={structures} onUpdateStructures={handleUpdateStructures} />
)}
</Widget>
{showEditDialog && (
<SystemStructuresOwnersDialog
visible={showEditDialog}
structures={structures}
onClose={handleCloseDialog}
onSave={handleSave}
/>
)}
</div>
);
};

View File

@@ -4,7 +4,14 @@ import { AutoComplete } from 'primereact/autocomplete';
import { Calendar } from 'primereact/calendar';
import clsx from 'clsx';
import { formatToISO, statusesRequiringTimer, StructureItem, StructureStatus } from '../helpers';
import {
calendarDateToUtcIso,
formatToISO,
statusesRequiringTimer,
StructureItem,
StructureStatus,
utcToCalendarDate,
} from '../helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
@@ -72,7 +79,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
// If this is the endTime (Date from Calendar), we store as ISO or string:
if (field === 'endTime' && val instanceof Date) {
return { ...prev, endTime: val.toISOString() };
return { ...prev, endTime: calendarDateToUtcIso(val) };
}
return { ...prev, [field]: val };
@@ -188,7 +195,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
Timer <br /> (Eve Time):
</span>
<Calendar
value={editData.endTime ? new Date(editData.endTime) : undefined}
value={editData.endTime ? utcToCalendarDate(editData.endTime) : undefined}
onChange={e => handleChange('endTime', e.value ?? '')}
showTime
hourFormat="24"

View File

@@ -0,0 +1,31 @@
.systemStructuresOwnersDialog {
.p-dialog-content {
background-color: var(--surface-800) !important;
}
.p-dialog-header {
background-color: var(--surface-700);
color: var(--text-color);
}
.p-dialog-header-icon,
.p-dialog-header-title {
color: var(--gray-200);
}
.p-inputtext {
background-color: #2a2a2a !important;
color: #ddd !important;
font-size: 12px !important;
padding: 0.25rem 0.5rem !important;
}
.p-dialog-footer {
.p-button {
font-size: 12px !important;
padding: 0.3rem 0.75rem !important;
}
}
}

View File

@@ -0,0 +1,180 @@
import clsx from 'clsx';
import { AutoComplete } from 'primereact/autocomplete';
import { Dialog } from 'primereact/dialog';
import React, { useCallback, useState } from 'react';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useToast } from '@/hooks/Mapper/ToastProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { StructureItem } from '../helpers';
interface StructuresOwnersEditDialogProps {
visible: boolean;
structures: StructureItem[];
onClose: () => void;
onSave: (updatedStuctures: StructureItem[]) => void;
}
export const SystemStructuresOwnersDialog: React.FC<StructuresOwnersEditDialogProps> = ({
visible,
structures,
onClose,
onSave,
}) => {
const [ownerInput, setOwnerInput] = useState('');
const [ownerSuggestions, setOwnerSuggestions] = useState<{ label: string; value: string }[]>([]);
const { outCommand } = useMapRootState();
const { show } = useToast();
const [prevQuery, setPrevQuery] = useState('');
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
const [editData, setEditData] = useState<StructureItem[]>(structures);
// Searching corporation owners via auto-complete
const searchOwners = useCallback(
async (e: { query: string }) => {
const newQuery = e.query.trim();
if (!newQuery) {
setOwnerSuggestions([]);
return;
}
// If user typed more text but we have partial match in prevResults
if (newQuery.startsWith(prevQuery) && prevResults.length > 0) {
const filtered = prevResults.filter(item => item.label.toLowerCase().includes(newQuery.toLowerCase()));
setOwnerSuggestions(filtered);
return;
}
try {
// TODO fix it
const { results = [] } = await outCommand({
type: OutCommand.getCorporationNames,
data: { search: newQuery },
});
setOwnerSuggestions(results);
setPrevQuery(newQuery);
setPrevResults(results);
} catch (err) {
show({
severity: 'error',
summary: 'Failed to fetch owners',
detail: `${err}`,
life: 10000,
});
}
},
[prevQuery, prevResults, outCommand],
);
// when user picks a corp from auto-complete
const handleSelectOwner = (selected: { label: string; value: string }) => {
setOwnerInput(selected.label);
setEditData(
structures.map(item => {
return { ...item, ownerName: selected.label, ownerId: selected.value };
}),
);
};
const handleSaveClick = async () => {
if (!editData) return;
// Get all unique owner IDs that need ticker lookup
const allOwnerIds = editData.filter(x => x.ownerId != null).map(x => x.ownerId as string);
const uniqueOwnerIds = [...new Set(allOwnerIds)];
// Fetch all tickers in parallel
const tickerResults = await Promise.all(
uniqueOwnerIds.map(async ownerId => {
try {
const { ticker } = await outCommand({
type: OutCommand.getCorporationTicker,
data: { corp_id: ownerId },
});
return { ownerId, ticker: ticker ?? '' };
} catch (err) {
console.error('Failed to fetch ticker for ownerId:', ownerId, err);
return { ownerId, ticker: '' };
}
}),
);
// Create a map of ownerId -> ticker for quick lookup
const tickerMap = new Map(tickerResults.map(r => [r.ownerId, r.ticker]));
// Create new array with updated values (no mutation)
const updatedStructures = editData.map(structure => {
if (!structure.ownerId) {
return structure;
}
return {
...structure,
ownerTicker: tickerMap.get(structure.ownerId) ?? '',
};
});
onSave(updatedStructures);
onClose();
};
return (
<Dialog
visible={visible}
onHide={onClose}
header={'Update All Structure Owners'}
className={clsx('myStructuresOwnersDialog', 'text-stone-200 w-full max-w-md')}
>
<div className="flex flex-col gap-2 text-[14px]">
<div className="flex gap-2">
Updating the corporation name below will update all structures currently saved within the system.
</div>
<hr />
<div className="flex flex-col gap-2">
<label className="grid grid-cols-[100px_1fr] gap-2 items-start mt-2">
<span className="mt-1">Structures to update:</span>
<ul>
{structures &&
structures.map((item, i) => (
<li key={i}>
{item.structureType || 'Unknown Type'} - {item.name}
</li>
))}
</ul>
</label>
</div>
<hr />
<div>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<span>Owner:</span>
<AutoComplete
id="owner"
value={ownerInput}
suggestions={ownerSuggestions}
completeMethod={searchOwners}
minLength={3}
delay={400}
field="label"
placeholder="Corporation name..."
onChange={e => setOwnerInput(e.value)}
onSelect={e => handleSelectOwner(e.value)}
/>
</label>
</div>
</div>
<div className="flex justify-end items-center gap-2 mt-4">
<WdButton label="Save" className="p-button-sm" onClick={handleSaveClick} />
</div>
</Dialog>
);
};

View File

@@ -43,6 +43,29 @@ export function mapServerStructure(serverData: any): StructureItem {
};
}
export function utcToCalendarDate(utcIso: string): Date {
// Parse ISO components manually to avoid browser quirks with
// 6-digit microsecond precision from Elixir's :utc_datetime_usec.
const m = utcIso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/);
if (m) {
const [, yr, mo, dy, hr, mi, sc] = m;
return new Date(+yr, +mo - 1, +dy, +hr, +mi, +sc);
}
// Fallback for non-ISO strings
const d = new Date(utcIso);
return new Date(d.getTime() + d.getTimezoneOffset() * 60_000);
}
export function calendarDateToUtcIso(localDate: Date): string {
// Read local-time components (which represent EVE/UTC time) and
// build the ISO string directly — no timezone arithmetic needed.
const pad = (n: number) => String(n).padStart(2, '0');
return (
`${localDate.getFullYear()}-${pad(localDate.getMonth() + 1)}-${pad(localDate.getDate())}` +
`T${pad(localDate.getHours())}:${pad(localDate.getMinutes())}:${pad(localDate.getSeconds())}.000Z`
);
}
export function formatToISO(datetimeLocal: string): string {
if (!datetimeLocal) return '';

View File

@@ -0,0 +1,202 @@
import { useCallback, useMemo, useRef } from 'react';
import { RoutesWidget } from '@/hooks/Mapper/components/mapInterface/widgets';
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { useLoadRoutesBy } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { Dropdown } from 'primereact/dropdown';
import { SelectItemOptionsType } from 'primereact/selectitem';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import clsx from 'clsx';
import { RoutesByCategoryType, RoutesByScopeType, RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { DEFAULT_ROUTES_SETTINGS } from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { PrimeIcons } from 'primereact/api';
export type RoutesByType = RoutesByCategoryType;
type WRoutesByProps = {
type?: RoutesByType;
title?: string;
};
const ROUTES_BY_OPTIONS: SelectItemOptionsType = [
{
label: 'Blue Loot',
value: 'blueLoot',
icon: 'images/30747_64.png',
},
{
label: 'Red Loot',
value: 'redLoot',
icon: 'images/89219_64.png',
},
{
label: 'Thera',
value: 'thera',
icon: 'images/map.png',
},
{
label: 'Turnur',
value: 'turnur',
icon: 'images/map.png',
},
{
label: 'Security Office',
value: 'so_cleaning',
icon: 'images/concord-so.png',
},
{
label: 'Trade Hubs',
value: 'trade_hubs',
icon: 'images/market.png',
},
];
const ROUTES_BY_SECURITY_OPTIONS = [
{ label: 'All', value: 'ALL' },
{ label: 'High', value: 'HIGH' },
];
export const WRoutesBy = ({ type = 'blueLoot', title = 'Routes By' }: WRoutesByProps) => {
const {
outCommand,
storedSettings: { settingsRoutesBy, settingsRoutesByUpdate },
data,
} = useMapRootState();
const criteriaType = settingsRoutesBy.type ?? type;
const securityType = settingsRoutesBy.scope ?? 'ALL';
const routesSettings = settingsRoutesBy.routes ?? DEFAULT_ROUTES_SETTINGS;
const routesListBy = data.routesListBy;
const availableRoutesBy = data.availableRoutesBy;
const routesByOptions = useMemo(() => {
if (!availableRoutesBy || availableRoutesBy.length === 0) {
return ROUTES_BY_OPTIONS;
}
return ROUTES_BY_OPTIONS.filter(option => availableRoutesBy.includes(option.value as RoutesByType));
}, [availableRoutesBy]);
const resolvedCriteriaType = useMemo(() => {
const optionValues = routesByOptions.map(option => option.value as RoutesByType);
if (optionValues.length === 0) {
return criteriaType;
}
return optionValues.includes(criteriaType) ? criteriaType : optionValues[0];
}, [routesByOptions, criteriaType]);
const loadRoutesCommand: LoadRoutesCommand = useCallback(
async (systemId, currentRoutesSettings) => {
await outCommand({
type: OutCommand.getRoutesBy,
data: {
system_id: systemId,
type: resolvedCriteriaType,
securityType: securityType === 'HIGH' ? 'high' : 'both',
routes_settings: currentRoutesSettings,
},
});
},
[outCommand, resolvedCriteriaType, securityType],
);
const hubs = useMemo(() => routesListBy?.routes?.map(route => route.destination.toString()) ?? [], [routesListBy]);
const { loading: internalLoading } = useLoadRoutesBy({
data: routesSettings,
loadRoutesCommand,
routesList: routesListBy,
deps: [resolvedCriteriaType, securityType],
});
const updateRoutesSettings = useCallback(
(next: RoutesType) => settingsRoutesByUpdate(prev => ({ ...prev, routes: next })),
[settingsRoutesByUpdate],
);
const ref = useRef<HTMLDivElement>(null);
const compactSmall = useMaxWidth(ref, 180);
const compactMiddle = useMaxWidth(ref, 245);
const titleNode = useMemo(
() => (
<div className="flex items-center gap-2">
<span className="select-none">{title}</span>
<WdImgButton
className={PrimeIcons.QUESTION_CIRCLE}
tooltip={{
position: TooltipPosition.top,
content: 'Alpha map users can access only 1 route',
}}
/>
</div>
),
[title],
);
return (
<RoutesWidget
title={titleNode}
nohubsPlaceholder="Not found any destinations"
renderContent={(content /*, compact*/) => (
<div className="h-full grid grid-rows-[1fr_auto]" ref={ref}>
{content}
<div className="flex items-center gap-2 justify-end mb-2 px-2 pt-2">
{!compactSmall && (
<Dropdown
value={securityType}
options={ROUTES_BY_SECURITY_OPTIONS}
onChange={e => settingsRoutesByUpdate(prev => ({ ...prev, scope: e.value as RoutesByScopeType }))}
className="w-[90px] [&_span]:!text-[12px]"
/>
)}
<Dropdown
value={resolvedCriteriaType}
itemTemplate={e => (
<div className="flex items-center gap-2">
{e.icon && <img src={e.icon} height="18" width="18" />}
<span className="text-[12px]">{e.label}</span>
</div>
)}
valueTemplate={e => {
if (!e) {
return null;
}
if (compactMiddle) {
return (
<div className="flex items-center gap-2 min-w-[50px]">
{e.icon ? <img src={e.icon} height="18" width="18" /> : <span>{e.label}</span>}
</div>
);
}
return (
<div className="flex items-center gap-2">
{e.icon && <img src={e.icon} height="18" width="18" />}
<span className="text-[12px]">{e.label}</span>
</div>
);
}}
options={routesByOptions}
onChange={e => settingsRoutesByUpdate(prev => ({ ...prev, type: e.value as RoutesByCategoryType }))}
className={clsx({
['w-[130px]']: !compactMiddle,
['w-[65px]']: compactMiddle,
})}
/>
</div>
</div>
)}
data={routesSettings}
update={updateRoutesSettings}
hubs={hubs}
routesList={routesListBy}
loading={internalLoading}
/>
);
};

View File

@@ -0,0 +1,2 @@
export { WRoutesBy } from './WRoutesBy';
export type { RoutesByType } from './WRoutesBy';

View File

@@ -6,4 +6,5 @@ export * from './SystemStructures';
export * from './WSystemKills';
export * from './WRoutesUser';
export * from './WRoutesPublic';
export * from './WRoutesBy';
export * from './CommentsWidget';

View File

@@ -38,9 +38,11 @@ export const OldSettingsDialog = () => {
localWidget: createSettings(widgetLocal, {}),
widgets: createSettings(widgetsOld, {}),
routes: createSettings(widgetRoutes, {}),
routesBy: createSettings(widgetRoutes, {}),
onTheMap: createSettings(onTheMapOld, {}),
signaturesWidget: createSettings(signatures, {}),
interface: createSettings(interfaceSettings, {}),
map: createSettings(null, { viewport: { zoom: 1, x: 0, y: 0 } }),
};
if (asFile) {

View File

@@ -5,7 +5,7 @@ import {
import { SystemsSettingsProvider } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/Provider.tsx';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { MassState, OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { useCallback, useEffect } from 'react';
@@ -15,6 +15,7 @@ type SystemSignaturePrepared = Omit<SystemSignature, 'linked_system'> & {
linked_system: string;
k162Type: string;
time_status: TimeStatus;
mass_status: MassState;
};
export interface MapSettingsProps {
@@ -59,6 +60,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
custom_info: JSON.stringify({
k162Type: values.k162Type,
time_status: values.time_status,
mass_status: values.mass_status,
}),
};
@@ -139,16 +141,19 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
let k162Type = null;
let time_status = TimeStatus._24h;
let mass_status = MassState.normal;
if (custom_info) {
const customInfo = JSON.parse(custom_info);
k162Type = customInfo.k162Type;
time_status = customInfo.time_status;
mass_status = customInfo.mass_status ?? MassState.normal;
}
signatureForm.reset({
linked_system: linked_system?.solar_system_id.toString() ?? undefined,
k162Type: k162Type,
time_status: time_status,
mass_status: mass_status,
...rest,
});
}, [signatureForm, signatureData]);

View File

@@ -5,6 +5,7 @@ import { SignatureK162TypeSelect } from '@/hooks/Mapper/components/mapRootConten
import { SignatureLeadsToSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLeadsToSelect';
import { SignatureLifetimeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLifetimeSelect.tsx';
import { SignatureTempName } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureTempName.tsx';
import { SignatureMassStatusSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureMassStatusSelect.tsx';
export const SignatureGroupContentWormholes = () => {
const { watch } = useFormContext<SystemSignature>();
@@ -34,6 +35,11 @@ export const SignatureGroupContentWormholes = () => {
<SignatureLifetimeSelect name="time_status" />
</div>
<div className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
<span>Mass status:</span>
<SignatureMassStatusSelect name="mass_status" />
</div>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
<span>Temp. Name:</span>
<SignatureTempName />

View File

@@ -13,6 +13,26 @@ export const renderK162Type = (option: K162Type) => {
return renderNoValue();
}
if (['c1_c2_c3', 'c4_c5'].includes(value)) {
const arr = whClassName.split('_');
return (
<div className="flex gap-1 items-center">
{arr.map(x => (
<WHClassView
key={x}
classNameWh="!text-[11px] !font-bold"
hideWhClassName
hideTooltip
whClassName={x}
noOffset
useShortTitle
/>
))}
</div>
);
}
return (
<WHClassView
classNameWh="!text-[11px] !font-bold"

View File

@@ -0,0 +1,30 @@
import { Controller, useFormContext } from 'react-hook-form';
import { MassState, SystemSignature } from '@/hooks/Mapper/types';
import { WdMassStatusSelector } from '@/hooks/Mapper/components/ui-kit/WdMassStatusSelector.tsx';
export interface SignatureMassStatusSelectProps {
name: string;
defaultValue?: MassState;
}
export const SignatureMassStatusSelect = ({
name,
defaultValue = MassState.normal,
}: SignatureMassStatusSelectProps) => {
const { control } = useFormContext<SystemSignature>();
return (
<div className="my-1">
<Controller
// @ts-ignore
name={name}
control={control}
defaultValue={defaultValue}
render={({ field }) => {
// @ts-ignore
return <WdMassStatusSelector massStatus={field.value} onChangeMassStatus={e => field.onChange(e)} />;
}}
/>
</div>
);
};

View File

@@ -2,3 +2,4 @@ export * from './SignatureGroupSelect';
export * from './SignatureGroupContent';
export * from './SignatureK162TypeSelect';
export * from './SignatureLifetimeSelect';
export * from './SignatureMassStatusSelect';

View File

@@ -4,12 +4,16 @@ import { DataTable } from 'primereact/datatable';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { TrackingCharacter } from '@/hooks/Mapper/types';
import { useTracking } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog/TrackingProvider.tsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const TrackingCharactersList = () => {
const [selected, setSelected] = useState<TrackingCharacter[]>([]);
const { trackingCharacters, main, following, updateTracking } = useTracking();
const refVars = useRef({ trackingCharacters });
refVars.current = { trackingCharacters };
const {
data: { expiredCharacters },
} = useMapRootState();
useEffect(() => {
setSelected(trackingCharacters.filter(x => x.tracked));
@@ -66,7 +70,9 @@ export const TrackingCharactersList = () => {
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
headerClassName="[&_div]:ml-2"
body={row => {
return <CharacterCard showCorporationLogo showTicker isOwn {...row.character} />;
const isExpired = expiredCharacters.includes(row.character.eve_id);
return <CharacterCard showCorporationLogo showTicker isOwn isExpired={isExpired} {...row.character} />;
}}
/>
</DataTable>

View File

@@ -38,7 +38,7 @@ const TrackingDialogComp = ({ visible, onHide }: TrackingDialogProps) => {
resizable={false}
visible={visible}
onHide={onHide}
className="w-[640px] h-[400px] text-text-color min-h-0"
className="w-[640px] h-[600px] text-text-color min-h-0"
>
<TabView
className="vertical-tabs-container h-full [&_.p-tabview-panels]:!pr-0"

View File

@@ -1,4 +1,4 @@
import { createContext, useCallback, useContext, useRef, useState, useEffect } from 'react';
import { createContext, useCallback, useContext, useRef, useState } from 'react';
import { Commands, OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { IncomingEvent, WithChildren } from '@/hooks/Mapper/types/common.ts';

View File

@@ -17,6 +17,7 @@ import { useCallback } from 'react';
import classes from './CharacterCard.module.scss';
import { ZKB_ICON } from '@/hooks/Mapper/icons';
import { charEveWhoLink, charZKBLink } from '@/hooks/Mapper/helpers/linkHelpers.ts';
import { WdCharStateWrapper } from '../../characters/components';
export type CharacterCardProps = {
compact?: boolean;
@@ -29,6 +30,9 @@ export type CharacterCardProps = {
showAllyLogo?: boolean;
showAllyLogoPlaceholder?: boolean;
simpleMode?: boolean;
isExpired?: boolean;
isMain?: boolean;
isFollowing?: boolean;
} & WithIsOwnCharacter &
WithClassName;
@@ -55,6 +59,10 @@ export const CharacterCard = ({
showTicker,
useSystemsCache,
className,
isExpired,
isMain,
isFollowing,
...char
}: CharacterCardInnerProps) => {
const handleSelect = useCallback(() => {
@@ -204,7 +212,22 @@ export const CharacterCard = ({
<div className={clsx('w-full text-xs box-border')} onClick={handleSelect}>
<div className="w-full flex items-center gap-2">
<div className="flex items-center gap-1">
<WdEveEntityPortrait eveId={char.eve_id} size={WdEveEntityPortraitSize.w33} />
<WdCharStateWrapper
eve_id={char.eve_id}
location={char.location}
isExpired={isExpired}
isOnline={char.online}
isMain={isMain}
isFollowing={isFollowing}
>
<WdEveEntityPortrait
eveId={char.eve_id}
size={WdEveEntityPortraitSize.w33}
className={clsx({
['border-red-600/50']: isExpired,
})}
/>
</WdCharStateWrapper>
{showCorporationLogo && (
<WdTooltipWrapper position={TooltipPosition.top} content={char.corporation_name}>

View File

@@ -13,7 +13,7 @@ export type SystemViewProps = {
export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomName, ...rest }: SystemViewProps) => {
const memSystems = useMemo(() => [systemId], [systemId]);
const { systems, loading } = useLoadSystemStatic({ systems: memSystems });
const { systems, lastUpdateKey, loading } = useLoadSystemStatic({ systems: memSystems });
const {
data: { systems: mapSystems },
@@ -23,9 +23,10 @@ export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomN
if (!systemId) {
return customSystemInfo;
}
return systems.get(parseInt(systemId));
// eslint-disable-next-line
}, [customSystemInfo, systemId, systems, loading]);
}, [customSystemInfo, systemId, systems, lastUpdateKey, loading]);
const mapSystemInfo = useMemo(() => {
if (!showCustomName) {

View File

@@ -0,0 +1,65 @@
import { WdButton } from '@/hooks/Mapper/components/ui-kit/WdButton.tsx';
import { MassState } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { BUILT_IN_TOOLTIP_OPTIONS } from './constants.ts';
const MASS_STATUS = [
{
id: MassState.verge,
label: 'Verge',
className: 'bg-red-400 hover:!bg-red-400',
inactiveClassName: 'bg-red-400/30',
description: 'Mass status: Verge of collapse',
},
{
id: MassState.half,
label: 'Half',
className: 'bg-orange-300 hover:!bg-orange-300',
inactiveClassName: 'bg-orange-300/30',
description: 'Mass status: Half',
},
{
id: MassState.normal,
label: 'Normal',
className: 'bg-indigo-300 hover:!bg-indigo-300',
inactiveClassName: 'bg-indigo-300/30',
description: 'Mass status: Normal',
},
];
export interface WdMassStatusSelectorProps {
massStatus?: MassState;
onChangeMassStatus(massStatus: MassState): void;
className?: string;
}
export const WdMassStatusSelector = ({
massStatus = MassState.normal,
onChangeMassStatus,
className,
}: WdMassStatusSelectorProps) => {
return (
<form>
<div className={clsx('grid grid-cols-[auto_auto_auto] gap-1', className)}>
{MASS_STATUS.map(x => (
<WdButton
key={x.id}
outlined={false}
value={x.label}
tooltip={x.description}
tooltipOptions={BUILT_IN_TOOLTIP_OPTIONS}
size="small"
className={clsx(
`py-[1px] justify-center min-w-auto w-auto border-0 text-[12px] font-bold leading-[20px]`,
{ [x.inactiveClassName]: massStatus !== x.id },
x.className,
)}
onClick={() => onChangeMassStatus(x.id)}
>
{x.label}
</WdButton>
))}
</div>
</form>
);
};

View File

@@ -0,0 +1,76 @@
import { WdButton } from '@/hooks/Mapper/components/ui-kit/WdButton.tsx';
import { ShipSizeStatus } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { BUILT_IN_TOOLTIP_OPTIONS } from './constants.ts';
import {
SHIP_SIZES_DESCRIPTION,
SHIP_SIZES_NAMES,
SHIP_SIZES_NAMES_ORDER,
SHIP_SIZES_NAMES_SHORT,
SHIP_SIZES_SIZE,
} from '@/hooks/Mapper/components/map/constants.ts';
const SHIP_SIZE_STYLES: Record<ShipSizeStatus, { className: string; inactiveClassName: string }> = {
[ShipSizeStatus.small]: {
className: 'bg-indigo-400 hover:!bg-indigo-400',
inactiveClassName: 'bg-indigo-400/30',
},
[ShipSizeStatus.medium]: {
className: 'bg-cyan-500 hover:!bg-cyan-500',
inactiveClassName: 'bg-cyan-500/30',
},
[ShipSizeStatus.large]: {
className: 'bg-indigo-300 hover:!bg-indigo-300',
inactiveClassName: 'bg-indigo-300/30',
},
[ShipSizeStatus.freight]: {
className: 'bg-indigo-300 hover:!bg-indigo-300',
inactiveClassName: 'bg-indigo-300/30',
},
[ShipSizeStatus.capital]: {
className: 'bg-indigo-300 hover:!bg-indigo-300',
inactiveClassName: 'bg-indigo-300/30',
},
};
export interface WdShipSizeSelectorProps {
shipSize?: ShipSizeStatus;
onChangeShipSize(shipSize: ShipSizeStatus): void;
className?: string;
}
export const WdShipSizeSelector = ({
shipSize = ShipSizeStatus.large,
onChangeShipSize,
className,
}: WdShipSizeSelectorProps) => {
return (
<form>
<div className={clsx('grid grid-cols-[1fr_1fr_1fr_1fr_1fr] gap-1', className)}>
{SHIP_SIZES_NAMES_ORDER.map(size => {
const style = SHIP_SIZE_STYLES[size];
const tooltip = `${SHIP_SIZES_NAMES[size]}${SHIP_SIZES_SIZE[size]} t. ${SHIP_SIZES_DESCRIPTION[size]}`;
return (
<WdButton
key={size}
outlined={false}
value={SHIP_SIZES_NAMES_SHORT[size]}
tooltip={tooltip}
tooltipOptions={BUILT_IN_TOOLTIP_OPTIONS}
size="small"
className={clsx(
`py-[1px] justify-center min-w-auto w-auto border-0 text-[11px] font-bold leading-[20px]`,
{ [style.inactiveClassName]: shipSize !== size },
style.className,
)}
onClick={() => onChangeShipSize(size)}
>
<span className="text-[11px] font-bold">{SHIP_SIZES_NAMES_SHORT[size]}</span>
</WdButton>
);
})}
</div>
</form>
);
};

View File

@@ -133,6 +133,16 @@ export const K162_TYPES: K162Type[] = [
value: 'pochven',
whClassName: 'F216',
},
{
label: 'C1/C2/C3',
value: 'c1_c2_c3',
whClassName: 'E004_D382_L477',
},
{
label: 'C4/C5',
value: 'c4_c5',
whClassName: 'M001_L614',
},
];
export const K162_TYPES_MAP: { [key: string]: K162Type } = K162_TYPES.reduce(

View File

@@ -6,7 +6,6 @@ import {
MapUnionTypes,
OutCommandHandler,
SolarSystemConnection,
StringBoolean,
TrackingCharacter,
UseCharactersCacheData,
UseCommentsData,
@@ -28,12 +27,14 @@ import {
MapSettings,
MapUserSettings,
OnTheMapSettingsType,
RoutesByType,
RoutesType,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_MAP_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_BY_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
STORED_INTERFACE_DEFAULT_VALUES,
@@ -55,6 +56,7 @@ export type MapRootData = MapUnionTypes & {
trackingCharactersData: TrackingCharacter[];
loadingPublicRoutes: boolean;
map_slug: string | null;
expiredCharacters: string[];
};
const INITIAL_DATA: MapRootData = {
@@ -76,6 +78,8 @@ const INITIAL_DATA: MapRootData = {
userHubs: [],
routes: undefined,
userRoutes: undefined,
routesListBy: undefined,
availableRoutesBy: [],
kills: [],
connections: [],
detailedKills: {},
@@ -99,6 +103,7 @@ const INITIAL_DATA: MapRootData = {
pings: [],
loadingPublicRoutes: false,
map_slug: null,
expiredCharacters: [],
};
export enum InterfaceStoredSettingsProps {
@@ -132,6 +137,8 @@ export interface MapRootContextProps {
setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>;
settingsRoutes: RoutesType;
settingsRoutesUpdate: Dispatch<SetStateAction<RoutesType>>;
settingsRoutesBy: RoutesByType;
settingsRoutesByUpdate: Dispatch<SetStateAction<RoutesByType>>;
settingsLocal: LocalWidgetSettings;
settingsLocalUpdate: Dispatch<SetStateAction<LocalWidgetSettings>>;
settingsSignatures: SignatureSettingsType;
@@ -179,6 +186,8 @@ const MapRootContext = createContext<MapRootContextProps>({
setInterfaceSettings: () => null,
settingsRoutes: DEFAULT_ROUTES_SETTINGS,
settingsRoutesUpdate: () => null,
settingsRoutesBy: { ...DEFAULT_ROUTES_BY_SETTINGS, routes: { ...DEFAULT_ROUTES_BY_SETTINGS.routes } },
settingsRoutesByUpdate: () => null,
settingsLocal: DEFAULT_WIDGET_LOCAL_SETTINGS,
settingsLocalUpdate: () => null,
settingsSignatures: DEFAULT_SIGNATURE_SETTINGS,

View File

@@ -7,6 +7,7 @@ import {
MiniMapPlacement,
OnTheMapSettingsType,
PingsPlacement,
RoutesByType,
RoutesType,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import { DEFAULT_WIDGETS, STORED_VISIBLE_WIDGETS_DEFAULT } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
@@ -43,6 +44,12 @@ export const DEFAULT_WIDGET_LOCAL_SETTINGS: LocalWidgetSettings = {
showShipName: false,
};
export const DEFAULT_ROUTES_BY_SETTINGS: RoutesByType = {
routes: DEFAULT_ROUTES_SETTINGS,
scope: 'ALL',
type: 'blueLoot',
};
export const DEFAULT_ON_THE_MAP_SETTINGS: OnTheMapSettingsType = {
hideOffline: false,
};

View File

@@ -3,6 +3,7 @@ import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_MAP_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_BY_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps,
@@ -17,6 +18,11 @@ export const createWidgetSettings = <T>(settings: T) => {
};
export const createDefaultStoredSettings = (): MapUserSettings => {
const defaultRoutesBy = {
...DEFAULT_ROUTES_BY_SETTINGS,
routes: { ...DEFAULT_ROUTES_BY_SETTINGS.routes },
};
return {
version: STORED_SETTINGS_VERSION,
migratedFromOld: false,
@@ -24,6 +30,7 @@ export const createDefaultStoredSettings = (): MapUserSettings => {
localWidget: createWidgetSettings(DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createWidgetSettings(getDefaultWidgetProps()),
routes: createWidgetSettings(DEFAULT_ROUTES_SETTINGS),
routesBy: createWidgetSettings(defaultRoutesBy),
onTheMap: createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createWidgetSettings(DEFAULT_SIGNATURE_SETTINGS),
interface: createWidgetSettings(STORED_INTERFACE_DEFAULT_VALUES),
@@ -43,6 +50,11 @@ export const getDefaultSettingsByType = (type: SettingsTypes): SettingsWrapper<a
return createWidgetSettings(getDefaultWidgetProps());
case SettingsTypes.routes:
return createWidgetSettings(DEFAULT_ROUTES_SETTINGS);
case SettingsTypes.routesBy:
return createWidgetSettings({
...DEFAULT_ROUTES_BY_SETTINGS,
routes: { ...DEFAULT_ROUTES_BY_SETTINGS.routes },
});
case SettingsTypes.onTheMap:
return createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS);
case SettingsTypes.signaturesWidget:

View File

@@ -10,3 +10,4 @@ export * from './useCommandComments';
export * from './useGetCacheCharacter';
export * from './useCommandsActivity';
export * from './useCommandPings';
export * from './useCommandPingBlocked';

View File

@@ -0,0 +1,21 @@
import { useToast } from '@/hooks/Mapper/ToastProvider';
import { CommandPingBlocked } from '@/hooks/Mapper/types';
import { useCallback } from 'react';
export const useCommandPingBlocked = () => {
const { show } = useToast();
const pingBlocked = useCallback(
({ message }: CommandPingBlocked) => {
show({
severity: 'warn',
summary: 'Cannot create ping',
detail: message,
life: 5000,
});
},
[show],
);
return { pingBlocked };
};

View File

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

View File

@@ -24,10 +24,12 @@ export const useMapInit = () => {
user_permissions,
options,
is_subscription_active,
available_routes_by,
main_character_eve_id,
following_character_eve_id,
user_hubs,
map_slug,
expired_characters,
} = props;
const updateData: Partial<MapRootData> = {};
@@ -85,6 +87,10 @@ export const useMapInit = () => {
updateData.isSubscriptionActive = is_subscription_active;
}
if (available_routes_by) {
updateData.availableRoutesBy = available_routes_by;
}
if (system_static_infos) {
system_static_infos.forEach(static_info => {
addSystemStatic(static_info);
@@ -103,6 +109,10 @@ export const useMapInit = () => {
updateData.map_slug = map_slug;
}
if ('expired_characters' in props) {
updateData.expiredCharacters = expired_characters;
}
update(updateData);
},
[update, addSystemStatic],

View File

@@ -112,3 +112,23 @@ export const useUserRoutes = () => {
update({ userRoutes: value });
}, []);
};
export const useRoutesListBy = () => {
const {
update,
data: { routesListBy },
} = useMapRootState();
const ref = useRef({ update, routesListBy });
ref.current = { update, routesListBy };
return useCallback((value: CommandRoutes) => {
const { update, routesListBy } = ref.current;
if (areRoutesListsEqual(routesListBy, value)) {
return;
}
update({ routesListBy: value });
}, []);
};

View File

@@ -63,7 +63,6 @@ export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData =
const removeComment = useCallback((systemId: number, commentId: string) => {
const cSystem = commentBySystemsRef.current.get(systemId);
console.log('cSystem', cSystem);
if (!cSystem) {
return;
}

View File

@@ -12,6 +12,7 @@ import {
CommandLinkSignatureToSystem,
CommandMapUpdated,
CommandPingAdded,
CommandPingBlocked,
CommandPingCancelled,
CommandPresentCharacters,
CommandRemoveConnections,
@@ -29,6 +30,7 @@ import { ForwardedRef, useImperativeHandle } from 'react';
import {
useCommandComments,
useCommandPingBlocked,
useCommandPings,
useCommandsCharacters,
useCommandsConnections,
@@ -36,6 +38,7 @@ import {
useMapInit,
useMapUpdated,
useRoutes,
useRoutesListBy,
useUserRoutes,
} from './api';
@@ -59,8 +62,10 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
const mapUpdated = useMapUpdated();
const mapRoutes = useRoutes();
const mapUserRoutes = useUserRoutes();
const mapRoutesListBy = useRoutesListBy();
const { addComment, removeComment } = useCommandComments();
const { pingAdded, pingCancelled } = useCommandPings();
const { pingBlocked } = useCommandPingBlocked();
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
useImperativeHandle(ref, () => {
@@ -112,6 +117,9 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
case Commands.userRoutes:
mapUserRoutes(data as CommandRoutes);
break;
case Commands.routesListBy:
mapRoutesListBy(data as CommandRoutes);
break;
case Commands.signaturesUpdated: // USED
updateSystemSignatures(data as CommandSignaturesUpdated);
@@ -172,6 +180,9 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
case Commands.pingCancelled:
pingCancelled(data as CommandPingCancelled);
break;
case Commands.pingBlocked:
pingBlocked(data as CommandPingBlocked);
break;
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;

View File

@@ -56,6 +56,12 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
map_slug,
'routes',
);
const [settingsRoutesBy, settingsRoutesByUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'routesBy',
);
const [settingsLocal, settingsLocalUpdate] = useSettingsValueAndSetter(
mapUserSettings,
@@ -188,6 +194,8 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
setInterfaceSettings,
settingsRoutes,
settingsRoutesUpdate,
settingsRoutesBy,
settingsRoutesByUpdate,
settingsLocal,
settingsLocalUpdate,
settingsSignatures,

View File

@@ -1,5 +1,6 @@
import { to_1 } from './to_1.ts';
import { to_2 } from './to_2.ts';
import { to_3 } from './to_3.ts';
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export default [to_1, to_2] as MigrationStructure[];
export default [to_1, to_2, to_3] as MigrationStructure[];

View File

@@ -0,0 +1,31 @@
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { DEFAULT_ROUTES_BY_SETTINGS, DEFAULT_ROUTES_SETTINGS } from '@/hooks/Mapper/mapRootProvider/constants.ts';
export const to_3: MigrationStructure = {
to: 3,
up: (prev: any) => {
const rawRoutesBy = prev?.routesBy;
const hasStructuredRoutesBy =
rawRoutesBy && typeof rawRoutesBy === 'object' && 'routes' in rawRoutesBy;
const routes = hasStructuredRoutesBy
? { ...DEFAULT_ROUTES_SETTINGS, ...rawRoutesBy.routes }
: { ...DEFAULT_ROUTES_SETTINGS, ...(rawRoutesBy ?? prev?.routes ?? {}) };
const scopeRaw = hasStructuredRoutesBy ? rawRoutesBy?.scope : undefined;
const scope = scopeRaw === 'HIGH' ? 'HIGH' : 'ALL';
const type = hasStructuredRoutesBy && rawRoutesBy?.type ? rawRoutesBy.type : DEFAULT_ROUTES_BY_SETTINGS.type;
return {
...prev,
routesBy: {
...DEFAULT_ROUTES_BY_SETTINGS,
...(hasStructuredRoutesBy ? rawRoutesBy : {}),
scope,
type,
routes,
},
};
},
};

View File

@@ -47,6 +47,22 @@ export type RoutesType = {
avoid: number[];
};
export type RoutesByCategoryType =
| 'blueLoot'
| 'redLoot'
| 'thera'
| 'turnur'
| 'so_cleaning'
| 'trade_hubs';
export type RoutesByScopeType = 'ALL' | 'HIGH';
export type RoutesByType = {
routes: RoutesType;
scope: RoutesByScopeType;
type: RoutesByCategoryType;
};
export type LocalWidgetSettings = {
compact: boolean;
showOffline: boolean;
@@ -79,6 +95,7 @@ export type MapUserSettings = {
interface: SettingsWrapper<InterfaceStoredSettings>;
onTheMap: SettingsWrapper<OnTheMapSettingsType>;
routes: SettingsWrapper<RoutesType>;
routesBy: SettingsWrapper<RoutesByType>;
localWidget: SettingsWrapper<LocalWidgetSettings>;
signaturesWidget: SettingsWrapper<SignatureSettingsType>;
killsWidget: SettingsWrapper<KillsWidgetSettings>;
@@ -98,6 +115,7 @@ export enum SettingsTypes {
localWidget = 'localWidget',
widgets = 'widgets',
routes = 'routes',
routesBy = 'routesBy',
onTheMap = 'onTheMap',
signaturesWidget = 'signaturesWidget',
interface = 'interface',

View File

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

View File

@@ -3,6 +3,7 @@ import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Ma
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { RoutesByCategoryType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types/system.ts';
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
@@ -25,6 +26,7 @@ export enum Commands {
detailedKillsUpdated = 'detailed_kills_updated',
routes = 'routes',
userRoutes = 'user_routes',
routesListBy = 'routes_list_by',
centerSystem = 'center_system',
selectSystem = 'select_system',
selectSystems = 'select_systems',
@@ -41,6 +43,7 @@ export enum Commands {
refreshTrackingData = 'refresh_tracking_data',
pingAdded = 'ping_added',
pingCancelled = 'ping_cancelled',
pingBlocked = 'ping_blocked',
}
export type Command =
@@ -61,6 +64,7 @@ export type Command =
| Commands.detailedKillsUpdated
| Commands.routes
| Commands.userRoutes
| Commands.routesListBy
| Commands.selectSystem
| Commands.selectSystems
| Commands.centerSystem
@@ -77,7 +81,8 @@ export type Command =
| Commands.showTracking
| Commands.refreshTrackingData
| Commands.pingAdded
| Commands.pingCancelled;
| Commands.pingCancelled
| Commands.pingBlocked;
export type CommandInit = {
systems: SolarSystemRawType[];
@@ -99,9 +104,11 @@ export type CommandInit = {
options: MapOptions;
reset?: boolean;
is_subscription_active?: boolean;
available_routes_by?: RoutesByCategoryType[];
main_character_eve_id?: string | null;
following_character_eve_id?: string | null;
map_slug?: string;
expired_characters: string[];
};
export type CommandAddSystems = SolarSystemRawType[];
@@ -119,6 +126,7 @@ export type CommandSignaturesUpdated = string;
export type CommandMapUpdated = Partial<CommandInit>;
export type CommandRoutes = RoutesList;
export type CommandUserRoutes = RoutesList;
export type CommandRoutesListBy = RoutesList;
export type CommandKillsUpdated = Kill[];
export type CommandDetailedKillsUpdated = Record<string, DetailedKill[]>;
export type CommandSelectSystem = string | undefined;
@@ -161,6 +169,10 @@ export type CommandUpdateTracking = {
};
export type CommandPingAdded = PingData[];
export type CommandPingCancelled = Pick<PingData, 'type' | 'id'>;
export type CommandPingBlocked = {
reason: string;
message: string;
};
export interface UserSettings {
primaryCharacterId?: string;
@@ -193,6 +205,7 @@ export interface CommandData {
[Commands.mapUpdated]: CommandMapUpdated;
[Commands.routes]: CommandRoutes;
[Commands.userRoutes]: CommandUserRoutes;
[Commands.routesListBy]: CommandRoutesListBy;
[Commands.killsUpdated]: CommandKillsUpdated;
[Commands.detailedKillsUpdated]: CommandDetailedKillsUpdated;
[Commands.selectSystem]: CommandSelectSystem;
@@ -212,6 +225,7 @@ export interface CommandData {
[Commands.refreshTrackingData]: CommandRefreshTrackingData;
[Commands.pingAdded]: CommandPingAdded;
[Commands.pingCancelled]: CommandPingCancelled;
[Commands.pingBlocked]: CommandPingBlocked;
}
export interface MapHandlers {
@@ -225,6 +239,7 @@ export enum OutCommand {
deleteUserHub = 'delete_user_hub',
getRoutes = 'get_routes',
getUserRoutes = 'get_user_routes',
getRoutesBy = 'get_routes_by',
getCharacterJumps = 'get_character_jumps',
getStructures = 'get_structures',
getSignatures = 'get_signatures',

View File

@@ -6,6 +6,7 @@ import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { MapOptions, PingData, UserPermissions } from '@/hooks/Mapper/types';
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
import { RoutesByCategoryType } from '@/hooks/Mapper/mapRootProvider/types.ts';
export type MapUnionTypes = {
wormholesData: Record<string, WormholeDataRaw>;
@@ -20,6 +21,8 @@ export type MapUnionTypes = {
systemSignatures: Record<string, SystemSignature[]>;
routes?: RoutesList;
userRoutes?: RoutesList;
routesListBy?: RoutesList;
availableRoutesBy?: RoutesByCategoryType[];
kills: Record<number, number>;
connections: SolarSystemConnection[];
userPermissions: Partial<UserPermissions>;

View File

@@ -13,12 +13,19 @@ export type SystemStaticInfoShort = Pick<
type MappedSystem = SolarSystemStaticInfoRaw | undefined;
export type RouteStationSummary = {
station_id: number;
station_name: string;
special?: boolean;
};
export type Route = {
destination: number;
has_connection: boolean;
origin: number;
systems?: number[];
mapped_systems?: MappedSystem[];
stations?: RouteStationSummary[];
success?: boolean;
};

View File

@@ -31,6 +31,7 @@ export type SignatureCustomInfo = {
k162Type?: string;
time_status?: number;
isCrit?: boolean;
mass_status?: number;
};
export type SystemSignature = {

View File

@@ -57,7 +57,7 @@ export default {
};
refreshZone.addEventListener('click', handleUpdate);
refreshZone.addEventListener('mouseover', handleUpdate);
// refreshZone.addEventListener('mouseover', handleUpdate);
this.updated();
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

View File

@@ -92,6 +92,31 @@ map_subscription_extra_hubs_10_price =
config_dir
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_HUBS_10_PRICE", 10_000_000)
# Parse promo codes from environment variable
# Format: "CODE1:10,CODE2:20" where numbers are discount percentages
promo_codes =
config_dir
|> get_var_from_path_or_env("WANDERER_PROMO_CODES", "")
|> case do
"" ->
%{}
codes_string ->
codes_string
|> String.split(",")
|> Enum.map(fn entry ->
case String.split(String.trim(entry), ":") do
[code, discount] ->
{String.upcase(String.trim(code)), String.to_integer(String.trim(discount))}
_ ->
nil
end
end)
|> Enum.reject(&is_nil/1)
|> Map.new()
end
map_connection_auto_expire_hours =
config_dir
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_AUTO_EXPIRE_HOURS", 24)
@@ -176,7 +201,8 @@ config :wanderer_app,
}
],
extra_characters_50: map_subscription_extra_characters_50_price,
extra_hubs_10: map_subscription_extra_hubs_10_price
extra_hubs_10: map_subscription_extra_hubs_10_price,
promo_codes: promo_codes
},
# Finch pool configuration - separate pools for different services
# ESI Character Tracking pool - high capacity for bulk character operations
@@ -264,7 +290,7 @@ config :logger,
case config_env() do
:prod -> "info"
:dev -> "info"
:test -> "debug"
:test -> "warning"
end
)
)

View File

@@ -16,6 +16,11 @@ defmodule WandererApp.Api.AccessList do
includes([:owner, :members])
default_fields([
:name,
:description
])
derive_filter?(true)
derive_sort?(true)
@@ -79,12 +84,15 @@ defmodule WandererApp.Api.AccessList do
attribute :name, :string do
allow_nil? false
public? true
end
attribute :description, :string do
allow_nil? true
public? true
end
# Note: api_key intentionally not public for security
attribute :api_key, :string do
allow_nil? true
end

View File

@@ -16,6 +16,14 @@ defmodule WandererApp.Api.AccessListMember do
includes([:access_list])
default_fields([
:name,
:eve_character_id,
:eve_corporation_id,
:eve_alliance_id,
:role
])
derive_filter?(true)
derive_sort?(true)
@@ -89,22 +97,27 @@ defmodule WandererApp.Api.AccessListMember do
attribute :name, :string do
allow_nil? false
public? true
end
attribute :eve_character_id, :string do
allow_nil? true
public? true
end
attribute :eve_corporation_id, :string do
allow_nil? true
public? true
end
attribute :eve_alliance_id, :string do
allow_nil? true
public? true
end
attribute :role, :atom do
default "viewer"
public? true
constraints(
one_of: [

View File

@@ -19,9 +19,10 @@ defmodule WandererApp.Api.Changes.InjectMapFromActor do
_other ->
# nil or unexpected return shape - check for direct map_id
# Check params (input), arguments, and attributes (in that order)
map_id = Map.get(changeset.params, :map_id) ||
Ash.Changeset.get_argument(changeset, :map_id) ||
Ash.Changeset.get_attribute(changeset, :map_id)
map_id =
Map.get(changeset.params, :map_id) ||
Ash.Changeset.get_argument(changeset, :map_id) ||
Ash.Changeset.get_attribute(changeset, :map_id)
case map_id do
nil ->

View File

@@ -39,6 +39,8 @@ defmodule WandererApp.Api.Character do
define(:active_by_user,
action: :active_by_user
)
define(:admin_all, action: :admin_all)
end
actions do
@@ -69,6 +71,10 @@ defmodule WandererApp.Api.Character do
filter(expr(user_id == ^arg(:user_id) and deleted == false))
end
read :admin_all do
prepare build(load: [:user])
end
read :last_active do
argument(:from, :utc_datetime, allow_nil?: false)

View File

@@ -13,6 +13,8 @@ defmodule WandererApp.Api.Map do
postgres do
repo(WandererApp.Repo)
table("maps_v1")
migration_defaults scopes: "'{wormholes}'"
end
json_api do
@@ -65,6 +67,8 @@ defmodule WandererApp.Api.Map do
)
define(:duplicate, action: :duplicate)
define(:admin_all, action: :admin_all)
define(:restore, action: :restore)
end
calculations do
@@ -105,12 +109,19 @@ defmodule WandererApp.Api.Map do
prepare WandererApp.Api.Preparations.FilterMapsByRoles
end
read :admin_all do
# Admin-only action that bypasses FilterMapsByRoles
# Returns ALL maps including soft-deleted ones with owner and ACLs loaded
prepare build(load: [:owner, :acls])
end
create :new do
accept [
:name,
:slug,
:description,
:scope,
:scopes,
:only_tracked_characters,
:owner_id,
:sse_enabled
@@ -135,6 +146,7 @@ defmodule WandererApp.Api.Map do
:slug,
:description,
:scope,
:scopes,
:only_tracked_characters,
:owner_id,
:sse_enabled
@@ -190,6 +202,14 @@ defmodule WandererApp.Api.Map do
change(set_attribute(:deleted, true))
end
update :restore do
# Admin-only action to restore a soft-deleted map
accept([])
require_atomic? false
change(set_attribute(:deleted, false))
end
update :update_api_key do
accept [:public_api_key]
require_atomic? false
@@ -198,6 +218,11 @@ defmodule WandererApp.Api.Map do
update :toggle_webhooks do
accept [:webhooks_enabled]
require_atomic? false
change after_action(fn _changeset, record, _context ->
WandererApp.Map.update_webhooks_enabled(record.id, record.webhooks_enabled)
{:ok, record}
end)
end
update :toggle_sse do
@@ -206,10 +231,15 @@ defmodule WandererApp.Api.Map do
# Validate subscription when enabling SSE
validate &validate_sse_subscription/2
change after_action(fn _changeset, record, _context ->
WandererApp.Map.update_sse_enabled(record.id, record.sse_enabled)
{:ok, record}
end)
end
create :duplicate do
accept [:name, :description, :scope, :only_tracked_characters]
accept [:name, :description, :scope, :scopes, :only_tracked_characters]
argument :source_map_id, :uuid, allow_nil?: false
argument :copy_acls, :boolean, default: true
argument :copy_user_settings, :boolean, default: true
@@ -225,9 +255,14 @@ defmodule WandererApp.Api.Map do
description =
Ash.Changeset.get_attribute(changeset, :description) || source_map.description
# Use provided scopes or fall back to source map scopes
scopes =
Ash.Changeset.get_attribute(changeset, :scopes) || source_map.scopes
changeset
|> Ash.Changeset.change_attribute(:description, description)
|> Ash.Changeset.change_attribute(:scope, source_map.scope)
|> Ash.Changeset.change_attribute(:scopes, scopes)
|> Ash.Changeset.change_attribute(
:only_tracked_characters,
source_map.only_tracked_characters
@@ -359,6 +394,24 @@ defmodule WandererApp.Api.Map do
public?(true)
end
attribute :scopes, {:array, :atom} do
default([:wormholes])
allow_nil?(true)
public?(true)
constraints(
items: [
one_of: [
:wormholes,
:hi,
:low,
:null,
:pochven
]
]
)
end
create_timestamp(:inserted_at)
update_timestamp(:updated_at)
end

View File

@@ -27,6 +27,11 @@ defmodule WandererApp.Api.MapCharacterSettings do
includes([:map, :character])
default_fields([
:tracked,
:followed
])
derive_filter?(true)
derive_sort?(true)
@@ -219,14 +224,17 @@ defmodule WandererApp.Api.MapCharacterSettings do
attribute :tracked, :boolean do
default false
public? true
allow_nil? true
end
attribute :followed, :boolean do
default false
public? true
allow_nil? true
end
# Note: These attributes are encrypted (AshCloak) and intentionally not public
attribute :solar_system_id, :integer
attribute :structure_id, :integer
attribute :station_id, :integer

View File

@@ -22,6 +22,19 @@ defmodule WandererApp.Api.MapConnection do
includes([:map])
default_fields([
:solar_system_source,
:solar_system_target,
:mass_status,
:time_status,
:ship_size_type,
:type,
:wormhole_type,
:count_of_passage,
:locked,
:custom_info
])
derive_filter?(true)
derive_sort?(true)
@@ -197,15 +210,20 @@ defmodule WandererApp.Api.MapConnection do
attributes do
uuid_primary_key :id
attribute :solar_system_source, :integer
attribute :solar_system_target, :integer
attribute :solar_system_source, :integer do
public? true
end
attribute :solar_system_target, :integer do
public? true
end
# where 0 - greater than half
# where 1 - less than half
# where 2 - critical less than 10%
attribute :mass_status, :integer do
default(0)
public? true
allow_nil?(true)
end
@@ -218,7 +236,7 @@ defmodule WandererApp.Api.MapConnection do
# 6 - EOL 48h
attribute :time_status, :integer do
default(0)
public? true
allow_nil?(true)
end
@@ -229,7 +247,7 @@ defmodule WandererApp.Api.MapConnection do
# where 4 - Capital
attribute :ship_size_type, :integer do
default(2)
public? true
allow_nil?(true)
end
@@ -238,21 +256,26 @@ defmodule WandererApp.Api.MapConnection do
# where 2 - Bridge
attribute :type, :integer do
default(0)
public? true
allow_nil?(true)
end
attribute :wormhole_type, :string
attribute :wormhole_type, :string do
public? true
end
attribute :count_of_passage, :integer do
default(0)
public? true
allow_nil?(true)
end
attribute :locked, :boolean
attribute :locked, :boolean do
public? true
end
attribute :custom_info, :string do
public? true
allow_nil? true
end

View File

@@ -23,6 +23,10 @@ defmodule WandererApp.Api.MapDefaultSettings do
:updated_by
])
default_fields([
:settings
])
routes do
base("/map_default_settings")
@@ -93,6 +97,7 @@ defmodule WandererApp.Api.MapDefaultSettings do
attribute :settings, :string do
allow_nil? false
public? true
constraints min_length: 2
description "JSON string containing the default map settings"
end

View File

@@ -80,6 +80,10 @@ defmodule WandererApp.Api.MapPing do
filter(expr(inserted_at <= ^arg(:inserted_before)))
end
# Admin action for cleanup - no actor filtering
read :all_pings do
end
end
attributes do

View File

@@ -18,6 +18,15 @@ defmodule WandererApp.Api.MapSubscription do
:map
])
default_fields([
:plan,
:status,
:characters_limit,
:hubs_limit,
:active_till,
:auto_renew?
])
# Enable automatic filtering and sorting
derive_filter?(true)
derive_sort?(true)
@@ -135,6 +144,7 @@ defmodule WandererApp.Api.MapSubscription do
attribute :plan, :atom do
default "alpha"
public? true
constraints(
one_of: [
@@ -150,6 +160,7 @@ defmodule WandererApp.Api.MapSubscription do
attribute :status, :atom do
default "active"
public? true
constraints(
one_of: [
@@ -164,22 +175,24 @@ defmodule WandererApp.Api.MapSubscription do
attribute :characters_limit, :integer do
default(100)
public? true
allow_nil?(true)
end
attribute :hubs_limit, :integer do
default(10)
public? true
allow_nil?(true)
end
attribute :active_till, :utc_datetime do
allow_nil? true
public? true
end
attribute :auto_renew?, :boolean do
allow_nil? false
public? true
end
create_timestamp(:inserted_at)

View File

@@ -19,6 +19,10 @@ defmodule WandererApp.Api.MapSystemComment do
:character
])
default_fields([
:text
])
routes do
base("/map_system_comments")
@@ -73,6 +77,7 @@ defmodule WandererApp.Api.MapSystemComment do
attribute :text, :string do
allow_nil? false
public? true
end
create_timestamp(:inserted_at)

View File

@@ -16,6 +16,20 @@ defmodule WandererApp.Api.MapSystemSignature do
includes([:system])
default_fields([
:eve_id,
:character_eve_id,
:name,
:description,
:temporary_name,
:type,
:linked_system_id,
:kind,
:group,
:custom_info,
:deleted
])
derive_filter?(true)
derive_sort?(true)
@@ -109,7 +123,8 @@ defmodule WandererApp.Api.MapSystemSignature do
:group,
:type,
:custom_info,
:deleted
:deleted,
:linked_system_id
]
end
@@ -126,7 +141,8 @@ defmodule WandererApp.Api.MapSystemSignature do
:type,
:custom_info,
:deleted,
:update_forced_at
:update_forced_at,
:linked_system_id
]
primary? true
@@ -184,42 +200,56 @@ defmodule WandererApp.Api.MapSystemSignature do
attribute :eve_id, :string do
allow_nil? false
public? true
end
attribute :character_eve_id, :string do
allow_nil? false
public? true
end
attribute :name, :string do
allow_nil? true
public? true
end
attribute :description, :string do
allow_nil? true
public? true
end
attribute :temporary_name, :string do
allow_nil? true
public? true
end
attribute :type, :string do
allow_nil? true
public? true
end
attribute :linked_system_id, :integer do
allow_nil? true
public? true
end
attribute :kind, :string
attribute :group, :string
attribute :kind, :string do
public? true
end
attribute :group, :string do
public? true
end
attribute :custom_info, :string do
allow_nil? true
public? true
end
attribute :deleted, :boolean do
allow_nil? false
default false
public? true
end
attribute :update_forced_at, :utc_datetime do

View File

@@ -41,6 +41,21 @@ defmodule WandererApp.Api.MapSystemStructure do
:system
])
default_fields([
:structure_type_id,
:structure_type,
:character_eve_id,
:solar_system_name,
:solar_system_id,
:name,
:notes,
:owner_name,
:owner_ticker,
:owner_id,
:status,
:end_time
])
# Enable automatic filtering and sorting
derive_filter?(true)
derive_sort?(true)
@@ -151,50 +166,62 @@ defmodule WandererApp.Api.MapSystemStructure do
attribute :structure_type_id, :string do
allow_nil? false
public? true
end
attribute :structure_type, :string do
allow_nil? false
public? true
end
attribute :character_eve_id, :string do
allow_nil? false
public? true
end
attribute :solar_system_name, :string do
allow_nil? false
public? true
end
attribute :solar_system_id, :integer do
allow_nil? false
public? true
end
attribute :name, :string do
allow_nil? false
public? true
end
attribute :notes, :string do
allow_nil? true
public? true
end
attribute :owner_name, :string do
allow_nil? true
public? true
end
attribute :owner_ticker, :string do
allow_nil? true
public? true
end
attribute :owner_id, :string do
allow_nil? true
public? true
end
attribute :status, :string do
allow_nil? true
public? true
end
attribute :end_time, :utc_datetime_usec do
allow_nil? true
public? true
end
create_timestamp :inserted_at

View File

@@ -5,6 +5,8 @@ defmodule WandererApp.Api.MapTransaction do
domain: WandererApp.Api,
data_layer: AshPostgres.DataLayer
import Ecto.Query
postgres do
repo(WandererApp.Repo)
table("map_transactions_v1")
@@ -19,6 +21,7 @@ defmodule WandererApp.Api.MapTransaction do
define(:by_map, action: :by_map)
define(:by_user, action: :by_user)
define(:create, action: :create)
define(:top_donators, action: :top_donators)
end
actions do
@@ -45,6 +48,35 @@ defmodule WandererApp.Api.MapTransaction do
argument(:user_id, :uuid, allow_nil?: false)
filter(expr(user_id == ^arg(:user_id)))
end
action :top_donators, {:array, :struct} do
argument(:map_id, :string, allow_nil?: false)
argument(:after, :utc_datetime, allow_nil?: true)
run fn input, _context ->
base =
from(t in __MODULE__,
where:
t.map_id == ^input.arguments.map_id and
t.type == :in and
not is_nil(t.user_id),
group_by: [t.user_id],
select: %{user_id: t.user_id, total_amount: sum(t.amount)},
order_by: [desc: sum(t.amount)],
limit: 10
)
query =
case input.arguments[:after] do
nil -> base
after_date -> base |> where([t], t.inserted_at >= ^after_date)
end
query
|> WandererApp.Repo.all()
|> then(&{:ok, &1})
end
end
end
attributes do

View File

@@ -24,6 +24,13 @@ defmodule WandererApp.Api.MapUserSettings do
:user
])
default_fields([
:settings,
:main_character_eve_id,
:following_character_eve_id,
:hubs
])
routes do
base("/map_user_settings")
@@ -85,19 +92,22 @@ defmodule WandererApp.Api.MapUserSettings do
attribute :settings, :string do
allow_nil? true
public? true
end
attribute :main_character_eve_id, :string do
allow_nil? true
public? true
end
attribute :following_character_eve_id, :string do
allow_nil? true
public? true
end
attribute :hubs, {:array, :string} do
allow_nil?(true)
public? true
default([])
end
end

View File

@@ -45,7 +45,17 @@ defmodule WandererApp.Api.MapWebhookSubscription do
:active?
]
defaults [:read, :destroy]
defaults [:read]
# Custom destroy to invalidate cache
destroy :destroy do
require_atomic? false
change after_action(fn _changeset, record, _context ->
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
{:ok, record}
end)
end
update :update do
accept [
@@ -60,6 +70,12 @@ defmodule WandererApp.Api.MapWebhookSubscription do
]
require_atomic? false
# Invalidate cache when subscription is updated
change after_action(fn _changeset, record, _context ->
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
{:ok, record}
end)
end
read :by_map do
@@ -124,6 +140,12 @@ defmodule WandererApp.Api.MapWebhookSubscription do
secret = generate_webhook_secret()
Ash.Changeset.force_change_attribute(changeset, :secret, secret)
end
# Invalidate cache when subscription is created
change after_action(fn _changeset, record, _context ->
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
{:ok, record}
end)
end
update :rotate_secret do
@@ -134,6 +156,11 @@ defmodule WandererApp.Api.MapWebhookSubscription do
new_secret = generate_webhook_secret()
Ash.Changeset.change_attribute(changeset, :secret, new_secret)
end
change after_action(fn _changeset, record, _context ->
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
{:ok, record}
end)
end
end

View File

@@ -31,6 +31,13 @@ defmodule WandererApp.Api.UserActivity do
includes([:character, :user])
default_fields([
:entity_id,
:entity_type,
:event_type,
:event_data
])
derive_filter?(true)
derive_sort?(true)
@@ -86,10 +93,12 @@ defmodule WandererApp.Api.UserActivity do
attribute :entity_id, :string do
allow_nil? false
public? true
end
attribute :entity_type, :atom do
default "map"
public? true
constraints(
one_of: [
@@ -104,6 +113,7 @@ defmodule WandererApp.Api.UserActivity do
attribute :event_type, :atom do
default "custom"
public? true
constraints(
one_of: [
@@ -153,7 +163,9 @@ defmodule WandererApp.Api.UserActivity do
allow_nil?(false)
end
attribute :event_data, :string
attribute :event_data, :string do
public? true
end
create_timestamp(:inserted_at)
update_timestamp(:updated_at)

View File

@@ -86,6 +86,11 @@ defmodule WandererApp.Application do
Supervisor.child_spec({Cachex, name: :wanderer_app_cache},
id: :wanderer_app_cache_worker
),
# Cache for webhook subscriptions - 5 minute TTL to reduce DB load
Supervisor.child_spec(
{Cachex, name: :webhook_subscriptions_cache, default_ttl: :timer.minutes(5)},
id: :webhook_subscriptions_cache_worker
),
{Registry, keys: :unique, name: WandererApp.Character.TrackerRegistry},
{PartitionSupervisor,
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
@@ -112,6 +117,7 @@ defmodule WandererApp.Application do
WandererApp.Scheduler,
WandererApp.Server.ServerStatusTracker,
WandererApp.Server.TheraDataFetcher,
WandererApp.Server.TurnurDataFetcher,
{WandererApp.Character.TrackerPoolSupervisor, []},
{WandererApp.Map.MapPoolSupervisor, []},
WandererApp.Character.TrackerManager,

View File

@@ -45,7 +45,7 @@ defmodule WandererApp.Cache do
def insert({id, key}, value, opts) when is_binary(id) and (is_binary(key) or is_atom(key)),
do: insert("#{id}:#{key}", value, opts)
def insert(key, nil, opts) when is_binary(key) or is_atom(key), do: delete(key)
def insert(key, nil, _opts) when is_binary(key) or is_atom(key), do: delete(key)
def insert(key, value, opts) when is_binary(key) or is_atom(key), do: put(key, value, opts)
def insert_or_update(key, value, update_fn, opts \\ [])

View File

@@ -265,6 +265,10 @@ defmodule WandererApp.Character.Tracker do
end
_ ->
Logger.debug(fn ->
"[Tracker] update_online skipped for character #{character_id} - no valid access token"
end)
{:error, :skipped}
end
end
@@ -598,12 +602,13 @@ defmodule WandererApp.Character.Tracker do
{:error, :skipped}
end
_ ->
{:error, :skipped}
end
_ ->
Logger.debug(fn ->
"[Tracker] update_location skipped for character #{character_id} - no valid access token"
end)
{:error, :skipped}
end
end
@@ -734,6 +739,14 @@ defmodule WandererApp.Character.Tracker do
{:character_alliance, {character_id, character_update}}
)
# Broadcast permission update to trigger LiveView refresh
# This ensures users are kicked off maps they no longer have access to
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character.eve_id}",
:update_permissions
)
state
|> Map.merge(%{alliance_id: nil})
end
@@ -772,6 +785,14 @@ defmodule WandererApp.Character.Tracker do
{:character_alliance, {character_id, character_update}}
)
# Broadcast permission update to trigger LiveView refresh
# This ensures users are kicked off maps they no longer have access to
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character.eve_id}",
:update_permissions
)
state
|> Map.merge(%{alliance_id: alliance_id})
@@ -799,7 +820,7 @@ defmodule WandererApp.Character.Tracker do
corporation_id
|> WandererApp.Esi.get_corporation_info()
|> case do
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker}} ->
{:ok, character} =
WandererApp.Character.get_character(character_id)
@@ -826,6 +847,14 @@ defmodule WandererApp.Character.Tracker do
}}}
)
# Broadcast permission update to trigger LiveView refresh
# This ensures users are kicked off maps they no longer have access to
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character.eve_id}",
:update_permissions
)
state
|> Map.merge(%{corporation_id: corporation_id})
@@ -1002,7 +1031,7 @@ defmodule WandererApp.Character.Tracker do
defp maybe_update_active_maps(
%{character_id: character_id, active_maps: active_maps} =
state,
%{map_id: map_id, track: true} = track_settings
%{map_id: map_id, track: true}
) do
if not Enum.member?(active_maps, map_id) do
WandererApp.Cache.put(

View File

@@ -40,10 +40,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
Logger.debug("[TrackerManager] Initialized with intervals: " <>
"garbage_collection=#{div(@garbage_collection_interval, 60_000)}min, " <>
"untrack=#{div(@untrack_characters_interval, 60_000)}min, " <>
"inactive_timeout=#{div(@inactive_character_timeout, 60_000)}min")
Logger.debug(
"[TrackerManager] Initialized with intervals: " <>
"garbage_collection=#{div(@garbage_collection_interval, 60_000)}min, " <>
"untrack=#{div(@untrack_characters_interval, 60_000)}min, " <>
"inactive_timeout=#{div(@inactive_character_timeout, 60_000)}min"
)
%{
characters: [],
@@ -57,7 +59,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
WandererApp.Cache.insert("tracked_characters", [])
if length(tracked_characters) > 0 do
Logger.debug("[TrackerManager] Restoring #{length(tracked_characters)} tracked characters from cache")
Logger.debug(
"[TrackerManager] Restoring #{length(tracked_characters)} tracked characters from cache"
)
end
tracked_characters
@@ -197,6 +201,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
[],
fn untrack_queue ->
original_length = length(untrack_queue)
filtered =
untrack_queue
|> Enum.reject(fn {m_id, c_id} -> m_id == map_id and c_id == character_id end)

View File

@@ -88,15 +88,4 @@ defmodule WandererApp.Character.TrackerPoolDynamicSupervisor do
{:ok, pid}
end
end
defp stop_child(uuid) do
case Registry.lookup(@registry, uuid) do
[{pid, _}] ->
GenServer.cast(pid, :stop)
_ ->
Logger.warn("Unable to locate pool assigned to #{inspect(uuid)}")
:ok
end
end
end

View File

@@ -38,7 +38,7 @@ defmodule WandererApp.Character.TrackingConfigUtils do
%{id: "default", title: "Default", value: default_count}
]
{:ok, pools_count} =
{:ok, _pools_count} =
Cachex.get(
:esi_auth_cache,
"configs_total_count"

View File

@@ -56,13 +56,7 @@ defmodule WandererApp.Character.TrackingUtils do
Only includes characters that have actual tracking permission.
"""
def build_tracking_data(map_id, current_user_id) do
with {:ok, map} <-
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
),
with {:ok, map} <- WandererApp.MapRepo.get(map_id),
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
{:ok, %{characters: characters_with_access}} <-
WandererApp.Maps.load_characters(map, current_user_id) do
@@ -75,7 +69,11 @@ defmodule WandererApp.Character.TrackingUtils do
build_character_tracking_data(characters_with_tracking_permission)
{:ok, main_character} =
get_main_character(user_settings, characters_with_tracking_permission, characters_with_tracking_permission)
get_main_character(
user_settings,
characters_with_tracking_permission,
characters_with_tracking_permission
)
following_character_eve_id =
case user_settings do
@@ -195,7 +193,13 @@ defmodule WandererApp.Character.TrackingUtils do
{true, settings_result} ->
case check_character_tracking_permission(character, map_id) do
{:ok, :allowed} ->
do_update_character_tracking_impl(character, map_id, track, caller_pid, settings_result)
do_update_character_tracking_impl(
character,
map_id,
track,
caller_pid,
settings_result
)
{:error, reason} ->
Logger.warning(

View File

@@ -42,6 +42,35 @@ defmodule WandererApp.Env do
def corp_eve_id(), do: get_key(:corp_id, -1)
def subscription_settings(), do: get_key(:subscription_settings)
@doc """
Returns the promo code configuration map.
Keys are uppercase code strings, values are discount percentages.
"""
def promo_codes() do
case subscription_settings() do
%{promo_codes: codes} when is_map(codes) -> codes
_ -> %{}
end
end
@doc """
Validates a promo code and returns the discount percentage.
Returns {:ok, discount_percent} if valid, {:error, :invalid_code} otherwise.
Codes are case-insensitive.
"""
def validate_promo_code(nil), do: {:error, :invalid_code}
def validate_promo_code(""), do: {:error, :invalid_code}
def validate_promo_code(code) when is_binary(code) do
normalized = String.upcase(String.trim(code))
case Map.get(promo_codes(), normalized) do
nil -> {:error, :invalid_code}
discount when is_integer(discount) and discount > 0 and discount <= 100 -> {:ok, discount}
_ -> {:error, :invalid_code}
end
end
@decorate cacheable(
cache: WandererApp.Cache,
key: "restrict_maps_creation"

View File

@@ -8,7 +8,6 @@ defmodule WandererApp.Esi.ApiClient do
@ttl :timer.hours(1)
@wanderrer_user_agent "(wanderer-industries@proton.me; +https://github.com/wanderer-industries/wanderer)"
@req_esi_options [base_url: "https://esi.evetech.net", finch: WandererApp.Finch]
@cache_opts [cache: true]
@retry_opts [retry: false, retry_log_level: :warning]
@@ -74,7 +73,7 @@ defmodule WandererApp.Esi.ApiClient do
|> Keyword.merge(@timeout_opts)
)
def get_routes_eve(hubs, origin, params, opts),
def get_routes_eve(hubs, origin, _params, _opts),
do:
{:ok,
hubs
@@ -101,33 +100,6 @@ defmodule WandererApp.Esi.ApiClient do
end
end)}
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,
"destination" => destination,
"systems" => result,
"success" => true
}
error ->
Logger.warning("Error getting routes: #{inspect(error)}")
%{"origin" => origin, "destination" => destination, "systems" => [], "success" => false}
end
end
@decorate cacheable(
cache: Cache,
key: "group-info-#{group_id}",
@@ -273,6 +245,8 @@ defmodule WandererApp.Esi.ApiClient do
opts: [ttl: @ttl]
)
defp get_search(character_eve_id, search_val, categories_val, merged_opts) do
# Note: search_val and categories_val are used by the @decorate cacheable annotation above
_unused = {search_val, categories_val}
get_character_auth_data(character_eve_id, "search", merged_opts)
end
@@ -348,7 +322,7 @@ defmodule WandererApp.Esi.ApiClient do
defp with_cache_opts(opts),
do: opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
defp do_get(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
defp do_get(path, api_opts, opts, pool \\ @general_pool) do
case Cachex.get(:api_cache, path) do
{:ok, cached_data} when not is_nil(cached_data) ->
{:ok, cached_data}
@@ -358,7 +332,7 @@ defmodule WandererApp.Esi.ApiClient do
end
end
defp do_get_request(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
defp do_get_request(path, api_opts, opts, pool) do
try do
req_options_for_pool(pool)
|> Req.new()
@@ -448,7 +422,7 @@ defmodule WandererApp.Esi.ApiClient do
{:ok, %{status: status} = _error} when status in [401, 403] ->
do_get_retry(path, api_opts, opts)
{:ok, %{status: status, headers: headers}} ->
{:ok, %{status: status}} ->
{:error, "Unexpected status: #{status}"}
{:error, %Mint.TransportError{reason: :timeout}} ->
@@ -832,10 +806,10 @@ defmodule WandererApp.Esi.ApiClient do
defp handle_refresh_token_result(
{:error, %OAuth2.Error{reason: :econnrefused} = error},
character,
_character,
character_id,
expires_at,
scopes
_scopes
) do
expires_at_datetime = DateTime.from_unix!(expires_at)
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
@@ -859,20 +833,44 @@ defmodule WandererApp.Esi.ApiClient do
defp handle_refresh_token_result(
{:error, %OAuth2.Error{} = error},
character,
_character,
character_id,
expires_at,
scopes
_scopes
) do
invalidate_character_tokens(character, character_id, expires_at, scopes)
Logger.warning("Failed to refresh token for #{character_id}: #{inspect(error)}")
{:error, :invalid_grant}
time_since_expiry = DateTime.diff(DateTime.utc_now(), DateTime.from_unix!(expires_at), :second)
Logger.warning("TOKEN_REFRESH_FAILED: Transient OAuth2 error during token refresh",
character_id: character_id,
error: inspect(error),
time_since_expiry_seconds: time_since_expiry
)
:telemetry.execute([:wanderer_app, :token, :refresh_failed], %{count: 1}, %{
character_id: character_id,
error_type: "oauth2_error",
time_since_expiry: time_since_expiry
})
{:error, :token_refresh_failed}
end
defp handle_refresh_token_result(error, character, character_id, expires_at, scopes) do
Logger.warning("Failed to refresh token for #{character_id}: #{inspect(error)}")
invalidate_character_tokens(character, character_id, expires_at, scopes)
{:error, :failed}
defp handle_refresh_token_result(error, _character, character_id, expires_at, _scopes) do
time_since_expiry = DateTime.diff(DateTime.utc_now(), DateTime.from_unix!(expires_at), :second)
Logger.warning("TOKEN_REFRESH_FAILED: Unexpected error during token refresh",
character_id: character_id,
error: inspect(error),
time_since_expiry_seconds: time_since_expiry
)
:telemetry.execute([:wanderer_app, :token, :refresh_failed], %{count: 1}, %{
character_id: character_id,
error_type: "unexpected_error",
time_since_expiry: time_since_expiry
})
{:error, :token_refresh_failed}
end
defp invalidate_character_tokens(character, character_id, expires_at, scopes) do
@@ -880,7 +878,6 @@ defmodule WandererApp.Esi.ApiClient do
with {:ok, _} <- WandererApp.Api.Character.update(character, attrs) do
WandererApp.Character.update_character(character_id, attrs)
:ok
else
error ->
Logger.error("Failed to clear tokens for #{character_id}: #{inspect(error)}")
@@ -891,5 +888,7 @@ defmodule WandererApp.Esi.ApiClient do
"character:#{character_id}",
:character_token_invalid
)
:ok
end
end

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