Compare commits

...

193 Commits

Author SHA1 Message Date
Dmitry Popov
1b4852d5b4 Merge pull request #517 from jackmurray/show-temp-name
Show system temp name when linking signatures
2025-10-10 00:00:41 +04:00
CI
5343c34488 chore: [skip ci] 2025-10-09 19:57:07 +00:00
CI
4878be1a53 chore: release version v1.81.5 2025-10-09 19:57:07 +00:00
Dmitry Popov
1ff689c26c fix(Core): Update connection ship size based on linked signature type 2025-10-09 21:56:23 +02:00
CI
79b660e899 chore: [skip ci] 2025-10-09 18:45:16 +00:00
CI
665a679bd5 chore: release version v1.81.4 2025-10-09 18:45:16 +00:00
Dmitry Popov
7bd634eb95 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-10-09 20:44:42 +02:00
Dmitry Popov
c3b5a77a86 fix(Core): Fixed signature to system link issues 2025-10-09 20:44:33 +02:00
CI
12f39a0133 chore: [skip ci] 2025-10-07 20:59:47 +00:00
CI
ffc2a86e95 chore: release version v1.81.3 2025-10-07 20:59:47 +00:00
Dmitry Popov
82babf41a2 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-10-07 22:59:10 +02:00
Dmitry Popov
81055b4fbd fix(Core): Fixed cancel ping errors 2025-10-07 22:59:01 +02:00
CI
5070a59f88 chore: [skip ci] 2025-10-07 20:49:34 +00:00
CI
65d5bf960d chore: release version v1.81.2 2025-10-07 20:49:34 +00:00
Dmitry Popov
8fc4cb190e Merge pull request #526 from guarzo/guarzo/apicustom
fix: api dropping custom name
2025-10-08 00:49:00 +04:00
Guarzo
095a4b2362 fix: api dropping custom name 2025-10-06 15:52:35 -04:00
CI
fafc631e49 chore: [skip ci] 2025-10-02 21:37:58 +00:00
CI
e56383c8b1 chore: release version v1.81.1 2025-10-02 21:37:58 +00:00
Dmitry Popov
b9c26bdb04 fix(Core): Fixed characters tracking updates. 2025-10-02 23:37:23 +02:00
CI
8aeaa81752 chore: [skip ci] 2025-10-02 16:09:27 +00:00
CI
b16ec0490f chore: release version v1.81.0 2025-10-02 16:09:27 +00:00
Dmitry Popov
eceaf1d73b Merge pull request #523 from dedo1911/feat/pwa
feat(core): fix pwa icons + add screen in manifest
2025-10-02 20:08:57 +04:00
dedo1911
34cf668a33 feat(core): fix pwa icons + add screen in manifest 2025-10-02 17:57:52 +02:00
CI
c22d410c9f chore: [skip ci] 2025-10-02 11:39:21 +00:00
CI
fc6af867f2 chore: release version v1.80.0 2025-10-02 11:39:21 +00:00
Dmitry Popov
2d96114984 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-10-02 13:38:50 +02:00
Dmitry Popov
fd7e19e490 feat(Core): Added PWA web manifest 2025-10-02 13:38:46 +02:00
CI
f7d996f5b2 chore: [skip ci] 2025-10-01 14:32:20 +00:00
CI
f8ab1383ab chore: release version v1.79.6 2025-10-01 14:32:20 +00:00
Dmitry Popov
e1559aac94 fix(Core): Fixed modals auto-save on Enter. 2025-10-01 16:31:48 +02:00
CI
2e17cce5cd chore: [skip ci] 2025-10-01 13:57:58 +00:00
CI
8fb831f171 chore: release version v1.79.5 2025-10-01 13:57:58 +00:00
Dmitry Popov
cb84f34515 fix(Core): Fixed system details modal auto-save on Enter. 2025-10-01 15:57:26 +02:00
CI
272cce1a77 chore: [skip ci] 2025-09-30 13:00:53 +00:00
CI
e0e3ed1580 chore: release version v1.79.4 2025-09-30 13:00:53 +00:00
Dmitry Popov
c4c848cf37 fix(Core): Fixed updating connection time status based on linked signature data. Fixed FR gas sites parsing.
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-09-30 15:00:14 +02:00
CI
32d25d86eb chore: [skip ci] 2025-09-27 15:37:16 +00:00
CI
863adccac1 chore: release version v1.79.3 2025-09-27 15:37:16 +00:00
Dmitry Popov
2d527e1d16 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-27 17:36:40 +02:00
Dmitry Popov
9a64ad6fa7 fix(Core): Fixed connection passages count 2025-09-27 17:36:35 +02:00
CI
5ce472ebff chore: [skip ci] 2025-09-26 18:28:30 +00:00
CI
76588af12f chore: release version v1.79.2 2025-09-26 18:28:30 +00:00
Dmitry Popov
134f169eb9 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-26 20:28:01 +02:00
Dmitry Popov
7c2d731c4c chore: fix 2025-09-26 20:27:54 +02:00
CI
c7e2a290cf chore: [skip ci] 2025-09-26 18:27:38 +00:00
CI
5ea966892a chore: release version v1.79.1 2025-09-26 18:27:38 +00:00
Dmitry Popov
b879db76b7 chore: fix 2025-09-26 20:27:06 +02:00
CI
d13a628029 chore: [skip ci] 2025-09-26 18:18:51 +00:00
CI
7c1e2595e3 chore: release version v1.79.0 2025-09-26 18:18:51 +00:00
Dmitry Popov
a99e8a915e Merge pull request #522 from wanderer-industries/update-lifetime
Update lifetime
2025-09-26 22:18:20 +04:00
Dmitry Popov
36f424da0b Merge branch 'main' into update-lifetime 2025-09-26 15:45:19 +02:00
Dmitry Popov
c0a65d5a23 Merge branch 'main' into update-lifetime 2025-09-26 00:57:26 +02:00
Dmitry Popov
02e31333d2 chore: fix 2025-09-26 00:54:55 +02:00
Dmitry Popov
d69616119d feat(Core): Updated connections EOL logic 2025-09-26 00:54:14 +02:00
Dmitry Popov
dbc770d40b Merge branch 'update-lifetime' of github.com:wanderer-industries/wanderer into update-lifetime 2025-09-24 18:44:10 +02:00
CI
e69a8fece5 chore: [skip ci] 2025-09-24 16:38:54 +00:00
CI
cf20be8a77 chore: release version v1.78.1 2025-09-24 16:38:54 +00:00
Dmitry Popov
450bcb649c Merge pull request #521 from wanderer-industries/kills-fix
Kills fix
2025-09-24 20:38:29 +04:00
Dmitry Popov
a00395351e Merge pull request #513 from guarzo/guarzo/killfilter
fix: removed wormhole only logic error
2025-09-24 20:33:00 +04:00
DanSylvest
3b24c760ff fix(Map): Fixed eslint problems 2025-09-24 13:12:32 +03:00
DanSylvest
3801f0be18 Merge branch 'main' into update-lifetime
# Conflicts:
#	assets/js/hooks/Mapper/components/map/components/ContextMenuConnection/ContextMenuConnection.tsx
#	assets/js/hooks/Mapper/components/mapRootContent/components/SignatureSettings/SignatureSettings.tsx
2025-09-24 13:11:02 +03:00
Dmitry Popov
f3104db2e4 Merge branch 'update-lifetime' of github.com:wanderer-industries/wanderer into update-lifetime 2025-09-23 20:19:53 +02:00
CI
602b1028c3 chore: [skip ci] 2025-09-23 16:27:20 +00:00
CI
4f98e979a2 chore: release version v1.78.0 2025-09-23 16:27:20 +00:00
Dmitry Popov
e0f46c4af7 Merge pull request #519 from wanderer-industries/jumpgates
Jumpgates
2025-09-23 20:23:25 +04:00
Dmitry Popov
bc8a9a2b85 Merge branch 'main' into jumpgates 2025-09-23 18:23:11 +02:00
Dmitry Popov
c2b03f925d Merge pull request #518 from leesolway/drag-dialog
Drag signature dialog
2025-09-23 20:22:47 +04:00
Dmitry Popov
efa2e52054 Merge branch 'main' into jumpgates 2025-09-23 18:18:32 +02:00
Dmitry Popov
cedf5761f8 Merge branch 'main' into jumpgates 2025-09-23 17:51:45 +02:00
Dmitry Popov
f601bb8751 Merge branch 'main' into jumpgates 2025-09-23 17:48:59 +02:00
Dmitry Popov
c39d2a56d2 Merge branch 'main' into jumpgates 2025-09-23 17:19:05 +02:00
Lee Solway
805722bbe8 Merge branch 'wanderer-industries:main' into drag-dialog 2025-09-20 16:05:34 +01:00
Lee Solway
fe3e38343b SystemSettingsDialog & SignatureSettings draggable 2025-09-19 17:35:28 +01:00
DanSylvest
616e82c497 fix(Map): Add support for Bridge. Made all tooltips left and right paddings. 2025-09-18 11:52:16 +03:00
guarzo
ab7e47b91f pr feedback 2025-09-18 06:18:23 +00:00
guarzo
cf1c103a46 fix: pr feedback 2025-09-17 15:50:26 +00:00
Jack
4bfe60b75c preferentially display the system's temporary name if it has one 2025-09-16 19:58:21 +00:00
Jack
6a44d10c56 enable display of custom name on the connection dropdown 2025-09-16 19:58:00 +00:00
CI
71202a4a29 chore: [skip ci] 2025-09-14 15:00:45 +00:00
CI
a7e0ceac4c chore: release version v1.77.19 2025-09-14 15:00:45 +00:00
Aleksei Chichenkov
6bce701aab Merge pull request #515 from wanderer-industries/wh-db-fixed
fix(Map): Fixed for all Large wormholes jump mass from 300 to 375. Fi…
2025-09-14 18:00:20 +03:00
CI
f8b9e206a5 chore: [skip ci] 2025-09-13 21:53:27 +00:00
CI
4c1ec2004b chore: release version v1.77.18 2025-09-13 21:53:27 +00:00
Dmitry Popov
ebed74d239 Revert "fix: Updated ACL create/update APIs"
This reverts commit b6c680e802.
2025-09-13 23:52:02 +02:00
DanSylvest
c789b69b54 fix(Map): Update lifetime design and buttons 2025-09-13 19:17:00 +03:00
Dmitry Popov
24c32511d8 Merge branch 'main' into jumpgates 2025-09-13 11:30:40 +02:00
Dmitry Popov
302fb0642d chore: update connection time values 2025-09-13 11:24:11 +02:00
DanSylvest
06e7b6e3eb fix(Map): Fixed for all Large wormholes jump mass from 300 to 375. Fixed jump mass and total mass for N290, K329. Fixed static for J005663 was H296 now Y790. Added J492 wormhole. Change lifetime for E587 from 16 to 48 2025-09-12 19:37:59 +03:00
DanSylvest
33acd55eaa fix(Map): Update wormhole lifetime UI and removed unnecessary code 2025-09-12 11:05:57 +03:00
CI
dec82e89c2 chore: [skip ci] 2025-09-11 19:14:13 +00:00
CI
f5ac5bc4ec chore: release version v1.77.17 2025-09-11 19:14:13 +00:00
Dmitry Popov
b6c680e802 fix: Updated ACL create/update APIs 2025-09-11 21:13:41 +02:00
CI
5fa57c13b4 chore: [skip ci] 2025-09-11 17:56:11 +00:00
CI
acc81fda44 chore: release version v1.77.16 2025-09-11 17:56:11 +00:00
Dmitry Popov
7ab5acf45f Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-11 19:55:37 +02:00
Dmitry Popov
0d4ffbcc22 fix: Fixed issue with ACL add members button for managers. Added WANDERER_RESTRICT_ACLS_CREATION env support. 2025-09-11 19:55:32 +02:00
CI
a9253ac2df chore: [skip ci] 2025-09-10 07:29:33 +00:00
CI
d00b4843a7 chore: release version v1.77.15 2025-09-10 07:29:33 +00:00
Aleksei Chichenkov
6068de2c71 Merge pull request #514 from wanderer-industries/unnecessary-rerenders
fix(Map): Fix problem with unnecessary rerenders and loads routes if …
2025-09-10 10:29:03 +03:00
DanSylvest
73da427c6b fix(Map): Fix problem with unnecessary rerenders and loads routes if move/positioning widgets. 2025-09-10 10:10:17 +03:00
CI
9b7ec0ddfe chore: [skip ci] 2025-09-08 22:07:20 +00:00
CI
c2f5f14c44 chore: release version v1.77.14 2025-09-08 22:07:20 +00:00
Dmitry Popov
0b7c3588d5 fix: Fixed issue with loading connection info 2025-09-09 00:06:49 +02:00
CI
a51fac5736 chore: [skip ci] 2025-09-07 22:27:12 +00:00
CI
726c3d0704 chore: release version v1.77.13 2025-09-07 22:27:12 +00:00
Dmitry Popov
8dd564dbd0 fix: Updated character tracking, added an extra check for offline characters to reduce errors 2025-09-08 00:26:40 +02:00
CI
e33c65cddc chore: [skip ci] 2025-09-07 19:28:25 +00:00
CI
f2fbd2ead0 chore: release version v1.77.12 2025-09-07 19:28:25 +00:00
Dmitry Popov
123a2e45eb Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-07 21:27:56 +02:00
Dmitry Popov
f8d2d9c680 fix: Decreased character tracking grace period 2025-09-07 21:27:53 +02:00
CI
9dcbef9a79 chore: [skip ci] 2025-09-07 19:16:08 +00:00
CI
0b14857a12 chore: release version v1.77.11 2025-09-07 19:16:08 +00:00
Dmitry Popov
bd3d516f60 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-07 21:15:39 +02:00
Dmitry Popov
40d0bd8cea fix: Fixed CSP errors 2025-09-07 21:15:35 +02:00
guarzo
873946a1a6 fix: removed wormhole only logic error 2025-09-05 01:43:47 +00:00
CI
968deeb254 chore: [skip ci] 2025-09-04 09:17:39 +00:00
CI
959041be52 chore: release version v1.77.10 2025-09-04 09:17:39 +00:00
Dmitry Popov
3319520179 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-04 11:17:08 +02:00
Dmitry Popov
580fcf3657 fix: Removed invalid invite options 2025-09-04 11:17:04 +02:00
CI
53dae7c520 chore: [skip ci] 2025-09-04 09:11:38 +00:00
CI
6d59d709f1 chore: release version v1.77.9 2025-09-04 09:11:38 +00:00
Dmitry Popov
4343e9070c fix: Auto select following char system on start 2025-09-04 11:10:59 +02:00
CI
b62373fb5f chore: [skip ci] 2025-09-03 14:38:52 +00:00
CI
3da98f8e56 chore: release version v1.77.8 2025-09-03 14:38:52 +00:00
Dmitry Popov
494d24952e Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-03 16:38:26 +02:00
Dmitry Popov
8a6b17bd7b fix: Updated character tracking 2025-09-03 16:38:23 +02:00
CI
d2e859a74e chore: [skip ci] 2025-09-03 13:03:26 +00:00
CI
4a78d55d22 chore: release version v1.77.7 2025-09-03 13:03:26 +00:00
Dmitry Popov
dc252b8c4b Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-03 15:02:57 +02:00
Dmitry Popov
c433205e89 fix: Updated character tracking 2025-09-03 15:02:53 +02:00
CI
d6bc5b57b1 chore: [skip ci] 2025-09-02 17:34:32 +00:00
CI
280a286266 chore: release version v1.77.6 2025-09-02 17:34:32 +00:00
Dmitry Popov
d5c18b5de3 fix: Updated character tracking, added grace period to reduce false-positive cases 2025-09-02 19:33:57 +02:00
CI
7452e5d011 chore: [skip ci] 2025-09-02 10:37:50 +00:00
CI
71674b0d52 chore: release version v1.77.5 2025-09-02 10:37:50 +00:00
Dmitry Popov
5b4824bd5d Merge pull request #510 from guarzo/guarzo/newtracking
fix: resolve tracking issues
2025-09-02 14:37:22 +04:00
CI
deda16a7da chore: [skip ci] 2025-09-02 10:26:47 +00:00
CI
0b7c067de7 chore: release version v1.77.4 2025-09-02 10:26:47 +00:00
Dmitry Popov
0d0db8c129 Merge pull request #509 from guarzo/guarzo/aclapi
fix: ensure pub/sub occurs after acl api change
2025-09-02 14:26:20 +04:00
guarzo
9f1b7994a3 fix: resolve tracking issues 2025-09-02 07:11:25 +00:00
guarzo
378df0ac70 fix: pr feedback 2025-09-02 00:27:40 +00:00
guarzo
0e4a132f69 refactor: dry 2025-09-01 22:38:12 +00:00
guarzo
631746375d fix: ensure pub/sub occurs after acl api change 2025-09-01 22:11:58 +00:00
CI
7dc01dad54 chore: [skip ci] 2025-08-29 00:33:30 +00:00
CI
8a9807d3e5 chore: release version v1.77.3 2025-08-29 00:33:30 +00:00
Dmitry Popov
39df3c97ce Merge pull request #505 from wanderer-industries/tracking-fix
Tracking fix
2025-08-29 04:33:00 +04:00
Dmitry Popov
46c1ccdfcc fix: Fixed character tracking settings 2025-08-29 02:31:00 +02:00
Dmitry Popov
8817536038 fix: Fixed character tracking settings 2025-08-29 02:30:19 +02:00
Dmitry Popov
c3bb23a6ee fix: Fixed character tracking settings 2025-08-29 01:41:08 +02:00
Dmitry Popov
7e9c4c575e fix: Fixed character tracking settings 2025-08-28 22:18:18 +02:00
CI
5a70eee91e chore: [skip ci] 2025-08-28 10:24:51 +00:00
CI
228f6990a1 chore: release version v1.77.2 2025-08-28 10:24:51 +00:00
Dmitry Popov
d80ed0e70e Merge pull request #504 from guarzo/guarzo/sigapi
fix: update system signature api to return correct system id
2025-08-28 14:24:26 +04:00
CI
4576c75737 chore: [skip ci] 2025-08-28 10:03:36 +00:00
CI
67764faaa7 chore: release version v1.77.1 2025-08-28 10:03:36 +00:00
Dmitry Popov
91dd0b27ae chore: Added support for limited telemetry (base only). 2025-08-28 12:03:01 +02:00
guarzo
99dcf49fbc Merge branch 'main' into guarzo/sigapi 2025-08-27 21:30:59 -04:00
guarzo
6fb3edbfd6 fix: update system signature api to return correct system id 2025-08-28 01:30:37 +00:00
CI
26f13ce857 chore: [skip ci] 2025-08-27 21:18:31 +00:00
CI
e9b475c0a8 chore: release version v1.77.0 2025-08-27 21:18:31 +00:00
Dmitry Popov
7752010092 feat(Core): Reduced DB calls to check existing system jumps 2025-08-27 23:17:58 +02:00
CI
d3705b3ed7 chore: [skip ci] 2025-08-27 20:46:18 +00:00
CI
1394e2897e chore: release version v1.76.13 2025-08-27 20:46:18 +00:00
Dmitry Popov
5117a1c5af fix(Core): Fixed maps start timeout 2025-08-27 22:42:29 +02:00
CI
3c62403f33 chore: [skip ci] 2025-08-20 14:37:18 +00:00
CI
a4760f5162 chore: release version v1.76.12 2025-08-20 14:37:18 +00:00
Dmitry Popov
b071070431 fix(Core): Reduced ESI api calls to update character corp/ally info 2025-08-20 16:36:46 +02:00
CI
3bcb9628e7 chore: [skip ci] 2025-08-20 07:53:27 +00:00
CI
e62c4cf5bf chore: release version v1.76.11 2025-08-20 07:53:27 +00:00
Dmitry Popov
af46962ce4 Merge pull request #503 from wanderer-industries/revert-501-guarzo/sigsfix
Revert "fix: default signature types not being shown"
2025-08-20 11:53:00 +04:00
Dmitry Popov
0b0967830b Revert "fix: default signature types not being shown" 2025-08-20 11:52:34 +04:00
CI
172251a208 chore: [skip ci] 2025-08-18 23:28:33 +00:00
CI
8a6fb63d55 chore: release version v1.76.10 2025-08-18 23:28:33 +00:00
Dmitry Popov
9652959e5e fix(Core): Added character trackers start queue 2025-08-19 01:27:58 +02:00
CI
825ef46d41 chore: [skip ci] 2025-08-18 11:42:47 +00:00
CI
ad9f7c6b95 chore: release version v1.76.9 2025-08-18 11:42:47 +00:00
Dmitry Popov
b960b5c149 Merge pull request #501 from guarzo/guarzo/sigsfix
fix: default signature types not being shown
2025-08-18 15:42:14 +04:00
CI
0f092d21f9 chore: [skip ci] 2025-08-17 21:28:20 +00:00
CI
031576caa6 chore: release version v1.76.8 2025-08-17 21:28:20 +00:00
Dmitry Popov
7a97a96c42 fix(Core): added DB connection default timeouts 2025-08-17 23:27:21 +02:00
CI
2efb2daba0 chore: [skip ci] 2025-08-16 22:17:44 +00:00
CI
4374c39924 chore: release version v1.76.7 2025-08-16 22:17:44 +00:00
Dmitry Popov
15711495c7 fix(Core): Fixed auth redirect URL 2025-08-17 00:17:17 +02:00
guarzo
236f803427 fix: default signature types not being shown 2025-08-15 23:03:22 +00:00
CI
6772130f2a chore: [skip ci] 2025-08-15 15:27:07 +00:00
CI
ddd72f3fac chore: release version v1.76.6 2025-08-15 15:27:07 +00:00
Dmitry Popov
6e262835ef Merge pull request #500 from guarzo/guarzo/moressefix
fix: empty subscriptions for sse
2025-08-15 19:26:34 +04:00
guarzo
2f3b8ddc5f fix: empty subscriptions for sse 2025-08-15 11:08:40 -04:00
CI
cea3a74b34 chore: [skip ci] 2025-08-15 10:29:11 +00:00
CI
867941a233 chore: release version v1.76.5 2025-08-15 10:29:11 +00:00
Dmitry Popov
3ff388a16d fix(Core): fixed tracking paused issues, fixed user activity data 2025-08-15 12:28:36 +02:00
CI
f4248e9ab9 chore: [skip ci] 2025-08-14 23:40:20 +00:00
Dmitry Popov
854524a03c feat(Core): added support for jumpgates connection type 2025-03-23 21:51:26 +01:00
144 changed files with 3467 additions and 2028 deletions

View File

@@ -13,4 +13,4 @@ export WANDERER_KILLS_BASE_URL="ws://host.docker.internal:4004"
export WANDERER_SSE_ENABLED="true"
export WANDERER_WEBHOOKS_ENABLED="true"
export WANDERER_SSE_MAX_CONNECTIONS="1000"
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"

View File

@@ -2,6 +2,413 @@
<!-- changelog -->
## [v1.81.5](https://github.com/wanderer-industries/wanderer/compare/v1.81.4...v1.81.5) (2025-10-09)
### Bug Fixes:
* Core: Update connection ship size based on linked signature type
## [v1.81.4](https://github.com/wanderer-industries/wanderer/compare/v1.81.3...v1.81.4) (2025-10-09)
### Bug Fixes:
* Core: Fixed signature to system link issues
## [v1.81.3](https://github.com/wanderer-industries/wanderer/compare/v1.81.2...v1.81.3) (2025-10-07)
### Bug Fixes:
* Core: Fixed cancel ping errors
## [v1.81.2](https://github.com/wanderer-industries/wanderer/compare/v1.81.1...v1.81.2) (2025-10-07)
### Bug Fixes:
* api dropping custom name
## [v1.81.1](https://github.com/wanderer-industries/wanderer/compare/v1.81.0...v1.81.1) (2025-10-02)
### Bug Fixes:
* Core: Fixed characters tracking updates.
## [v1.81.0](https://github.com/wanderer-industries/wanderer/compare/v1.80.0...v1.81.0) (2025-10-02)
### Features:
* core: fix pwa icons + add screen in manifest
## [v1.80.0](https://github.com/wanderer-industries/wanderer/compare/v1.79.6...v1.80.0) (2025-10-02)
### Features:
* Core: Added PWA web manifest
## [v1.79.6](https://github.com/wanderer-industries/wanderer/compare/v1.79.5...v1.79.6) (2025-10-01)
### Bug Fixes:
* Core: Fixed modals auto-save on Enter.
## [v1.79.5](https://github.com/wanderer-industries/wanderer/compare/v1.79.4...v1.79.5) (2025-10-01)
### Bug Fixes:
* Core: Fixed system details modal auto-save on Enter.
## [v1.79.4](https://github.com/wanderer-industries/wanderer/compare/v1.79.3...v1.79.4) (2025-09-30)
### Bug Fixes:
* Core: Fixed updating connection time status based on linked signature data. Fixed FR gas sites parsing.
## [v1.79.3](https://github.com/wanderer-industries/wanderer/compare/v1.79.2...v1.79.3) (2025-09-27)
### Bug Fixes:
* Core: Fixed connection passages count
## [v1.79.2](https://github.com/wanderer-industries/wanderer/compare/v1.79.1...v1.79.2) (2025-09-26)
## [v1.79.1](https://github.com/wanderer-industries/wanderer/compare/v1.79.0...v1.79.1) (2025-09-26)
## [v1.79.0](https://github.com/wanderer-industries/wanderer/compare/v1.78.1...v1.79.0) (2025-09-26)
### Features:
* Core: Updated connections EOL logic
### Bug Fixes:
* Map: Fixed eslint problems
* Map: Update lifetime design and buttons
* Map: Update wormhole lifetime UI and removed unnecessary code
## [v1.78.1](https://github.com/wanderer-industries/wanderer/compare/v1.78.0...v1.78.1) (2025-09-24)
### Bug Fixes:
* pr feedback
* removed wormhole only logic error
## [v1.78.0](https://github.com/wanderer-industries/wanderer/compare/v1.77.19...v1.78.0) (2025-09-23)
### Features:
* Core: added support for jumpgates connection type
### Bug Fixes:
* Map: Add support for Bridge. Made all tooltips left and right paddings.
## [v1.77.19](https://github.com/wanderer-industries/wanderer/compare/v1.77.18...v1.77.19) (2025-09-14)
### Bug Fixes:
* Map: Fixed for all Large wormholes jump mass from 300 to 375. Fixed jump mass and total mass for N290, K329. Fixed static for J005663 was H296 now Y790. Added J492 wormhole. Change lifetime for E587 from 16 to 48
## [v1.77.18](https://github.com/wanderer-industries/wanderer/compare/v1.77.17...v1.77.18) (2025-09-13)
## [v1.77.17](https://github.com/wanderer-industries/wanderer/compare/v1.77.16...v1.77.17) (2025-09-11)
### Bug Fixes:
* Updated ACL create/update APIs
## [v1.77.16](https://github.com/wanderer-industries/wanderer/compare/v1.77.15...v1.77.16) (2025-09-11)
### Bug Fixes:
* Fixed issue with ACL add members button for managers. Added WANDERER_RESTRICT_ACLS_CREATION env support.
## [v1.77.15](https://github.com/wanderer-industries/wanderer/compare/v1.77.14...v1.77.15) (2025-09-10)
### Bug Fixes:
* Map: Fix problem with unnecessary rerenders and loads routes if move/positioning widgets.
## [v1.77.14](https://github.com/wanderer-industries/wanderer/compare/v1.77.13...v1.77.14) (2025-09-08)
### Bug Fixes:
* Fixed issue with loading connection info
## [v1.77.13](https://github.com/wanderer-industries/wanderer/compare/v1.77.12...v1.77.13) (2025-09-07)
### Bug Fixes:
* Updated character tracking, added an extra check for offline characters to reduce errors
## [v1.77.12](https://github.com/wanderer-industries/wanderer/compare/v1.77.11...v1.77.12) (2025-09-07)
### Bug Fixes:
* Decreased character tracking grace period
## [v1.77.11](https://github.com/wanderer-industries/wanderer/compare/v1.77.10...v1.77.11) (2025-09-07)
### Bug Fixes:
* Fixed CSP errors
## [v1.77.10](https://github.com/wanderer-industries/wanderer/compare/v1.77.9...v1.77.10) (2025-09-04)
### Bug Fixes:
* Removed invalid invite options
## [v1.77.9](https://github.com/wanderer-industries/wanderer/compare/v1.77.8...v1.77.9) (2025-09-04)
### Bug Fixes:
* Auto select following char system on start
## [v1.77.8](https://github.com/wanderer-industries/wanderer/compare/v1.77.7...v1.77.8) (2025-09-03)
### Bug Fixes:
* Updated character tracking
## [v1.77.7](https://github.com/wanderer-industries/wanderer/compare/v1.77.6...v1.77.7) (2025-09-03)
### Bug Fixes:
* Updated character tracking
## [v1.77.6](https://github.com/wanderer-industries/wanderer/compare/v1.77.5...v1.77.6) (2025-09-02)
### Bug Fixes:
* Updated character tracking, added grace period to reduce false-positive cases
## [v1.77.5](https://github.com/wanderer-industries/wanderer/compare/v1.77.4...v1.77.5) (2025-09-02)
### Bug Fixes:
* resolve tracking issues
## [v1.77.4](https://github.com/wanderer-industries/wanderer/compare/v1.77.3...v1.77.4) (2025-09-02)
### Bug Fixes:
* pr feedback
* ensure pub/sub occurs after acl api change
## [v1.77.3](https://github.com/wanderer-industries/wanderer/compare/v1.77.2...v1.77.3) (2025-08-29)
### Bug Fixes:
* Fixed character tracking settings
* Fixed character tracking settings
* Fixed character tracking settings
* Fixed character tracking settings
## [v1.77.2](https://github.com/wanderer-industries/wanderer/compare/v1.77.1...v1.77.2) (2025-08-28)
### Bug Fixes:
* update system signature api to return correct system id
## [v1.77.1](https://github.com/wanderer-industries/wanderer/compare/v1.77.0...v1.77.1) (2025-08-28)
## [v1.77.0](https://github.com/wanderer-industries/wanderer/compare/v1.76.13...v1.77.0) (2025-08-27)
### Features:
* Core: Reduced DB calls to check existing system jumps
## [v1.76.13](https://github.com/wanderer-industries/wanderer/compare/v1.76.12...v1.76.13) (2025-08-27)
### Bug Fixes:
* Core: Fixed maps start timeout
## [v1.76.12](https://github.com/wanderer-industries/wanderer/compare/v1.76.11...v1.76.12) (2025-08-20)
### Bug Fixes:
* Core: Reduced ESI api calls to update character corp/ally info
## [v1.76.11](https://github.com/wanderer-industries/wanderer/compare/v1.76.10...v1.76.11) (2025-08-20)
## [v1.76.10](https://github.com/wanderer-industries/wanderer/compare/v1.76.9...v1.76.10) (2025-08-18)
### Bug Fixes:
* Core: Added character trackers start queue
## [v1.76.9](https://github.com/wanderer-industries/wanderer/compare/v1.76.8...v1.76.9) (2025-08-18)
### Bug Fixes:
* default signature types not being shown
## [v1.76.8](https://github.com/wanderer-industries/wanderer/compare/v1.76.7...v1.76.8) (2025-08-17)
### Bug Fixes:
* Core: added DB connection default timeouts
## [v1.76.7](https://github.com/wanderer-industries/wanderer/compare/v1.76.6...v1.76.7) (2025-08-16)
### Bug Fixes:
* Core: Fixed auth redirect URL
## [v1.76.6](https://github.com/wanderer-industries/wanderer/compare/v1.76.5...v1.76.6) (2025-08-15)
### Bug Fixes:
* empty subscriptions for sse
## [v1.76.5](https://github.com/wanderer-industries/wanderer/compare/v1.76.4...v1.76.5) (2025-08-15)
### Bug Fixes:
* Core: fixed tracking paused issues, fixed user activity data
## [v1.76.4](https://github.com/wanderer-industries/wanderer/compare/v1.76.3...v1.76.4) (2025-08-14)

View File

@@ -18,5 +18,28 @@ module.exports = {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
"linebreak-style": "off",
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "primereact/button",
"importNames": ["Button"],
"message": "Use WdButton instead Button"
}
]
}
],
"react/forbid-elements": [
"error",
{
"forbid": [
{
"element": "Button",
"message": "Use WdButton instead Button"
}
]
}
]
},
};

View File

@@ -5,8 +5,7 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { getSystemById } from '@/hooks/Mapper/helpers';
import clsx from 'clsx';
import { GRADIENT_MENU_ACTIVE_CLASSES } from '@/hooks/Mapper/constants.ts';
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
import { Button } from 'primereact/button';
import { LayoutEventBlocker, WdButton } from '@/hooks/Mapper/components/ui-kit';
const AVAILABLE_TAGS = [
'A',
@@ -61,7 +60,7 @@ export const useTagMenu = (
<LayoutEventBlocker className="flex flex-col gap-1 w-[200px] h-full px-2">
<div className="grid grid-cols-[auto_auto_auto_auto_auto_auto] gap-1">
{AVAILABLE_TAGS.map(x => (
<Button
<WdButton
outlined={system?.tag !== x}
severity="warning"
key={x}
@@ -71,9 +70,9 @@ export const useTagMenu = (
onClick={() => system?.tag !== x && onSystemTag(x)}
>
{x}
</Button>
</WdButton>
))}
<Button
<WdButton
disabled={!isSelectedTag}
icon="pi pi-ban"
size="small"
@@ -81,7 +80,7 @@ export const useTagMenu = (
outlined
severity="help"
onClick={() => onSystemTag()}
></Button>
></WdButton>
</div>
</LayoutEventBlocker>
);

View File

@@ -1,3 +1,10 @@
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import type { PanelPosition } from '@reactflow/core';
import clsx from 'clsx';
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
import ReactFlow, {
Background,
@@ -16,8 +23,6 @@ import ReactFlow, {
import 'reactflow/dist/style.css';
import classes from './Map.module.scss';
import { MapProvider, useMapState } from './MapProvider';
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import {
ContextMenuConnection,
ContextMenuRoot,
@@ -26,14 +31,9 @@ import {
useContextMenuRootHandlers,
} from './components';
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import clsx from 'clsx';
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
import { useBackgroundVars } from './hooks/useBackgroundVars';
import type { PanelPosition } from '@reactflow/core';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };

View File

@@ -8,6 +8,10 @@
background-image: linear-gradient(207deg, transparent, var(--conn-frigate));
}
.ConnectionBridge {
background-image: linear-gradient(207deg, transparent, var(--conn-bridge));
}
.ConnectionSave {
background-image: linear-gradient(207deg, transparent, var(--conn-save));
}
@@ -15,3 +19,14 @@
.SelectedItem {
background-color: var(--selected-item-bg);
}
.FastActions {
:global {
.p-menuitem-content {
background-color: initial !important;
}
.p-menuitem-content:hover {
background-color: initial !important;
}
}
}

View File

@@ -1,10 +1,3 @@
import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import classes from './ContextMenuConnection.module.scss';
import {
MASS_STATE_NAMES,
MASS_STATE_NAMES_ORDER,
@@ -13,14 +6,25 @@ import {
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';
import { ContextMenu } from 'primereact/contextmenu';
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 classes from './ContextMenuConnection.module.scss';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { isNullsecSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
export interface ContextMenuConnectionProps {
contextMenuRef: RefObject<ContextMenu>;
onDeleteConnection(): void;
onChangeTimeState(): void;
onChangeTimeState(lifetime: TimeStatus): void;
onChangeMassState(state: MassState): void;
onChangeShipSizeStatus(state: ShipSizeStatus): void;
onChangeType(type: ConnectionType): void;
onToggleMassSave(isLocked: boolean): void;
onHide(): void;
edge?: Edge<SolarSystemConnection>;
@@ -32,6 +36,7 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
onChangeTimeState,
onChangeMassState,
onChangeShipSizeStatus,
onChangeType,
onToggleMassSave,
onHide,
edge,
@@ -41,88 +46,128 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
return [];
}
const sourceInfo = getSystemStaticInfo(edge.data?.source);
const targetInfo = getSystemStaticInfo(edge.data?.target);
const bothNullsec =
sourceInfo && targetInfo && isNullsecSpace(sourceInfo.system_class) && isNullsecSpace(targetInfo.system_class);
const isFrigateSize = edge.data?.ship_size_type === ShipSizeStatus.small;
const isWormhole = edge.data?.type !== ConnectionType.gate;
if (edge.data?.type === ConnectionType.bridge) {
return [
{
label: `Set as Wormhole`,
icon: 'pi hero-arrow-uturn-left',
command: () => onChangeType(ConnectionType.wormhole),
},
{
label: 'Disconnect',
icon: PrimeIcons.TRASH,
command: onDeleteConnection,
},
];
}
if (edge.data?.type === ConnectionType.gate) {
return [
{
label: 'Disconnect',
icon: PrimeIcons.TRASH,
command: onDeleteConnection,
},
];
}
return [
...(isWormhole
{
className: clsx(classes.FastActions, '!h-[54px]'),
template: () => {
return <LifetimeActionsWrapper lifetime={edge.data?.time_status} onChangeLifetime={onChangeTimeState} />;
},
},
{
label: `Frigate`,
className: clsx({
[classes.ConnectionFrigate]: isFrigateSize,
}),
icon: PrimeIcons.CLOUD,
command: () =>
onChangeShipSizeStatus(
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
),
},
{
label: `Save mass`,
className: clsx({
[classes.ConnectionSave]: edge.data?.locked,
}),
icon: PrimeIcons.LOCK,
command: () => onToggleMassSave(!edge.data?.locked),
},
...(!isFrigateSize
? [
{
label: `EOL`,
className: clsx({
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
}),
icon: PrimeIcons.CLOCK,
command: onChangeTimeState,
},
{
label: `Frigate`,
className: clsx({
[classes.ConnectionFrigate]: isFrigateSize,
}),
icon: PrimeIcons.CLOUD,
command: () =>
onChangeShipSizeStatus(
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
),
},
{
label: `Save mass`,
className: clsx({
[classes.ConnectionSave]: edge.data?.locked,
}),
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
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?.ship_size_type === x,
[classes.SelectedItem]: edge.data?.mass_status === x,
}),
command: () => onChangeShipSizeStatus(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
? [
{
label: `Set as Bridge`,
icon: 'pi hero-forward',
command: () => onChangeType(ConnectionType.bridge),
},
]
: []),
{
label: 'Disconnect',
icon: PrimeIcons.TRASH,
command: onDeleteConnection,
},
];
}, [edge, onChangeTimeState, onDeleteConnection, onChangeShipSizeStatus, onToggleMassSave, onChangeMassState]);
}, [
edge,
onChangeTimeState,
onDeleteConnection,
onChangeType,
onChangeShipSizeStatus,
onToggleMassSave,
onChangeMassState,
]);
return (
<>
<ContextMenu model={items} ref={contextMenuRef} onHide={onHide} breakpoint="767px" />
<ContextMenu model={items} ref={contextMenuRef} onHide={onHide} breakpoint="767px" className="!w-[250px]" />
</>
);
};

View File

@@ -0,0 +1,12 @@
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
import { WdLifetimeSelector, WdLifetimeSelectorProps } from '@/hooks/Mapper/components/ui-kit/WdLifetimeSelector.tsx';
export const LifetimeActionsWrapper = (props: WdLifetimeSelectorProps) => {
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">Life time:</div>
<WdLifetimeSelector {...props} />
</LayoutEventBlocker>
);
};

View File

@@ -30,7 +30,7 @@ export const useContextMenuConnectionHandlers = () => {
setEdge(undefined);
};
const onChangeTimeState = () => {
const onChangeTimeState = (lifetime: TimeStatus) => {
if (!edge || !edge.data) {
return;
}
@@ -40,7 +40,7 @@ export const useContextMenuConnectionHandlers = () => {
data: {
source: edge.source,
target: edge.target,
value: edge.data.time_status === TimeStatus.default ? TimeStatus.eol : TimeStatus.default,
value: lifetime,
},
});
setEdge(undefined);

View File

@@ -56,7 +56,8 @@ export const KillsCounter = ({
className={className}
tooltipClassName="!px-0"
size={size}
interactive={true}
interactive
smallPaddings
>
{children}
</WdTooltipWrapper>

View File

@@ -46,7 +46,13 @@ export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIc
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
})}
>
<WdTooltipWrapper content={pilotTooltipContent} position={TooltipPosition.right} offset={0} interactive={true}>
<WdTooltipWrapper
content={pilotTooltipContent}
position={TooltipPosition.right}
offset={0}
interactive={true}
smallPaddings
>
<div className={clsx(classes.hoverTarget)}>
<div
className={clsx(classes.localCounter, {

View File

@@ -5,6 +5,16 @@
stroke: #80a5c5;
stroke-width: 3px;
&.time1 {
stroke: #f11ab2;
stroke-width: 4px;
}
&.time4 {
stroke: #a654e3;
stroke-width: 4px;
}
&.TimeCrit {
stroke: #f11ab2;
stroke-width: 4px;
@@ -29,6 +39,13 @@
&.Gate {
stroke: #9aff40;
}
&.Bridge {
stroke: #9aff40;
stroke-dasharray: 10 5;
stroke-linecap: round;
}
}
.EdgePathFront {

View File

@@ -9,6 +9,7 @@ import { PrimeIcons } from 'primereact/api';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SHIP_SIZES_DESCRIPTION, SHIP_SIZES_NAMES_SHORT } from '@/hooks/Mapper/components/map/constants.ts';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
const MAP_TRANSLATES: Record<string, string> = {
[Position.Top]: 'translate(-48%, 0%)',
@@ -42,7 +43,9 @@ export const SHIP_SIZES_COLORS = {
export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }: EdgeProps<SolarSystemConnection>) => {
const sourceNode = useStore(useCallback(store => store.nodeInternals.get(source), [source]));
const targetNode = useStore(useCallback(store => store.nodeInternals.get(target), [target]));
const isWormhole = data?.type !== ConnectionType.gate;
const isWormhole = data?.type === ConnectionType.wormhole;
const isGate = data?.type === ConnectionType.gate;
const isBridge = data?.type === ConnectionType.bridge;
const {
data: { isThickConnections },
@@ -55,9 +58,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
const method = isWormhole ? getBezierPath : getBezierPath;
const [edgePath, labelX, labelY] = method({
const [edgePath, labelX, labelY] = getBezierPath({
sourceX: sx - offset.x,
sourceY: sy - offset.y,
sourcePosition: sourcePos,
@@ -67,7 +68,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
});
return [edgePath, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos];
}, [isThickConnections, sourceNode, targetNode, isWormhole]);
}, [isThickConnections, sourceNode, targetNode]);
if (!sourceNode || !targetNode || !data) {
return null;
@@ -79,9 +80,11 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
id={`back_${id}`}
className={clsx(classes.EdgePathBack, {
[classes.Tick]: isThickConnections,
[classes.TimeCrit]: isWormhole && data.time_status === TimeStatus.eol,
[classes.time1]: isWormhole && data.time_status === TimeStatus._1h,
[classes.time4]: isWormhole && data.time_status === TimeStatus._4h,
[classes.Hovered]: hovered,
[classes.Gate]: !isWormhole,
[classes.Gate]: isGate,
[classes.Bridge]: isBridge,
})}
d={path}
markerEnd={markerEnd}
@@ -95,7 +98,8 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
[classes.MassVerge]: isWormhole && data.mass_status === MassState.verge,
[classes.MassHalf]: isWormhole && data.mass_status === MassState.half,
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
[classes.Gate]: !isWormhole,
[classes.Gate]: isGate,
[classes.Bridge]: isBridge,
})}
d={path}
markerEnd={markerEnd}
@@ -147,6 +151,19 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
</WdTooltipWrapper>
)}
{isBridge && (
<WdTooltipWrapper
content="Ansiblex Jump Bridge"
position={TooltipPosition.top}
className={clsx(
classes.LinkLabel,
'pointer-events-auto bg-lime-300 rounded opacity-100 cursor-auto text-neutral-900',
)}
>
B
</WdTooltipWrapper>
)}
{isWormhole && data.ship_size_type !== ShipSizeStatus.large && (
<WdTooltipWrapper
content={SHIP_SIZES_DESCRIPTION[data.ship_size_type]}

View File

@@ -58,6 +58,7 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
</InfoDrawer>
</div>
}
smallPaddings
>
<div className={clsx(classes.Box, whClassStyle)}>
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">

View File

@@ -716,11 +716,12 @@ export const STATUS_CLASSES: Record<number, string> = {
[STATUSES.dangerous]: 'eve-system-status-dangerous',
};
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate];
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate, ConnectionType.bridge];
export const TYPE_NAMES = {
[ConnectionType.wormhole]: 'Wormhole',
[ConnectionType.gate]: 'Gate',
[ConnectionType.bridge]: 'Jumpgate',
};
export const MASS_STATE_NAMES_ORDER = [MassState.verge, MassState.half, MassState.normal];

View File

@@ -15,3 +15,12 @@ export const isKnownSpace = (wormholeClassID: number) => {
export const isPossibleSpace = (spaces: number[], wormholeClassID: number) => {
return spaces.includes(wormholeClassID);
};
export const isNullsecSpace = (wormholeClassID: number) => {
switch (wormholeClassID) {
case SOLAR_SYSTEM_CLASS_IDS.ns:
return true;
}
return false;
};

View File

@@ -1,4 +1,6 @@
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useEventBuffer } from '@/hooks/Mapper/hooks';
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useCallback, useRef } from 'react';
import { useReactFlow } from 'reactflow';
@@ -11,6 +13,20 @@ export const useMapInit = () => {
const ref = useRef({ rf, data, update });
ref.current = { update, data, rf };
const updateSystems = useCallback((systems: SolarSystemRawType[]) => {
const { rf } = ref.current;
rf.setNodes(systems.map(convertSystem2Node));
}, []);
const { handleEvent: handleUpdateSystems } = useEventBuffer<any>(updateSystems);
const updateEdges = useCallback((connections: SolarSystemConnection[]) => {
const { rf } = ref.current;
rf.setEdges(connections.map(convertConnection2Edge));
}, []);
const { handleEvent: handleUpdateConnections } = useEventBuffer<any>(updateEdges);
return useCallback(
({
systems,
@@ -24,7 +40,6 @@ export const useMapInit = () => {
hubs,
}: CommandInit) => {
const { update } = ref.current;
const { rf } = ref.current;
const updateData: Partial<MapData> = {};
@@ -63,11 +78,13 @@ export const useMapInit = () => {
update(updateData);
if (systems) {
rf.setNodes(systems.map(convertSystem2Node));
handleUpdateSystems(systems);
// rf.setNodes(systems.map(convertSystem2Node));
}
if (connections) {
rf.setEdges(connections.map(convertConnection2Edge));
handleUpdateConnections(connections);
// rf.setEdges(connections.map(convertConnection2Edge));
}
},
[],

View File

@@ -1,7 +1,7 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandSelectSystems } from '@/hooks/Mapper/types';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import { CommandSelectSystems } from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
import { useReactFlow } from 'reactflow';
export const useSelectSystems = (onSelectionChange: OnMapSelectionChange) => {
const rf = useReactFlow();

View File

@@ -1,4 +1,3 @@
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
import {
CommandAddConnections,
CommandAddSystems,
@@ -19,8 +18,11 @@ import {
CommandUpdateSystems,
MapHandlers,
} from '@/hooks/Mapper/types/mapHandlers.ts';
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import {
useCenterSystem,
useCommandsCharacters,
useCommandsConnections,
useMapAddSystems,
@@ -28,10 +30,8 @@ import {
useMapInit,
useMapRemoveSystems,
useMapUpdateSystems,
useCenterSystem,
useSelectSystems,
} from './api';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange: OnMapSelectionChange) => {
const mapInit = useMapInit();

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef } from 'react';
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { useCallback, useEffect, useRef } from 'react';
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
const useThrottle = () => {
const throttleSeed = useRef<number | null>(null);

View File

@@ -118,6 +118,7 @@ $homeDark30: color.adjust($homeBase, $lightness: -30%);
--conn-time-eol: #7452c3e3;
--conn-frigate: #325d88;
--conn-bridge: rgba(135, 185, 93, 0.85);
--conn-save: rgba(155, 102, 45, 0.85);
--selected-item-bg: rgba(98, 98, 98, 0.33);
}

View File

@@ -1,16 +1,15 @@
import { Dialog } from 'primereact/dialog';
import { SystemViewStandalone, WdButton, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { IconField } from 'primereact/iconfield';
import { AutoComplete } from 'primereact/autocomplete';
import { OutCommand, SearchSystemItem } from '@/hooks/Mapper/types';
import { SystemViewStandalone, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { AutoComplete } from 'primereact/autocomplete';
import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield';
import { useCallback, useRef, useState } from 'react';
import classes from './AddSystemDialog.module.scss';
import clsx from 'clsx';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import clsx from 'clsx';
export type SearchOnSubmitCallback = (item: SearchSystemItem) => void;
@@ -34,6 +33,7 @@ export const AddSystemDialog = ({
data: { wormholesData },
} = useMapRootState();
// TODO fix it
const inputRef = useRef<any>();
const onShow = useCallback(() => {
inputRef.current?.focus();
@@ -62,6 +62,7 @@ export const AddSystemDialog = ({
},
});
// TODO fix it
let prepared = (result.systems as SearchSystemItem[]).sort((a, b) => {
const amatch = a.label.indexOf(query);
const bmatch = b.label.indexOf(query);
@@ -114,90 +115,93 @@ export const AddSystemDialog = ({
setVisible(false);
}}
>
<div className="flex flex-col gap-3 px-1.5">
<div className="flex flex-col gap-2 py-3.5">
<div className="flex flex-col gap-1">
<IconField>
<AutoComplete
ref={inputRef}
multiple
showEmptyMessage
scrollHeight="300px"
value={selectedItem}
suggestions={filteredItems}
completeMethod={searchItems}
onChange={e => {
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
}}
emptyMessage="Not found any system..."
placeholder="Type here..."
field="label"
id="value"
className="w-full"
itemTemplate={(item: SearchSystemItem) => {
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
const sortedStatics = sortWHClasses(wormholesData, statics);
const isWH = isWormholeSpace(system_class);
<form onSubmit={handleSubmit}>
<div className="flex flex-col gap-3 px-1.5">
<div className="flex flex-col gap-2 py-3.5">
<div className="flex flex-col gap-1">
<IconField>
<AutoComplete
ref={inputRef}
multiple
showEmptyMessage
scrollHeight="300px"
value={selectedItem}
suggestions={filteredItems}
completeMethod={searchItems}
onChange={e => {
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
}}
emptyMessage="Not found any system..."
placeholder="Type here..."
field="label"
id="value"
className="w-full"
itemTemplate={(item: SearchSystemItem) => {
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
const sortedStatics = sortWHClasses(wormholesData, statics);
const isWH = isWormholeSpace(system_class);
return (
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
<SystemViewStandalone
security={security}
system_class={system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
{effect_name && isWH && (
<WHEffectView
effectName={effect_name}
effectPower={effect_power}
className={classes.SearchItemEffect}
return (
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
<SystemViewStandalone
security={security}
system_class={system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
)}
{isWH && (
<div className="flex gap-1 grow justify-between">
<div></div>
<div className="flex gap-1">
{sortedStatics.map(x => (
<WHClassView key={x} whClassName={x} />
))}
{effect_name && isWH && (
<WHEffectView
effectName={effect_name}
effectPower={effect_power}
className={classes.SearchItemEffect}
/>
)}
{isWH && (
<div className="flex gap-1 grow justify-between">
<div></div>
<div className="flex gap-1">
{sortedStatics.map(x => (
<WHClassView key={x} whClassName={x} />
))}
</div>
</div>
</div>
)}
</div>
);
}}
selectedItemTemplate={(item: SearchSystemItem) => (
<SystemViewStandalone
security={item.system_static_info.security}
system_class={item.system_static_info.system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
)}
/>
</IconField>
)}
</div>
);
}}
selectedItemTemplate={(item: SearchSystemItem) => (
<SystemViewStandalone
security={item.system_static_info.security}
system_class={item.system_static_info.system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
)}
/>
</IconField>
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
</div>
</div>
<div className="flex gap-2 justify-end">
<WdButton
type="submit"
onClick={handleSubmit}
outlined
disabled={!selectedItem || selectedItem.length !== 1}
size="small"
label="Submit"
/>
</div>
</div>
<div className="flex gap-2 justify-end">
<Button
onClick={handleSubmit}
outlined
disabled={!selectedItem || selectedItem.length !== 1}
size="small"
label="Submit"
/>
</div>
</div>
</form>
</Dialog>
);
};

View File

@@ -4,6 +4,7 @@ import {
SystemView,
TimeAgo,
TooltipPosition,
WdButton,
WdImgButton,
WdImgButtonTooltip,
} from '@/hooks/Mapper/components/ui-kit';
@@ -13,7 +14,6 @@ import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { Commands, OutCommand, PingType } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { Button } from 'primereact/button';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { Toast } from 'primereact/toast';
import { useCallback, useEffect, useMemo, useRef } from 'react';
@@ -256,7 +256,7 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
)}
></Toast>
<Button
<WdButton
icon="pi pi-bell"
severity="warning"
aria-label="Notification"

View File

@@ -1,13 +1,12 @@
import { InputText } from 'primereact/inputtext';
import { Dialog } from 'primereact/dialog';
import { TooltipPosition, WdButton, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { IconField } from 'primereact/iconfield';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { WdImageSize, WdImgButton, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield';
import { InputText } from 'primereact/inputtext';
import { useCallback, useEffect, useRef, useState } from 'react';
interface SystemCustomLabelDialog {
systemId: string;
@@ -126,7 +125,7 @@ export const SystemCustomLabelDialog = ({ systemId, visible, setVisible }: Syste
</div>
<div className="flex gap-2 justify-end">
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
<WdButton type="submit" onClick={handleSave} outlined size="small" label="Save"></WdButton>
</div>
</div>
</form>

View File

@@ -9,10 +9,9 @@ import {
} from '@/hooks/Mapper/components/map/constants.ts';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
@@ -116,14 +115,14 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
);
const handleSelect = useCallback(
async (signature: SystemSignature) => {
(signature: SystemSignature) => {
if (!signature) {
return;
}
const { outCommand } = ref.current;
await outCommand({
outCommand({
type: OutCommand.linkSignatureToSystem,
data: {
...data,
@@ -131,32 +130,9 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
},
});
if (parseSignatureCustomInfo(signature.custom_info).isEOL === true) {
await outCommand({
type: OutCommand.updateConnectionTimeStatus,
data: {
source: data.solar_system_source,
target: data.solar_system_target,
value: TimeStatus.eol,
},
});
}
const whShipSize = getWhSize(wormholes, signature.type);
if (whShipSize !== undefined && whShipSize !== null) {
await outCommand({
type: OutCommand.updateConnectionShipSizeType,
data: {
source: data.solar_system_source,
target: data.solar_system_target,
value: whShipSize,
},
});
}
setVisible(false);
},
[data, setVisible, wormholes],
[data, setVisible],
);
useEffect(() => {

View File

@@ -1,12 +1,11 @@
import { InputTextarea } from 'primereact/inputtextarea';
import { Dialog } from 'primereact/dialog';
import { SystemView, WdButton } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { PingType } from '@/hooks/Mapper/types/ping.ts';
import { SystemView } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
import { Dialog } from 'primereact/dialog';
import { InputTextarea } from 'primereact/inputtextarea';
import { useCallback, useRef, useState } from 'react';
const PING_TITLES = {
[PingType.Rally]: 'RALLY',
@@ -63,7 +62,7 @@ export const SystemPingDialog = ({ systemId, type, visible, setVisible }: System
</div>
}
visible={visible}
draggable={false}
draggable={true}
style={{ width: '450px' }}
onShow={onShow}
onHide={() => {
@@ -92,7 +91,7 @@ export const SystemPingDialog = ({ systemId, type, visible, setVisible }: System
</div>
<div className="flex gap-2 justify-end">
<Button onClick={handleSave} size="small" severity="danger" label="Ping!"></Button>
<WdButton type="submit" onClick={handleSave} size="small" severity="danger" label="Ping!" />
</div>
</div>
</form>

View File

@@ -1,16 +1,15 @@
import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
import { Dialog } from 'primereact/dialog';
import { TooltipPosition, WdButton, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { IconField } from 'primereact/iconfield';
import { TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
import { OutCommand } from '@/hooks/Mapper/types';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield';
import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
import { useCallback, useEffect, useRef, useState } from 'react';
interface SystemSettingsDialog {
systemId: string;
@@ -114,7 +113,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
<Dialog
header="System settings"
visible={visible}
draggable={false}
draggable={true}
style={{ width: '450px' }}
onShow={onShow}
onHide={() => {
@@ -226,7 +225,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
</div>
<div className="flex gap-2 justify-end">
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
<WdButton onClick={handleSave} outlined size="small" label="Save" type="submit" />
</div>
</div>
</form>

View File

@@ -1,11 +1,9 @@
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { Dialog } from 'primereact/dialog';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import {
RoutesType,
useRouteProvider,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
interface RoutesSettingsDialog {
visible: boolean;
@@ -83,7 +81,7 @@ export const RoutesSettingsDialog = ({ visible, setVisible }: RoutesSettingsDial
</div>
<div className="flex gap-2 justify-end">
<Button onClick={handleSave} outlined size="small" label="Apply"></Button>
<WdButton onClick={handleSave} outlined size="small" label="Apply"></WdButton>
</div>
</div>
</Dialog>

View File

@@ -3,6 +3,7 @@ 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';
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
@@ -64,12 +65,8 @@ export const useLoadRoutes = ({
systems?.length,
connections,
hubs,
routesSettings,
...Object.keys(routesSettings)
.sort()
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
.map(x => routesSettings[x]),
// we need make it flat recursively
...flattenValues(routesSettings),
...deps,
]);

View File

@@ -1,6 +1,5 @@
import { Dialog } from 'primereact/dialog';
import { useCallback, useState } from 'react';
import { Button } from 'primereact/button';
import { TabPanel, TabView } from 'primereact/tabview';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { Dropdown } from 'primereact/dropdown';
@@ -10,6 +9,7 @@ import {
SIGNATURE_SETTINGS,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
interface SystemSignatureSettingsDialogProps {
settings: SignatureSettingsType;
@@ -92,7 +92,7 @@ export const SystemSignatureSettingsDialog = ({
</div>
<div className="flex gap-2 justify-end">
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
<WdButton onClick={handleSave} outlined size="small" label="Save" />
</div>
</div>
</Dialog>

View File

@@ -1,13 +1,13 @@
import React, { useEffect, useState, useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { AutoComplete } from 'primereact/autocomplete';
import { Calendar } from 'primereact/calendar';
import clsx from 'clsx';
import { StructureItem, StructureStatus, statusesRequiringTimer, formatToISO } from '../helpers';
import { formatToISO, statusesRequiringTimer, StructureItem, StructureStatus } from '../helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
interface StructuresEditDialogProps {
visible: boolean;
@@ -54,14 +54,13 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
// 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()),
);
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 },
@@ -96,9 +95,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
// when user picks a corp from auto-complete
const handleSelectOwner = (selected: { label: string; value: string }) => {
setOwnerInput(selected.label);
setEditData(prev =>
prev ? { ...prev, ownerName: selected.label, ownerId: selected.value } : null,
);
setEditData(prev => (prev ? { ...prev, ownerName: selected.label, ownerId: selected.value } : null));
};
const handleStatusChange = (val: string) => {
@@ -125,6 +122,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
// fetch corporation ticker if we have an ownerId
if (editData.ownerId) {
try {
// TODO fix it
const { ticker } = await outCommand({
type: OutCommand.getCorporationTicker,
data: { corp_id: editData.ownerId },
@@ -157,11 +155,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
<div className="flex flex-col gap-2 text-[14px]">
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<span>Type:</span>
<input
readOnly
className="p-inputtext p-component cursor-not-allowed"
value={editData.structureType ?? ''}
/>
<input readOnly className="p-inputtext p-component cursor-not-allowed" value={editData.structureType ?? ''} />
</label>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<span>Name:</span>
@@ -204,10 +198,12 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
{statusesRequiringTimer.includes(editData.status) && (
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<span>Timer <br /> (Eve Time):</span>
<span>
Timer <br /> (Eve Time):
</span>
<Calendar
value={editData.endTime ? new Date(editData.endTime) : undefined}
onChange={(e) => handleChange('endTime', e.value ?? '')}
onChange={e => handleChange('endTime', e.value ?? '')}
showTime
hourFormat="24"
dateFormat="yy-mm-dd"
@@ -227,8 +223,8 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
</div>
<div className="flex justify-end items-center gap-2 mt-4">
<Button label="Delete" severity="danger" className="p-button-sm" onClick={handleDeleteClick} />
<Button label="Save" className="p-button-sm" onClick={handleSaveClick} />
<WdButton label="Delete" severity="danger" className="p-button-sm" onClick={handleDeleteClick} />
<WdButton label="Save" className="p-button-sm" onClick={handleSaveClick} />
</div>
</Dialog>
);

View File

@@ -16,8 +16,21 @@ const SystemKillsContent = () => {
} = useMapRootState();
const [systemId] = selectedSystems || [];
const whCacheRef = useMemo(() => new Map<number, boolean>(), []);
const systemStaticInfo = getSystemStaticInfo(systemId)!;
const isWormholeSystem = useCallback(
(systemId: number): boolean => {
const cached = whCacheRef.get(systemId);
if (cached !== undefined) return cached;
const info = getSystemStaticInfo(systemId);
const isWH = info?.system_class != null ? isWormholeSpace(Number(info.system_class)) : false;
whCacheRef.set(systemId, isWH);
return isWH;
},
[whCacheRef],
);
const { kills, isLoading, error } = useSystemKills({
systemId,
@@ -30,15 +43,9 @@ const SystemKillsContent = () => {
const showLoading = isLoading && kills.length === 0;
const filteredKills = useMemo(() => {
if (!settingsKills.whOnly || !settingsKills.showAll) return kills;
return kills.filter(kill => {
if (!systemStaticInfo) {
console.warn(`System with id ${kill.solar_system_id} not found.`);
return false;
}
return isWormholeSpace(systemStaticInfo.system_class);
});
}, [kills, settingsKills.whOnly, systemStaticInfo, settingsKills.showAll]);
if (!settingsKills.whOnly) return kills;
return kills.filter(kill => isWormholeSystem(Number(kill.solar_system_id)));
}, [kills, settingsKills.whOnly, isWormholeSystem]);
if (!isSubscriptionActive) {
return (

View File

@@ -1,13 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { SystemView, TooltipPosition, WdButton, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { PrimeIcons } from 'primereact/api';
import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import { SystemView, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
interface KillsSettingsDialogProps {
@@ -158,7 +156,7 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
</div>
<div className="flex gap-2 justify-end mt-4">
<Button onClick={handleApply} label="Apply" outlined size="small" />
<WdButton onClick={handleApply} label="Apply" outlined size="small" />
</div>
</div>

View File

@@ -1,22 +1,21 @@
import classes from './Connections.module.scss';
import { Sidebar } from 'primereact/sidebar';
import { useEffect, useMemo, useState, useCallback } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import clsx from 'clsx';
import {
ConnectionType,
ConnectionOutput,
ConnectionInfoOutput,
ConnectionOutput,
ConnectionType,
OutCommand,
Passage,
SolarSystemConnection,
} from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { Sidebar } from 'primereact/sidebar';
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import { useCallback, useEffect, useMemo, useState } from 'react';
import classes from './Connections.module.scss';
import { PassageCard } from './PassageCard';
import { InfoDrawer, SystemView } from '@/hooks/Mapper/components/ui-kit';
import { InfoDrawer, SystemView, TimeAgo } from '@/hooks/Mapper/components/ui-kit';
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
import { TimeAgo } from '@/hooks/Mapper/components/ui-kit';
import { PassageCard } from './PassageCard';
const sortByDate = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime();
@@ -78,7 +77,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
}, [connections, selectedConnection]);
const isWormhole = useMemo(() => {
return cnInfo?.type !== ConnectionType.gate;
return cnInfo?.type === ConnectionType.wormhole;
}, [cnInfo]);
const [passages, setPassages] = useState<Passage[]>([]);

View File

@@ -1,7 +1,6 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import { Button } from 'primereact/button';
import { callToastError, callToastSuccess, callToastWarn } from '@/hooks/Mapper/helpers';
import { OutCommand } from '@/hooks/Mapper/types';
import { ConfirmPopup } from 'primereact/confirmpopup';
@@ -10,6 +9,7 @@ import { MapUserSettings, RemoteAdminSettingsResponse } from '@/hooks/Mapper/map
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import fastDeepEqual from 'fast-deep-equal';
import { useDetectSettingsChanged } from '@/hooks/Mapper/components/hooks';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
export const AdminSettings = () => {
const {
@@ -92,7 +92,7 @@ export const AdminSettings = () => {
<div className="w-full h-full flex flex-col gap-5">
<div className="flex flex-col gap-1">
<div>
<Button
<WdButton
// @ts-ignore
ref={cfRef}
onClick={cfShow}

View File

@@ -7,8 +7,7 @@ import {
import { useMapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx';
import { SettingsListItem } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/types.ts';
import { useCallback } from 'react';
import { Button } from 'primereact/button';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { TooltipPosition, WdButton, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
@@ -42,7 +41,7 @@ export const CommonSettings = () => {
<div className="grid grid-cols-[1fr_auto]">
<div />
<WdTooltipWrapper content="This dangerous action. And can not be undone" position={TooltipPosition.top}>
<Button
<WdButton
// @ts-ignore
ref={cfRef}
className="py-[4px]"

View File

@@ -1,10 +1,10 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { Divider } from 'primereact/divider';
import { callToastError, callToastSuccess, callToastWarn } from '@/hooks/Mapper/helpers';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
type SaveDefaultSettingsReturn = { success: boolean; error: string };
@@ -65,7 +65,7 @@ export const DefaultSettings = () => {
<div className="flex flex-col gap-1">
<div>
<Button
<WdButton
onClick={handleSaveAsDefault}
icon="pi pi-save"
size="small"

View File

@@ -2,13 +2,13 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { createDefaultWidgetSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
import { callToastSuccess } from '@/hooks/Mapper/helpers';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
import { RemoteAdminSettingsResponse } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
export const ServerSettings = () => {
const {
@@ -64,7 +64,7 @@ export const ServerSettings = () => {
<div className="w-full h-full flex flex-col gap-5">
<div className="flex flex-col gap-1">
<div>
<Button
<WdButton
// @ts-ignore
ref={cfRef}
onClick={cfShow}

View File

@@ -2,8 +2,7 @@ import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/compon
import { WIDGETS_CHECKBOXES_PROPS, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback } from 'react';
import { Button } from 'primereact/button';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
export interface WidgetsSettingsProps {}
@@ -33,7 +32,7 @@ export const WidgetsSettings = ({}: WidgetsSettingsProps) => {
<div className="grid grid-cols-[1fr_auto]">
<div />
<Button className="py-[4px]" onClick={resetWidgets} outlined size="small" label="Reset Widgets"></Button>
<WdButton className="py-[4px]" onClick={resetWidgets} outlined size="small" label="Reset Widgets" />
</div>
</div>
);

View File

@@ -1,6 +1,6 @@
import { SettingsListItem, UserSettingsRemoteProps } from './types.ts';
import { InterfaceStoredSettingsProps } from '@/hooks/Mapper/mapRootProvider';
import { AvailableThemes, MiniMapPlacement, PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { SettingsListItem, UserSettingsRemoteProps } from './types.ts';
export const DEFAULT_REMOTE_SETTINGS = {
[UserSettingsRemoteProps.link_signature_on_splash]: false,
@@ -51,7 +51,7 @@ export const SIGNATURES_CHECKBOXES_PROPS: SettingsListItem[] = [
export const CONNECTIONS_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: UserSettingsRemoteProps.delete_connection_with_sigs,
label: 'Delete connections to linked signatures',
label: 'Delete connections with linked signatures',
type: 'checkbox',
},
{

View File

@@ -11,11 +11,11 @@ import {
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { saveTextFile } from '@/hooks/Mapper/utils';
import { Button } from 'primereact/button';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { Dialog } from 'primereact/dialog';
import { Toast } from 'primereact/toast';
import { useCallback, useRef } from 'react';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
const createSettings = function <T>(lsSettings: string | null, defaultValues: T) {
return {
@@ -139,7 +139,7 @@ export const OldSettingsDialog = () => {
className="w-[640px] h-[400px] text-text-color min-h-0"
footer={
<div className="flex items-center justify-end">
<Button
<WdButton
// @ts-ignore
ref={cfRef}
onClick={cfShow}
@@ -168,7 +168,7 @@ export const OldSettingsDialog = () => {
<div className="h-[30px]"></div>
<div className="flex items-center gap-3">
<Button
<WdButton
onClick={handleExportClipboard}
icon="pi pi-copy"
size="small"
@@ -176,7 +176,7 @@ export const OldSettingsDialog = () => {
label="Export to Clipboard"
/>
<Button
<WdButton
onClick={handleExportAsFile}
icon="pi pi-download"
size="small"

View File

@@ -8,11 +8,14 @@ import {
} from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components';
import { InputText } from 'primereact/inputtext';
import { SystemsSettingsProvider } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/Provider.tsx';
import { Button } from 'primereact/button';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
type SystemSignaturePrepared = Omit<SystemSignature, 'linked_system'> & { linked_system: string };
type SystemSignaturePrepared = Omit<SystemSignature, 'linked_system'> & {
linked_system: string;
k162Type: string;
time_status: TimeStatus;
};
export interface MapSettingsProps {
systemId: string;
@@ -22,10 +25,7 @@ export interface MapSettingsProps {
}
export const SignatureSettings = ({ systemId, show, onHide, signatureData }: MapSettingsProps) => {
const {
outCommand,
data: { wormholes },
} = useMapRootState();
const { outCommand } = useMapRootState();
const handleShow = async () => {};
const signatureForm = useForm<Partial<SystemSignaturePrepared>>({});
@@ -52,41 +52,13 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
solar_system_target: values.linked_system,
},
});
// TODO: need fix
if (values.isEOL) {
await outCommand({
type: OutCommand.updateConnectionTimeStatus,
data: {
source: systemId,
target: values.linked_system,
value: TimeStatus.eol,
},
});
}
if (values.type) {
const whShipSize = getWhSize(wormholes, values.type);
if (whShipSize !== undefined && whShipSize !== null) {
await outCommand({
type: OutCommand.updateConnectionShipSizeType,
data: {
source: systemId,
target: values.linked_system,
value: whShipSize,
},
});
}
}
}
out = {
...out,
custom_info: JSON.stringify({
// TODO: need fix
k162Type: values.k162Type,
// TODO: need fix
isEOL: values.isEOL,
time_status: values.time_status,
}),
};
@@ -153,7 +125,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
signatureForm.reset();
onHide();
},
[signatureData, signatureForm, outCommand, systemId, onHide, wormholes],
[signatureData, signatureForm, outCommand, systemId, onHide],
);
useEffect(() => {
@@ -165,18 +137,17 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
const { linked_system, custom_info, ...rest } = signatureData;
let k162Type = null;
let isEOL = false;
let time_status = TimeStatus._24h;
if (custom_info) {
const customInfo = JSON.parse(custom_info);
k162Type = customInfo.k162Type;
isEOL = customInfo.isEOL;
time_status = customInfo.time_status;
}
signatureForm.reset({
linked_system: linked_system?.solar_system_id.toString() ?? undefined,
// TODO: need fix
k162Type: k162Type,
isEOL: isEOL,
time_status: time_status,
...rest,
});
}, [signatureForm, signatureData]);
@@ -185,7 +156,8 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
<Dialog
header={`Signature Edit [${signatureData?.eve_id}]`}
visible={show}
draggable={false}
draggable
resizable={false}
style={{ width: '390px' }}
onShow={handleShow}
onHide={() => {
@@ -220,8 +192,8 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
</label>
</div>
<div className="flex gap-2 justify-end">
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
<div className="flex gap-2 justify-end px-[0.75rem] pb-[0.5rem]">
<WdButton type="submit" outlined size="small" label="Save" />
</div>
</div>
</form>

View File

@@ -1,24 +0,0 @@
import { InputSwitch } from 'primereact/inputswitch';
import { Controller, useFormContext } from 'react-hook-form';
import { SystemSignature } from '@/hooks/Mapper/types';
export interface SignatureEOLCheckboxProps {
name: string;
defaultValue?: boolean;
}
export const SignatureEOLCheckbox = ({ name, defaultValue = false }: SignatureEOLCheckboxProps) => {
const { control } = useFormContext<SystemSignature>();
return (
<Controller
// @ts-ignore
name={name}
control={control}
defaultValue={defaultValue}
render={({ field }) => {
return <InputSwitch className="my-1" checked={!!field.value} onChange={e => field.onChange(e.value)} />;
}}
/>
);
};

View File

@@ -3,7 +3,7 @@ import { SystemSignature } from '@/hooks/Mapper/types';
import { SignatureWormholeTypeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureWormholeTypeSelect';
import { SignatureK162TypeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
import { SignatureLeadsToSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLeadsToSelect';
import { SignatureEOLCheckbox } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureEOLCheckbox';
import { SignatureLifetimeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLifetimeSelect.tsx';
import { SignatureTempName } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureTempName.tsx';
export const SignatureGroupContentWormholes = () => {
@@ -29,10 +29,10 @@ export const SignatureGroupContentWormholes = () => {
<SignatureLeadsToSelect name="linked_system" />
</label>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
<span>EOL:</span>
<SignatureEOLCheckbox name="isEOL" />
</label>
<div className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
<span>Lifetime:</span>
<SignatureLifetimeSelect name="time_status" />
</div>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
<span>Temp. Name:</span>

View File

@@ -20,7 +20,7 @@ const renderLinkedSystemItem = (option: { value: string }) => {
return (
<div className="flex gap-2 items-center">
<SystemView systemId={value} className={classes.SystemView} />
<SystemView systemId={value} className={classes.SystemView} showCustomName={true} />
</div>
);
};
@@ -37,7 +37,7 @@ const renderLinkedSystemValue = (option: { value: string }) => {
return (
<div className="flex gap-2 items-center">
<SystemView systemId={option.value} className={classes.SystemView} />
<SystemView systemId={option.value} className={classes.SystemView} showCustomName={true} />
</div>
);
};

View File

@@ -0,0 +1,27 @@
import { Controller, useFormContext } from 'react-hook-form';
import { SystemSignature } from '@/hooks/Mapper/types';
import { WdLifetimeSelector } from '@/hooks/Mapper/components/ui-kit/WdLifetimeSelector.tsx';
export interface SignatureEOLCheckboxProps {
name: string;
defaultValue?: boolean;
}
export const SignatureLifetimeSelect = ({ name, defaultValue = false }: SignatureEOLCheckboxProps) => {
const { control } = useFormContext<SystemSignature>();
return (
<div className="my-1">
<Controller
// @ts-ignore
name={name}
control={control}
defaultValue={defaultValue}
render={({ field }) => {
// @ts-ignore
return <WdLifetimeSelector lifetime={field.value} onChangeLifetime={e => field.onChange(e)} />;
}}
/>
</div>
);
};

View File

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

View File

@@ -42,5 +42,5 @@ export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomN
return <SystemViewStandalone {...rest} {...systemInfo} />;
}
return <SystemViewStandalone customName={mapSystemInfo.name ?? undefined} {...rest} {...systemInfo} />;
return <SystemViewStandalone customName={mapSystemInfo.temporary_name ?? mapSystemInfo.name ?? undefined} {...rest} {...systemInfo} />;
};

View File

@@ -0,0 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import { Button, ButtonProps } from 'primereact/button';
export const WdButton = ({ type = 'button', ...props }: ButtonProps) => {
// eslint-disable-next-line react/forbid-elements
return <Button {...props} type={type} />;
};

View File

@@ -0,0 +1,86 @@
import { WdButton } from '@/hooks/Mapper/components/ui-kit/WdButton.tsx';
import { TimeStatus } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { BUILT_IN_TOOLTIP_OPTIONS } from './constants.ts';
const LIFE_TIME = [
{
id: TimeStatus._1h,
label: '1H',
className: 'bg-purple-400 hover:!bg-purple-400',
inactiveClassName: 'bg-purple-400/30',
description: 'Less than one 1 hours remaining',
},
{
id: TimeStatus._4h,
label: '4H',
className: 'bg-purple-300 hover:!bg-purple-300',
inactiveClassName: 'bg-purple-300/30',
description: 'Less than one 4 hours remaining',
},
{
id: TimeStatus._4h30m,
label: '4.5H',
className: 'bg-indigo-300 hover:!bg-indigo-300',
inactiveClassName: 'bg-indigo-300/30',
description: 'Less than one 4.5 hours remaining. All small holes have such lifetime.',
},
{
id: TimeStatus._16h,
label: '16H',
className: 'bg-orange-300 hover:!bg-orange-300',
inactiveClassName: 'bg-orange-400/30',
description: 'Less than one 16 hours remaining',
},
{
id: TimeStatus._24h,
label: '24H',
className: 'bg-orange-300 hover:!bg-orange-300',
inactiveClassName: 'bg-orange-400/30',
description: 'Less than one 24 hours remaining',
},
{
id: TimeStatus._48h,
label: '48H',
className: 'bg-orange-300 hover:!bg-orange-300',
inactiveClassName: 'bg-orange-400/30',
description: 'Less than one 24 hours remaining. Related only with C6. B041, B520, U319, C391.',
},
];
export interface WdLifetimeSelectorProps {
lifetime?: TimeStatus;
onChangeLifetime(lifetime: TimeStatus): void;
className?: string;
}
export const WdLifetimeSelector = ({
lifetime = TimeStatus._24h,
onChangeLifetime,
className,
}: WdLifetimeSelectorProps) => {
return (
<form>
<div className={clsx('grid grid-cols-[1fr_1fr_1fr_1fr_1fr_1fr] gap-1', className)}>
{LIFE_TIME.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]: lifetime !== x.id },
x.className,
)}
onClick={() => onChangeLifetime(x.id)}
>
{x.label}
</WdButton>
))}
</div>
</form>
);
};

View File

@@ -18,6 +18,7 @@ export interface TooltipProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
content: (() => React.ReactNode) | React.ReactNode;
targetSelector?: string;
interactive?: boolean;
smallPaddings?: boolean;
}
export interface OffsetPosition {
@@ -47,6 +48,7 @@ export const WdTooltip = forwardRef(
position: tPosition = TooltipPosition.default,
offset = 5,
interactive = false,
smallPaddings = false,
className,
...restProps
}: TooltipProps,
@@ -264,10 +266,14 @@ export const WdTooltip = forwardRef(
ref={tooltipRef}
className={clsx(
classes.tooltip,
interactive ? 'pointer-events-auto' : 'pointer-events-none',
'absolute px-1 py-1',
'absolute px-2 py-1',
'border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
pos == null && 'invisible',
{
'pointer-events-auto': interactive,
'pointer-events-none': !interactive,
invisible: pos == null,
'!px-1': smallPaddings,
},
className,
)}
style={{

View File

@@ -8,13 +8,26 @@ export type WdTooltipWrapperProps = {
content?: (() => ReactNode) | ReactNode;
size?: TooltipSize;
interactive?: boolean;
smallPaddings?: boolean;
tooltipClassName?: string;
} & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> &
Omit<TooltipProps, 'content'>;
export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperProps>(
(
{ className, children, content, offset, position, targetSelector, interactive, size, tooltipClassName, ...props },
{
className,
children,
content,
offset,
position,
targetSelector,
interactive,
smallPaddings,
size,
tooltipClassName,
...props
},
forwardedRef,
) => {
const suffix = useMemo(() => Math.random().toString(36).slice(2, 7), []);
@@ -31,6 +44,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
position={position}
content={content}
interactive={interactive}
smallPaddings={smallPaddings}
targetSelector={finalTargetSelector}
className={clsx(size && sizeClass(size), tooltipClassName)}
/>

View File

@@ -0,0 +1,6 @@
export const BUILT_IN_TOOLTIP_OPTIONS = {
mouseTrack: true,
mouseTrackLeft: 10,
className:
'rounded-[3px] bg-stone-900/90 px-1 py-1 [&_.p-tooltip-text]:!text-stone-300 text-[13px] [&_.p-tooltip-text]:!p-1',
};

View File

@@ -21,3 +21,5 @@ export * from './LoadingWrapper';
export * from './WdMenuItem';
export * from './MenuItemWithInfo';
export * from './MarkdownTextViewer.tsx';
export * from './WdButton.tsx';
export * from './constants.ts';

View File

@@ -1,6 +1,7 @@
export * from './useClipboard';
export * from './useConfirmPopup';
export * from './useEventBuffer';
export * from './useHotkey';
export * from './usePageVisibility';
export * from './useSkipContextMenu';
export * from './useThrottle';
export * from './useConfirmPopup';

View File

@@ -0,0 +1,41 @@
import debounce from 'lodash.debounce';
import { useCallback, useRef } from 'react';
export type UseEventBufferHandler<T> = (event: T) => void;
export const useEventBuffer = <T>(handler: UseEventBufferHandler<T>) => {
// @ts-ignore
const eventsBufferRef = useRef<T[]>([]);
const eventTick = useCallback(
debounce(() => {
if (eventsBufferRef.current.length === 0) {
return;
}
const event = eventsBufferRef.current.shift()!;
handler(event);
// TODO - do not delete THIS code it needs for debug
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
if (eventsBufferRef.current.length > 0) {
eventTick();
}
}, 10),
[],
);
const eventTickRef = useRef(eventTick);
eventTickRef.current = eventTick;
// @ts-ignore
const handleEvent = useCallback(event => {
if (!eventTickRef.current) {
return;
}
eventsBufferRef.current.push(event);
eventTickRef.current();
}, []);
return { handleEvent };
};

View File

@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { CommandInit } from '@/hooks/Mapper/types';
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { CommandInit } from '@/hooks/Mapper/types';
import { useCallback } from 'react';
export const useMapInit = () => {
const { update } = useMapRootState();

View File

@@ -1,4 +1,3 @@
import { ForwardedRef, useImperativeHandle } from 'react';
import {
CommandAddConnections,
CommandAddSystems,
@@ -8,24 +7,25 @@ import {
CommandCharactersUpdated,
CommandCharacterUpdated,
CommandCommentAdd,
CommandCommentRemoved,
CommandInit,
CommandLinkSignatureToSystem,
CommandMapUpdated,
CommandPingAdded,
CommandPingCancelled,
CommandPresentCharacters,
CommandRemoveConnections,
CommandRemoveSystems,
CommandRoutes,
Commands,
CommandSignaturesUpdated,
CommandTrackingCharactersData,
CommandUpdateConnection,
CommandUpdateSystems,
CommandUserSettingsUpdated,
Commands,
MapHandlers,
CommandCommentRemoved,
CommandPingAdded,
CommandPingCancelled,
} from '@/hooks/Mapper/types/mapHandlers.ts';
import { ForwardedRef, useImperativeHandle } from 'react';
import {
useCommandComments,
@@ -39,9 +39,9 @@ import {
useUserRoutes,
} from './api';
import { useCommandsActivity } from './api/useCommandsActivity';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { DetailedKill } from '../../types/kills';
import { useCommandsActivity } from './api/useCommandsActivity';
export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
const mapInit = useMapInit();
@@ -63,127 +63,123 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
const { pingAdded, pingCancelled } = useCommandPings();
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
useImperativeHandle(
ref,
() => {
return {
command(type, data) {
switch (type) {
case Commands.init: // USED
mapInit(data as CommandInit);
break;
case Commands.addSystems: // USED
addSystems(data as CommandAddSystems);
break;
case Commands.updateSystems: // USED
updateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems: // USED
removeSystems(data as CommandRemoveSystems);
break;
case Commands.addConnections: // USED
addConnections(data as CommandAddConnections);
break;
case Commands.removeConnections: // USED
removeConnections(data as CommandRemoveConnections);
break;
case Commands.updateConnection: // USED
updateConnection(data as CommandUpdateConnection);
break;
case Commands.charactersUpdated: // USED
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded: // USED
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved: // USED
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated: // USED
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters: // USED
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.mapUpdated: // USED
mapUpdated(data as CommandMapUpdated);
break;
case Commands.routes:
mapRoutes(data as CommandRoutes);
break;
case Commands.userRoutes:
mapUserRoutes(data as CommandRoutes);
break;
useImperativeHandle(ref, () => {
return {
command(type, data) {
switch (type) {
case Commands.init: // USED
mapInit(data as CommandInit);
break;
case Commands.addSystems: // USED
addSystems(data as CommandAddSystems);
break;
case Commands.updateSystems: // USED
updateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems: // USED
removeSystems(data as CommandRemoveSystems);
break;
case Commands.addConnections: // USED
addConnections(data as CommandAddConnections);
break;
case Commands.removeConnections: // USED
removeConnections(data as CommandRemoveConnections);
break;
case Commands.updateConnection: // USED
updateConnection(data as CommandUpdateConnection);
break;
case Commands.charactersUpdated: // USED
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded: // USED
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved: // USED
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated: // USED
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters: // USED
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.mapUpdated: // USED
mapUpdated(data as CommandMapUpdated);
break;
case Commands.routes:
mapRoutes(data as CommandRoutes);
break;
case Commands.userRoutes:
mapUserRoutes(data as CommandRoutes);
break;
case Commands.signaturesUpdated: // USED
updateSystemSignatures(data as CommandSignaturesUpdated);
break;
case Commands.signaturesUpdated: // USED
updateSystemSignatures(data as CommandSignaturesUpdated);
break;
case Commands.linkSignatureToSystem: // USED
setTimeout(() => {
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
}, 200);
break;
case Commands.linkSignatureToSystem: // USED
setTimeout(() => {
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
}, 200);
break;
case Commands.centerSystem: // USED
// do nothing here
break;
case Commands.centerSystem: // USED
// do nothing here
break;
case Commands.selectSystem: // USED
// do nothing here
break;
case Commands.selectSystem: // USED
// do nothing here
break;
case Commands.killsUpdated:
// do nothing here
break;
case Commands.killsUpdated:
// do nothing here
break;
case Commands.detailedKillsUpdated:
updateDetailedKills(data as Record<string, DetailedKill[]>);
break;
case Commands.detailedKillsUpdated:
updateDetailedKills(data as Record<string, DetailedKill[]>);
break;
case Commands.characterActivityData:
characterActivityData(data as CommandCharacterActivityData);
break;
case Commands.characterActivityData:
characterActivityData(data as CommandCharacterActivityData);
break;
case Commands.trackingCharactersData:
trackingCharactersData(data as CommandTrackingCharactersData);
break;
case Commands.trackingCharactersData:
trackingCharactersData(data as CommandTrackingCharactersData);
break;
case Commands.updateActivity:
break;
case Commands.updateActivity:
break;
case Commands.updateTracking:
break;
case Commands.updateTracking:
break;
case Commands.userSettingsUpdated:
userSettingsUpdated(data as CommandUserSettingsUpdated);
break;
case Commands.userSettingsUpdated:
userSettingsUpdated(data as CommandUserSettingsUpdated);
break;
case Commands.systemCommentAdded:
addComment(data as CommandCommentAdd);
break;
case Commands.systemCommentAdded:
addComment(data as CommandCommentAdd);
break;
case Commands.systemCommentRemoved:
removeComment(data as CommandCommentRemoved);
break;
case Commands.systemCommentRemoved:
removeComment(data as CommandCommentRemoved);
break;
case Commands.pingAdded:
pingAdded(data as CommandPingAdded);
break;
case Commands.pingAdded:
pingAdded(data as CommandPingAdded);
break;
case Commands.pingCancelled:
pingCancelled(data as CommandPingCancelled);
break;
case Commands.pingCancelled:
pingCancelled(data as CommandPingCancelled);
break;
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;
}
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;
}
emitMapEvent({ name: type, data });
},
};
},
[],
);
emitMapEvent({ name: type, data });
},
};
}, []);
};

View File

@@ -1,6 +1,7 @@
export enum ConnectionType {
wormhole,
gate,
bridge,
}
export enum MassState {
@@ -10,8 +11,13 @@ export enum MassState {
}
export enum TimeStatus {
default,
eol,
reserved, // TODO: this reserved for not broke prev solution
_1h,
_4h,
_4h30m,
_16h,
_24h,
_48h,
}
export enum ShipSizeStatus {

View File

@@ -122,7 +122,7 @@ export enum SignatureGroupRU {
export enum SignatureGroupFR {
CosmicSignature = 'Signature cosmique (groupe)',
Wormhole = 'Trou de ver',
GasSite = 'Site de gaz',
GasSite = 'Site de collecte de gaz',
RelicSite = 'Site de reliques',
DataSite = 'Site de données',
OreSite = 'Site de minerai',

View File

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

View File

@@ -0,0 +1,132 @@
const TYPE_ORDER = [
'undefined',
'null',
'boolean',
'number',
'bigint',
'string',
'symbol',
'function',
'date',
'regexp',
'other',
] as const;
type TypeTag = (typeof TYPE_ORDER)[number];
const getTypeTag = (v: unknown): TypeTag => {
if (v === undefined) return 'undefined';
if (v === null) return 'null';
const t = typeof v;
if (t === 'boolean' || t === 'number' || t === 'bigint' || t === 'string' || t === 'symbol' || t === 'function')
return t as TypeTag;
const tag = Object.prototype.toString.call(v);
if (tag === '[object Date]') return 'date';
if (tag === '[object RegExp]') return 'regexp';
return 'other';
};
const cmp = (a: unknown, b: unknown): number => {
const ta = getTypeTag(a);
const tb = getTypeTag(b);
if (ta !== tb) return TYPE_ORDER.indexOf(ta) - TYPE_ORDER.indexOf(tb);
switch (ta) {
case 'undefined':
case 'null':
return 0;
case 'boolean':
return (a as boolean) === (b as boolean) ? 0 : a ? 1 : -1;
case 'number': {
const na = a as number,
nb = b as number;
const aIsNaN = Number.isNaN(na),
bIsNaN = Number.isNaN(nb);
if (aIsNaN || bIsNaN) return aIsNaN && bIsNaN ? 0 : aIsNaN ? 1 : -1; // NaN в конец чисел
return na === nb ? 0 : na < nb ? -1 : 1;
}
case 'bigint': {
const ba = a as bigint,
bb = b as bigint;
return ba === bb ? 0 : ba < bb ? -1 : 1;
}
case 'string':
return (a as string).localeCompare(b as string);
case 'symbol': {
const da = (a as symbol).description ?? '';
const db = (b as symbol).description ?? '';
return da.localeCompare(db);
}
case 'function':
// @ts-ignore
return ((a as Function).name || '').localeCompare((b as Function).name || '');
case 'date':
return (a as Date).getTime() - (b as Date).getTime();
case 'regexp':
return a!.toString().localeCompare(b!.toString());
default:
return String(a).localeCompare(String(b));
}
};
const isIterable = (v: unknown): v is Iterable<unknown> =>
v != null && typeof (v as any)[Symbol.iterator] === 'function';
const pushTypedArrayValues = (v: unknown, out: unknown[]) => {
if (ArrayBuffer.isView(v) && !(v instanceof DataView)) {
// @ts-ignore
out.push(...(v as ArrayLike<number> as any));
return true;
}
return false;
};
/**
* Generate this func with ChatGPT 5. Cause it pure func and looks like what i need
* May be in net we can find smtng like that
* @param input
*/
export const flattenValues = (input: unknown): unknown[] => {
const out: unknown[] = [];
const seen = new WeakSet<object>();
const visit = (v: unknown): void => {
const tag = getTypeTag(v);
if (tag !== 'other') {
out.push(v);
return;
}
if (v && typeof v === 'object') {
if (seen.has(v)) return;
seen.add(v);
if (pushTypedArrayValues(v, out)) return;
if (v instanceof Map) {
for (const val of v.values()) visit(val);
return;
}
if (v instanceof Set) {
for (const val of v.values()) visit(val);
return;
}
if (Array.isArray(v) || isIterable(v)) {
for (const item of v as Iterable<unknown>) visit(item);
return;
}
for (const key of Object.keys(v)) {
// @ts-ignore
visit((v as never)[key]);
}
return;
}
out.push(v);
};
visit(input);
return out.sort(cmp);
};

View File

@@ -4,7 +4,7 @@ import 'phoenix_html';
import './live_reload.css';
const animateBg = function (bgCanvas) {
const { TweenMax, _ } = window;
const { TweenMax } = window;
/**
* Utility function for returning a random integer in a given range
* @param {Int} max
@@ -212,12 +212,6 @@ const animateBg = function (bgCanvas) {
};
}
window.myJump = new JumpToHyperspace(bgCanvas);
window.addEventListener(
'resize',
_.debounce(() => {
window.myJump.reset();
}, 250),
);
};
document.addEventListener('DOMContentLoaded', function () {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
assets/static/favicon-96x96.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

33
assets/static/site.webmanifest Executable file
View File

@@ -0,0 +1,33 @@
{
"name": "Wanderer",
"short_name": "Wanderer",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#171717",
"background_color": "#171717",
"display": "standalone",
"start_url": "/",
"screenshots": [
{
"src": "web-app-manifest.webp",
"sizes": "720x1280",
"type": "image/webp"
},
{
"src": "web-app-manifest-wide.webp",
"sizes": "1280x720",
"type": "image/webp",
"form_factor": "wide"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -11,11 +11,13 @@ config :wanderer_app, WandererAppWeb.Endpoint,
config :wanderer_app, WandererApp.Repo,
ssl: false,
stacktrace: true,
show_sensitive_data_on_connection_error: true,
show_sensitive_data_on_connection_error: false,
pool_size: 15,
migration_timestamps: [type: :utc_datetime_usec],
migration_lock: nil,
queue_target: 5000
queue_target: 5000,
queue_interval: 1000,
checkout_timeout: 15000
# Configures Swoosh API Client
config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: WandererApp.Finch

View File

@@ -121,6 +121,11 @@ restrict_maps_creation =
|> get_var_from_path_or_env("WANDERER_RESTRICT_MAPS_CREATION", "false")
|> String.to_existing_atom()
restrict_acls_creation =
config_dir
|> get_var_from_path_or_env("WANDERER_RESTRICT_ACLS_CREATION", "false")
|> String.to_existing_atom()
config :wanderer_app,
web_app_url: web_app_url,
git_sha: System.get_env("GIT_SHA", "111"),
@@ -129,6 +134,8 @@ config :wanderer_app,
admin_username: System.get_env("WANDERER_ADMIN_USERNAME", "admin"),
admin_password: System.get_env("WANDERER_ADMIN_PASSWORD"),
admins: admins,
base_metrics_only:
System.get_env("WANDERER_BASE_METRICS_ONLY", "false") |> String.to_existing_atom(),
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
corp_wallet_eve_id: System.get_env("WANDERER_CORP_WALLET_EVE_ID", "-1"),
@@ -148,6 +155,7 @@ config :wanderer_app,
map_connection_eol_expire_timeout_mins: map_connection_eol_expire_timeout_mins,
wallet_tracking_enabled: wallet_tracking_enabled,
restrict_maps_creation: restrict_maps_creation,
restrict_acls_creation: restrict_acls_creation,
subscription_settings: %{
plans: [
%{

View File

@@ -124,7 +124,7 @@ defmodule WandererApp.Api.Character do
update :update_corporation do
require_atomic? false
accept([:corporation_id, :corporation_name, :corporation_ticker, :alliance_id])
accept([:corporation_id, :corporation_name, :corporation_ticker])
end
update :update_alliance do

View File

@@ -79,8 +79,7 @@ defmodule WandererApp.Api.MapCharacterSettings do
accept [
:map_id,
:character_id,
:tracked,
:followed
:tracked
]
argument :map_id, :uuid, allow_nil?: false

View File

@@ -147,8 +147,13 @@ defmodule WandererApp.Api.MapConnection do
allow_nil?(true)
end
# where 0 - normal
# where 1 - end of life
# 0 - normal (env settings)
# 1 - EOL 1h
# 2 - EOL 4h
# 3 - EOL 4.5h
# 4 - EOL 16h
# 5 - EOL 24h
# 6 - EOL 48h
attribute :time_status, :integer do
default(0)
@@ -168,6 +173,7 @@ defmodule WandererApp.Api.MapConnection do
# where 0 - Wormhole
# where 1 - Gate
# where 2 - Bridge
attribute :type, :integer do
default(0)

View File

@@ -54,6 +54,7 @@ defmodule WandererApp.Application do
child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
{PartitionSupervisor,
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
WandererAppWeb.PresenceGracePeriodManager,
WandererAppWeb.Presence,
WandererAppWeb.Endpoint
]

View File

@@ -113,6 +113,63 @@ defmodule WandererApp.CachedInfo do
end
end
def get_solar_system_jumps() do
case WandererApp.Cache.lookup(:solar_system_jumps) do
{:ok, nil} ->
data = WandererApp.EveDataService.get_solar_system_jumps_data()
cache_items(data, :solar_system_jumps)
{:ok, data}
{:ok, data} ->
{:ok, data}
end
end
def get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
# Create normalized cache key (smaller ID first for bidirectional lookup)
{id1, id2} =
if from_solar_system_id < to_solar_system_id do
{from_solar_system_id, to_solar_system_id}
else
{to_solar_system_id, from_solar_system_id}
end
cache_key = "jump_#{id1}_#{id2}"
case WandererApp.Cache.lookup(cache_key) do
{:ok, nil} ->
# Build jump index if not exists
build_jump_index()
WandererApp.Cache.lookup(cache_key)
result ->
result
end
end
defp build_jump_index() do
case get_solar_system_jumps() do
{:ok, jumps} ->
jumps
|> Enum.each(fn jump ->
{id1, id2} =
if jump.from_solar_system_id < jump.to_solar_system_id do
{jump.from_solar_system_id, jump.to_solar_system_id}
else
{jump.to_solar_system_id, jump.from_solar_system_id}
end
cache_key = "jump_#{id1}_#{id2}"
WandererApp.Cache.put(cache_key, jump)
end)
_ ->
:error
end
end
def get_wormhole_types!() do
case get_wormhole_types() do
{:ok, wormhole_types} ->

View File

@@ -263,7 +263,7 @@ defmodule WandererApp.Character do
end
end
defp maybe_merge_map_character_settings(%{id: character_id} = character, map_id, true) do
defp maybe_merge_map_character_settings(%{id: character_id} = character, _map_id, true) do
{:ok, tracking_paused} =
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)

View File

@@ -49,11 +49,13 @@ defmodule WandererApp.Character.Activity do
"""
def process_character_activity(map_id, current_user) do
with {:ok, map_user_settings} <- get_map_user_settings(map_id, current_user.id),
raw_activity <- WandererApp.Map.get_character_activity(map_id),
{:ok, raw_activity} <- WandererApp.Map.get_character_activity(map_id),
{:ok, user_characters} <-
WandererApp.Api.Character.active_by_user(%{user_id: current_user.id}) do
result = process_activity_data(raw_activity, map_user_settings, user_characters)
result
process_activity_data(raw_activity, map_user_settings, user_characters)
else
_ ->
[]
end
end

View File

@@ -7,6 +7,7 @@ defmodule WandererApp.Character.Tracker do
defstruct [
:character_id,
:alliance_id,
:corporation_id,
:opts,
server_online: true,
start_time: nil,
@@ -21,6 +22,8 @@ defmodule WandererApp.Character.Tracker do
@type t :: %__MODULE__{
character_id: integer,
alliance_id: integer,
corporation_id: integer,
opts: map,
server_online: boolean,
start_time: DateTime.t(),
@@ -35,12 +38,13 @@ defmodule WandererApp.Character.Tracker do
@pause_tracking_timeout :timer.minutes(60 * 10)
@offline_timeout :timer.minutes(5)
@online_error_timeout :timer.minutes(2)
@ship_error_timeout :timer.minutes(2)
@location_error_timeout :timer.minutes(2)
@online_error_timeout :timer.minutes(10)
@ship_error_timeout :timer.minutes(10)
@location_error_timeout :timer.minutes(10)
@online_forbidden_ttl :timer.seconds(7)
@offline_check_delay_ttl :timer.seconds(15)
@online_limit_ttl :timer.seconds(7)
@forbidden_ttl :timer.seconds(5)
@forbidden_ttl :timer.seconds(10)
@limit_ttl :timer.seconds(5)
@location_limit_ttl :timer.seconds(1)
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
@@ -49,8 +53,15 @@ defmodule WandererApp.Character.Tracker do
def new(args), do: __struct__(args)
def init(args) do
character_id = args[:character_id]
{:ok, %{corporation_id: corporation_id, alliance_id: alliance_id}} =
WandererApp.Character.get_character(character_id)
%{
character_id: args[:character_id],
character_id: character_id,
corporation_id: corporation_id,
alliance_id: alliance_id,
start_time: DateTime.utc_now(),
opts: args
}
@@ -61,18 +72,19 @@ defmodule WandererApp.Character.Tracker do
WandererApp.Cache.lookup!("character:#{character_id}:last_online_time")
|> case do
nil ->
WandererApp.Cache.insert(
"character:#{character_id}:last_online_time",
DateTime.utc_now()
)
:ok
last_online_time ->
duration = DateTime.diff(DateTime.utc_now(), last_online_time, :millisecond)
if duration >= @offline_timeout do
pause_tracking(character_id)
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
WandererApp.Cache.delete("character:#{character_id}:last_online_time")
:ok
else
@@ -100,7 +112,8 @@ defmodule WandererApp.Character.Tracker do
duration = DateTime.diff(DateTime.utc_now(), error_time, :millisecond)
if duration >= timeout do
# pause_tracking(character_id)
pause_tracking(character_id)
WandererApp.Cache.delete("character:#{character_id}:#{type}_error_time")
:ok
else
@@ -113,15 +126,14 @@ defmodule WandererApp.Character.Tracker do
if WandererApp.Character.can_pause_tracking?(character_id) &&
not WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused") do
# Log character tracking statistics before pausing
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
Logger.debug(fn ->
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
Logger.warning(
"CHARACTER_TRACKING_PAUSED: Character tracking paused due to sustained errors",
character_id: character_id,
"CHARACTER_TRACKING_PAUSED: Character tracking paused due to sustained errors: #{inspect(character_id: character_id,
active_maps: length(character_state.active_maps),
is_online: character_state.is_online,
tracking_duration_minutes: get_tracking_duration_minutes(character_id)
)
tracking_duration_minutes: get_tracking_duration_minutes(character_id))}"
end)
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
@@ -176,7 +188,9 @@ defmodule WandererApp.Character.Tracker do
|> WandererApp.Character.get_character_state!()
|> update_online()
def update_online(%{track_online: true, character_id: character_id} = character_state) do
def update_online(
%{track_online: true, character_id: character_id, is_online: is_online} = character_state
) do
case WandererApp.Character.get_character(character_id) do
{:ok, %{eve_id: eve_id, access_token: access_token, tracking_pool: tracking_pool}}
when not is_nil(access_token) ->
@@ -187,13 +201,11 @@ defmodule WandererApp.Character.Tracker do
{:error, :skipped}
_ ->
# Monitor cache for potential evictions before ESI call
case WandererApp.Esi.get_character_online(eve_id,
access_token: access_token,
character_id: character_id
) do
{:ok, online} ->
{:ok, online} when is_map(online) ->
online = get_online(online)
if online.online == true do
@@ -201,70 +213,67 @@ defmodule WandererApp.Character.Tracker do
"character:#{character_id}:last_online_time",
DateTime.utc_now()
)
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
else
# Delay next online updates for offline characters
WandererApp.Cache.put(
"character:#{character_id}:online_forbidden",
true,
ttl: @offline_check_delay_ttl
)
end
if online.online == true && online.online != is_online do
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
WandererApp.Cache.delete("character:#{character_id}:info_forbidden")
WandererApp.Cache.delete("character:#{character_id}:ship_forbidden")
WandererApp.Cache.delete("character:#{character_id}:location_forbidden")
WandererApp.Cache.delete("character:#{character_id}:wallet_forbidden")
WandererApp.Cache.delete("character:#{character_id}:corporation_info_forbidden")
end
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
WandererApp.Cache.delete("character:#{character_id}:info_forbidden")
WandererApp.Cache.delete("character:#{character_id}:ship_forbidden")
WandererApp.Cache.delete("character:#{character_id}:location_forbidden")
WandererApp.Cache.delete("character:#{character_id}:wallet_forbidden")
try do
WandererApp.Character.update_character(character_id, online)
rescue
error ->
Logger.error("DB_ERROR: Failed to update character in database",
character_id: character_id,
error: inspect(error),
operation: "update_character_online"
)
if online.online != is_online do
try do
WandererApp.Character.update_character(character_id, online)
rescue
error ->
Logger.error("DB_ERROR: Failed to update character in database",
character_id: character_id,
error: inspect(error),
operation: "update_character_online"
)
# Re-raise to maintain existing error handling
reraise error, __STACKTRACE__
end
# Re-raise to maintain existing error handling
reraise error, __STACKTRACE__
end
update = %{
character_state
| is_online: online.online,
track_ship: online.online,
track_location: online.online
}
try do
WandererApp.Character.update_character_state(character_id, %{
character_state
| is_online: online.online,
track_ship: online.online,
track_location: online.online
})
rescue
error ->
Logger.error("DB_ERROR: Failed to update character state in database",
character_id: character_id,
error: inspect(error),
operation: "update_character_state"
)
try do
WandererApp.Character.update_character_state(character_id, update)
rescue
error ->
Logger.error("DB_ERROR: Failed to update character state in database",
character_id: character_id,
error: inspect(error),
operation: "update_character_state"
)
# Re-raise to maintain existing error handling
reraise error, __STACKTRACE__
# Re-raise to maintain existing error handling
reraise error, __STACKTRACE__
end
end
:ok
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_online",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.warning("ESI_ERROR: Character online tracking failed",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
endpoint: "character_online"
)
WandererApp.Cache.put(
"character:#{character_id}:online_forbidden",
true,
@@ -291,28 +300,6 @@ defmodule WandererApp.Character.Tracker do
remaining =
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
# Emit telemetry for tracking
:telemetry.execute(
[:wanderer_app, :esi, :rate_limited],
%{
reset_duration: reset_timeout,
count: 1
},
%{
endpoint: "character_online",
tracking_pool: tracking_pool,
character_id: character_id
}
)
Logger.warning("ESI_RATE_LIMITED: Character online tracking rate limited",
character_id: character_id,
tracking_pool: tracking_pool,
endpoint: "character_online",
reset_seconds: reset_seconds,
remaining_requests: remaining
)
WandererApp.Cache.put(
"character:#{character_id}:online_forbidden",
true,
@@ -322,15 +309,7 @@ defmodule WandererApp.Character.Tracker do
{:error, :skipped}
{:error, error} ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_online",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character online tracking failed",
Logger.error("ESI_ERROR: Character online tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -388,31 +367,25 @@ defmodule WandererApp.Character.Tracker do
{:ok, %{eve_id: eve_id, tracking_pool: tracking_pool}} =
WandererApp.Character.get_character(character_id)
case WandererApp.Esi.get_character_info(eve_id) do
{:ok, _info} ->
character_eve_id = eve_id |> String.to_integer()
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
{:ok, [character_aff_info]} when not is_nil(character_aff_info) ->
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
update = maybe_update_corporation(character_state, eve_id |> String.to_integer())
WandererApp.Character.update_character_state(character_id, update)
alliance_id = character_aff_info |> Map.get("alliance_id")
corporation_id = character_aff_info |> Map.get("corporation_id")
updated_state =
character_state
|> maybe_update_corporation(corporation_id)
|> maybe_update_alliance(alliance_id)
WandererApp.Character.update_character_state(character_id, updated_state)
:ok
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_info",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.warning("ESI_ERROR: Character info tracking failed",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
endpoint: "character_info"
)
WandererApp.Cache.put(
"character:#{character_id}:info_forbidden",
true,
@@ -424,33 +397,6 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
reset_seconds =
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
remaining = Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
# Emit telemetry for tracking
:telemetry.execute(
[:wanderer_app, :esi, :rate_limited],
%{
reset_duration: reset_timeout,
count: 1
},
%{
endpoint: "character_info",
tracking_pool: tracking_pool,
character_id: character_id
}
)
Logger.warning("ESI_RATE_LIMITED: Character info tracking rate limited",
character_id: character_id,
tracking_pool: tracking_pool,
endpoint: "character_info",
reset_seconds: reset_seconds,
remaining_requests: remaining
)
WandererApp.Cache.put(
"character:#{character_id}:info_forbidden",
true,
@@ -460,21 +406,13 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited}
{:error, error} ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_info",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
WandererApp.Cache.put(
"character:#{character_id}:info_forbidden",
true,
ttl: @forbidden_ttl
)
Logger.error("ESI_ERROR: Character info tracking failed",
Logger.error("ESI_ERROR: Character info tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -521,21 +459,6 @@ defmodule WandererApp.Character.Tracker do
:ok
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_ship",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.warning("ESI_ERROR: Character ship tracking failed",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
endpoint: "character_ship"
)
WandererApp.Cache.put(
"character:#{character_id}:ship_forbidden",
true,
@@ -554,34 +477,6 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
reset_seconds =
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
remaining =
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
# Emit telemetry for tracking
:telemetry.execute(
[:wanderer_app, :esi, :rate_limited],
%{
reset_duration: reset_timeout,
count: 1
},
%{
endpoint: "character_ship",
tracking_pool: tracking_pool,
character_id: character_id
}
)
Logger.warning("ESI_RATE_LIMITED: Character ship tracking rate limited",
character_id: character_id,
tracking_pool: tracking_pool,
endpoint: "character_ship",
reset_seconds: reset_seconds,
remaining_requests: remaining
)
WandererApp.Cache.put(
"character:#{character_id}:ship_forbidden",
true,
@@ -591,15 +486,7 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited}
{:error, error} ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_ship",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character ship tracking failed",
Logger.error("ESI_ERROR: Character ship tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -622,14 +509,6 @@ defmodule WandererApp.Character.Tracker do
{:error, error}
_ ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_ship",
error_type: "wrong_response",
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character ship tracking failed - wrong response",
character_id: character_id,
tracking_pool: tracking_pool,
@@ -692,14 +571,6 @@ defmodule WandererApp.Character.Tracker do
:ok
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_location",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.warning("ESI_ERROR: Character location tracking failed",
character_id: character_id,
tracking_pool: tracking_pool,
@@ -721,34 +592,6 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers, @location_limit_ttl)
reset_seconds =
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
remaining =
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
# Emit telemetry for tracking
:telemetry.execute(
[:wanderer_app, :esi, :rate_limited],
%{
reset_duration: reset_timeout,
count: 1
},
%{
endpoint: "character_location",
tracking_pool: tracking_pool,
character_id: character_id
}
)
Logger.warning("ESI_RATE_LIMITED: Character location tracking rate limited",
character_id: character_id,
tracking_pool: tracking_pool,
endpoint: "character_location",
reset_seconds: reset_seconds,
remaining_requests: remaining
)
WandererApp.Cache.put(
"character:#{character_id}:location_forbidden",
true,
@@ -758,15 +601,7 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited}
{:error, error} ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_location",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character location tracking failed",
Logger.error("ESI_ERROR: Character location tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -785,14 +620,6 @@ defmodule WandererApp.Character.Tracker do
{:error, :skipped}
_ ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_location",
error_type: "wrong_response",
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character location tracking failed - wrong response",
character_id: character_id,
tracking_pool: tracking_pool,
@@ -854,14 +681,6 @@ defmodule WandererApp.Character.Tracker do
:ok
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_wallet",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.warning("ESI_ERROR: Character wallet tracking failed",
character_id: character_id,
tracking_pool: tracking_pool,
@@ -880,34 +699,6 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
reset_seconds =
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
remaining =
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
# Emit telemetry for tracking
:telemetry.execute(
[:wanderer_app, :esi, :rate_limited],
%{
reset_duration: reset_timeout,
count: 1
},
%{
endpoint: "character_wallet",
tracking_pool: tracking_pool,
character_id: character_id
}
)
Logger.warning("ESI_RATE_LIMITED: Character wallet tracking rate limited",
character_id: character_id,
tracking_pool: tracking_pool,
endpoint: "character_wallet",
reset_seconds: reset_seconds,
remaining_requests: remaining
)
WandererApp.Cache.put(
"character:#{character_id}:wallet_forbidden",
true,
@@ -917,15 +708,7 @@ defmodule WandererApp.Character.Tracker do
{:error, :skipped}
{:error, error} ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_wallet",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character wallet tracking failed",
Logger.error("ESI_ERROR: Character wallet tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -941,15 +724,7 @@ defmodule WandererApp.Character.Tracker do
{:error, :skipped}
error ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_wallet",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character wallet tracking failed",
Logger.error("ESI_ERROR: Character wallet tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -975,7 +750,38 @@ defmodule WandererApp.Character.Tracker do
end
end
defp update_alliance(%{character_id: character_id} = state, alliance_id) do
defp maybe_update_alliance(
%{character_id: character_id, alliance_id: old_alliance_id} = state,
alliance_id
)
when old_alliance_id != alliance_id and is_nil(alliance_id) do
{:ok, character} = WandererApp.Character.get_character(character_id)
character_update = %{
alliance_id: nil,
alliance_name: nil,
alliance_ticker: nil
}
{:ok, _character} =
Character.update_alliance(character, character_update)
WandererApp.Character.update_character(character_id, character_update)
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character_id}:alliance",
{:character_alliance, {character_id, character_update}}
)
state
end
defp maybe_update_alliance(
%{character_id: character_id, alliance_id: old_alliance_id} = state,
alliance_id
)
when old_alliance_id != alliance_id do
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
@@ -1015,8 +821,15 @@ defmodule WandererApp.Character.Tracker do
end
end
defp update_corporation(%{character_id: character_id} = state, corporation_id) do
defp maybe_update_alliance(state, _alliance_id), do: state
defp maybe_update_corporation(
%{character_id: character_id, corporation_id: old_corporation_id} = state,
corporation_id
)
when old_corporation_id != corporation_id do
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:corporation_info_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
true ->
@@ -1027,16 +840,13 @@ defmodule WandererApp.Character.Tracker do
|> WandererApp.Esi.get_corporation_info()
|> case do
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
alliance_id = Map.get(corporation_info, "alliance_id")
{:ok, character} =
WandererApp.Character.get_character(character_id)
character_update = %{
corporation_id: corporation_id,
corporation_name: corporation_name,
corporation_ticker: corporation_ticker,
alliance_id: alliance_id
corporation_ticker: corporation_ticker
}
{:ok, _character} =
@@ -1057,8 +867,18 @@ defmodule WandererApp.Character.Tracker do
)
state
|> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id})
|> maybe_update_alliance()
|> Map.merge(%{corporation_id: corporation_id})
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
WandererApp.Cache.put(
"character:#{character_id}:corporation_info_forbidden",
true,
ttl: reset_timeout
)
state
error ->
Logger.warning(
@@ -1072,6 +892,8 @@ defmodule WandererApp.Character.Tracker do
end
end
defp maybe_update_corporation(state, _corporation_id), do: state
defp maybe_update_ship(
%{
character_id: character_id
@@ -1153,58 +975,6 @@ defmodule WandererApp.Character.Tracker do
structure_id != new_structure_id ||
station_id != new_station_id
defp maybe_update_corporation(
state,
character_eve_id
)
when not is_nil(character_eve_id) and is_integer(character_eve_id) do
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
{:ok, [character_aff_info]} when not is_nil(character_aff_info) ->
update_corporation(state, character_aff_info |> Map.get("corporation_id"))
_error ->
state
end
end
defp maybe_update_corporation(
state,
_info
),
do: state
defp maybe_update_alliance(
%{character_id: character_id, alliance_id: alliance_id} =
state
) do
case alliance_id do
nil ->
{:ok, character} = WandererApp.Character.get_character(character_id)
character_update = %{
alliance_id: nil,
alliance_name: nil,
alliance_ticker: nil
}
{:ok, _character} =
Character.update_alliance(character, character_update)
WandererApp.Character.update_character(character_id, character_update)
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character_id}:alliance",
{:character_alliance, {character_id, character_update}}
)
state
_ ->
update_alliance(state, alliance_id)
end
end
defp maybe_update_wallet(
%{character_id: character_id} =
state,

View File

@@ -12,10 +12,10 @@ defmodule WandererApp.Character.TrackerManager.Impl do
opts: map
}
@garbage_collection_interval :timer.minutes(15)
@untrack_characters_interval :timer.minutes(1)
@inactive_character_timeout :timer.minutes(10)
@untrack_character_timeout :timer.minutes(10)
@check_start_queue_interval :timer.seconds(1)
@garbage_collection_interval :timer.minutes(5)
@untrack_characters_interval :timer.minutes(5)
@inactive_character_timeout :timer.minutes(5)
@logger Application.compile_env(:wanderer_app, :logger)
@@ -23,6 +23,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
def new(args), do: __struct__(args)
def init(args) do
Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
@@ -46,25 +47,21 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
def start_tracking(state, character_id, opts) do
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
false <- Enum.member?(characters, character_id) do
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
if not WandererApp.Cache.has_key?("#{character_id}:track_requested") do
WandererApp.Cache.insert(
"#{character_id}:track_requested",
true
)
tracked_characters = [character_id | characters] |> Enum.uniq()
WandererApp.Cache.insert("tracked_characters", tracked_characters)
Logger.debug(fn -> "Add character to track_characters_queue: #{inspect(character_id)}" end)
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
character_id,
%{opts: opts}
])
WandererApp.Cache.insert_or_update(
"track_characters_queue",
[character_id],
fn existing ->
[character_id | existing] |> Enum.uniq()
end
)
end
state
@@ -73,29 +70,25 @@ defmodule WandererApp.Character.TrackerManager.Impl do
def stop_tracking(state, character_id) do
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
true <- Enum.member?(characters, character_id),
{:ok, %{start_time: start_time}} <-
WandererApp.Character.get_character_state(character_id, false) do
false <- WandererApp.Cache.has_key?("#{character_id}:track_requested") do
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
WandererApp.Cache.delete("character:#{character_id}:last_active_time")
WandererApp.Character.delete_character_state(character_id)
tracked_characters =
characters |> Enum.reject(fn c_id -> c_id == character_id end)
WandererApp.Cache.insert("tracked_characters", tracked_characters)
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{
duration: duration
})
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
end
WandererApp.Cache.insert_or_update(
"tracked_characters",
[],
fn tracked_characters ->
tracked_characters
|> Enum.reject(fn c_id -> c_id == character_id end)
end
)
state
end
@@ -122,25 +115,17 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
def add_to_untrack_queue(map_id, character_id) do
if not WandererApp.Cache.has_key?("#{map_id}:#{character_id}:untrack_requested") do
WandererApp.Cache.insert(
"#{map_id}:#{character_id}:untrack_requested",
DateTime.utc_now()
)
end
WandererApp.Cache.insert_or_update(
"character_untrack_queue",
[{map_id, character_id}],
fn untrack_queue ->
[{map_id, character_id} | untrack_queue] |> Enum.uniq()
[{map_id, character_id} | untrack_queue]
|> Enum.uniq_by(fn {map_id, character_id} -> map_id <> character_id end)
end
)
end
def remove_from_untrack_queue(map_id, character_id) do
WandererApp.Cache.delete("#{map_id}:#{character_id}:untrack_requested")
WandererApp.Cache.insert_or_update(
"character_untrack_queue",
[],
@@ -178,6 +163,21 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
end
def handle_info(
:check_start_queue,
state
) do
Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
{:ok, track_characters_queue} = WandererApp.Cache.lookup("track_characters_queue", [])
track_characters_queue
|> Enum.each(fn character_id ->
track_character(character_id, %{})
end)
state
end
def handle_info(
:garbage_collect,
state
@@ -203,7 +203,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
end
end,
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.seconds(60)
)
@@ -229,52 +229,34 @@ defmodule WandererApp.Character.TrackerManager.Impl do
WandererApp.Cache.lookup!("character_untrack_queue", [])
|> Task.async_stream(
fn {map_id, character_id} ->
untrack_timeout_reached =
if WandererApp.Cache.has_key?("#{map_id}:#{character_id}:untrack_requested") do
untrack_requested =
WandererApp.Cache.lookup!(
"#{map_id}:#{character_id}:untrack_requested",
DateTime.utc_now()
)
remove_from_untrack_queue(map_id, character_id)
duration = DateTime.diff(DateTime.utc_now(), untrack_requested, :millisecond)
duration >= @untrack_character_timeout
else
false
end
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
Logger.debug(fn -> "Untrack timeout reached: #{inspect(untrack_timeout_reached)}" end)
{:ok, character_state} =
WandererApp.Character.Tracker.update_settings(character_id, %{
map_id: map_id,
track: false
})
if untrack_timeout_reached do
remove_from_untrack_queue(map_id, character_id)
{:ok, character} = WandererApp.Character.get_character(character_id)
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
{:ok, _updated} =
WandererApp.MapCharacterSettingsRepo.update(map_id, character_id, %{
ship: character.ship,
ship_name: character.ship_name,
ship_item_id: character.ship_item_id,
solar_system_id: character.solar_system_id,
structure_id: character.structure_id,
station_id: character.station_id
})
{:ok, character_state} =
WandererApp.Character.Tracker.update_settings(character_id, %{
map_id: map_id,
track: false
})
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, _updated} =
WandererApp.MapCharacterSettingsRepo.update(map_id, character_id, %{
ship: character.ship,
ship_name: character.ship_name,
ship_item_id: character.ship_item_id,
solar_system_id: character.solar_system_id,
structure_id: character.structure_id,
station_id: character.station_id
})
WandererApp.Character.update_character_state(character_id, character_state)
WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
end
WandererApp.Character.update_character_state(character_id, character_state)
WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
end,
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.seconds(30)
)
@@ -294,8 +276,56 @@ defmodule WandererApp.Character.TrackerManager.Impl do
state
end
def handle_info(_event, state),
do: state
def track_character(character_id, opts) do
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
false <- Enum.member?(characters, character_id) do
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
WandererApp.Cache.insert_or_update(
"tracked_characters",
[character_id],
fn existing ->
[character_id | existing] |> Enum.uniq()
end
)
WandererApp.Cache.insert_or_update(
"track_characters_queue",
[],
fn existing ->
existing
|> Enum.reject(fn c_id -> c_id == character_id end)
end
)
WandererApp.Cache.delete("#{character_id}:track_requested")
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
character_id,
%{opts: opts}
])
else
_ ->
WandererApp.Cache.insert_or_update(
"track_characters_queue",
[],
fn existing ->
existing
|> Enum.reject(fn c_id -> c_id == character_id end)
end
)
WandererApp.Cache.delete("#{character_id}:track_requested")
end
end
def character_is_present(map_id, character_id) do
{:ok, presence_character_ids} =

View File

@@ -18,13 +18,13 @@ defmodule WandererApp.Character.TrackerPool do
@update_location_interval :timer.seconds(1)
@update_online_interval :timer.seconds(5)
@check_offline_characters_interval :timer.minutes(2)
@check_offline_characters_interval :timer.minutes(5)
@check_online_errors_interval :timer.minutes(1)
@check_ship_errors_interval :timer.minutes(1)
@check_location_errors_interval :timer.minutes(1)
@update_ship_interval :timer.seconds(2)
@update_info_interval :timer.minutes(1)
@update_wallet_interval :timer.minutes(1)
@update_info_interval :timer.minutes(2)
@update_wallet_interval :timer.minutes(10)
@logger Application.compile_env(:wanderer_app, :logger)
@@ -124,7 +124,7 @@ defmodule WandererApp.Character.TrackerPool do
Process.send_after(self(), :check_online_errors, :timer.seconds(60))
Process.send_after(self(), :check_ship_errors, :timer.seconds(90))
Process.send_after(self(), :check_location_errors, :timer.seconds(120))
# Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
Process.send_after(self(), :update_location, 300)
Process.send_after(self(), :update_ship, 500)
Process.send_after(self(), :update_info, 1500)
@@ -176,11 +176,15 @@ defmodule WandererApp.Character.TrackerPool do
try do
characters
|> Enum.each(fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_online, [
character_id
])
end)
|> Task.async_stream(
fn character_id ->
WandererApp.Character.Tracker.update_online(character_id)
end,
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.seconds(5)
)
|> Enum.each(fn _result -> :ok end)
rescue
e ->
Logger.error("""
@@ -234,25 +238,15 @@ defmodule WandererApp.Character.TrackerPool do
characters
|> Task.async_stream(
fn character_id ->
if WandererApp.Character.can_pause_tracking?(character_id) do
WandererApp.TaskWrapper.start_link(
WandererApp.Character.Tracker,
:check_offline,
[
character_id
]
)
else
:ok
end
WandererApp.Character.Tracker.check_offline(character_id)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in check_offline: #{inspect(reason)}")
error -> @logger.error("Error in check_offline: #{inspect(error)}")
end)
rescue
e ->
@@ -287,12 +281,12 @@ defmodule WandererApp.Character.TrackerPool do
)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in check_online_errors: #{inspect(reason)}")
error -> @logger.error("Error in check_online_errors: #{inspect(error)}")
end)
rescue
e ->
@@ -327,12 +321,12 @@ defmodule WandererApp.Character.TrackerPool do
)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in check_ship_errors: #{inspect(reason)}")
error -> @logger.error("Error in check_ship_errors: #{inspect(error)}")
end)
rescue
e ->
@@ -367,12 +361,12 @@ defmodule WandererApp.Character.TrackerPool do
)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in check_location_errors: #{inspect(reason)}")
error -> @logger.error("Error in check_location_errors: #{inspect(error)}")
end)
rescue
e ->
@@ -397,11 +391,15 @@ defmodule WandererApp.Character.TrackerPool do
try do
characters
|> Enum.each(fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_location, [
character_id
])
end)
|> Task.async_stream(
fn character_id ->
WandererApp.Character.Tracker.update_location(character_id)
end,
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.seconds(5)
)
|> Enum.each(fn _result -> :ok end)
rescue
e ->
Logger.error("""
@@ -434,11 +432,15 @@ defmodule WandererApp.Character.TrackerPool do
try do
characters
|> Enum.each(fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_ship, [
character_id
])
end)
|> Task.async_stream(
fn character_id ->
WandererApp.Character.Tracker.update_ship(character_id)
end,
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.seconds(5)
)
|> Enum.each(fn _result -> :ok end)
rescue
e ->
Logger.error("""
@@ -473,17 +475,15 @@ defmodule WandererApp.Character.TrackerPool do
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_info, [
character_id
])
WandererApp.Character.Tracker.update_info(character_id)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> Logger.error("Error in update_info: #{inspect(reason)}")
error -> Logger.error("Error in update_info: #{inspect(error)}")
end)
rescue
e ->
@@ -519,17 +519,15 @@ defmodule WandererApp.Character.TrackerPool do
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_wallet, [
character_id
])
WandererApp.Character.Tracker.update_wallet(character_id)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
timeout: :timer.minutes(5),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> Logger.error("Error in update_wallet: #{inspect(reason)}")
error -> Logger.error("Error in update_wallet: #{inspect(error)}")
end)
rescue
e ->

View File

@@ -20,7 +20,7 @@ defmodule WandererApp.Character.TrackingUtils do
)
when not is_nil(caller_pid) do
with {:ok, character} <-
WandererApp.Character.get_by_eve_id(character_eve_id),
WandererApp.Character.get_by_eve_id("#{character_eve_id}"),
{:ok, %{tracked: is_tracked}} <-
do_update_character_tracking(character, map_id, track, caller_pid) do
# Determine which event to send based on tracking mode and previous state
@@ -55,15 +55,19 @@ defmodule WandererApp.Character.TrackingUtils do
Builds tracking data for all characters with access to a map.
"""
def build_tracking_data(map_id, current_user_id) do
with {:ok, map} <- WandererApp.MapRepo.get(map_id, [:acls]),
{:ok, character_settings} <-
WandererApp.Character.Activity.get_map_character_settings(map_id),
with {:ok, map} <-
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
),
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
{:ok, %{characters: characters_with_access}} <-
WandererApp.Maps.load_characters(map, character_settings, current_user_id) do
WandererApp.Maps.load_characters(map, current_user_id) do
# Map characters to tracking data
{:ok, characters_data} =
build_character_tracking_data(characters_with_access, character_settings)
build_character_tracking_data(characters_with_access)
{:ok, main_character} =
get_main_character(user_settings, characters_with_access, characters_with_access)
@@ -98,21 +102,19 @@ defmodule WandererApp.Character.TrackingUtils do
end
# Helper to build tracking data for each character
defp build_character_tracking_data(characters, character_settings) do
defp build_character_tracking_data(characters) do
{:ok,
Enum.map(characters, fn char ->
setting = Enum.find(character_settings, &(&1.character_id == char.id))
%{
character: char |> WandererAppWeb.MapEventHandler.map_ui_character_stat(),
tracked: (setting && setting.tracked) || false
tracked: char.tracked
}
end)}
end
# Private implementation of update character tracking
defp do_update_character_tracking(character, map_id, track, caller_pid) do
WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character.id)
WandererApp.MapCharacterSettingsRepo.get(map_id, character.id)
|> case do
# Untracking flow
{:ok, %{tracked: true} = existing_settings} ->

View File

@@ -11,49 +11,56 @@ defmodule WandererApp.Env do
def vsn(), do: Application.spec(@app)[:vsn]
def git_sha(), do: get_key(:git_sha, "<GIT_SHA>")
def base_url, do: get_key(:web_app_url, "<BASE_URL>")
def custom_route_base_url, do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
def invites, do: get_key(:invites, false)
def base_url(), do: get_key(:web_app_url, "<BASE_URL>")
def base_metrics_only(), do: get_key(:base_metrics_only, false)
def custom_route_base_url(), do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
def invites(), do: get_key(:invites, false)
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
def websocket_events_enabled?, do: get_key(:websocket_events_enabled, false)
def public_api_disabled?, do: get_key(:public_api_disabled, false)
def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)
def websocket_events_enabled?(), do: get_key(:websocket_events_enabled, false)
def public_api_disabled?(), do: get_key(:public_api_disabled, false)
@decorate cacheable(
cache: WandererApp.Cache,
key: "active_tracking_pool"
)
def active_tracking_pool, do: get_key(:active_tracking_pool, "default")
def active_tracking_pool(), do: get_key(:active_tracking_pool, "default")
@decorate cacheable(
cache: WandererApp.Cache,
key: "tracking_pool_max_size"
)
def tracking_pool_max_size, do: get_key(:tracking_pool_max_size, 300)
def character_tracking_pause_disabled?, do: get_key(:character_tracking_pause_disabled, true)
def character_api_disabled?, do: get_key(:character_api_disabled, false)
def wanderer_kills_service_enabled?, do: get_key(:wanderer_kills_service_enabled, false)
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
def admins, do: get_key(:admins, [])
def admin_username, do: get_key(:admin_username)
def admin_password, do: get_key(:admin_password)
def corp_wallet, do: get_key(:corp_wallet, "")
def corp_wallet_eve_id, do: get_key(:corp_wallet_eve_id, "-1")
def corp_eve_id, do: get_key(:corp_id, -1)
def subscription_settings, do: get_key(:subscription_settings)
def tracking_pool_max_size(), do: get_key(:tracking_pool_max_size, 300)
def character_tracking_pause_disabled?(), do: get_key(:character_tracking_pause_disabled, true)
def character_api_disabled?(), do: get_key(:character_api_disabled, false)
def wanderer_kills_service_enabled?(), do: get_key(:wanderer_kills_service_enabled, false)
def wallet_tracking_enabled?(), do: get_key(:wallet_tracking_enabled, false)
def admins(), do: get_key(:admins, [])
def admin_username(), do: get_key(:admin_username)
def admin_password(), do: get_key(:admin_password)
def corp_wallet(), do: get_key(:corp_wallet, "")
def corp_wallet_eve_id(), do: get_key(:corp_wallet_eve_id, "-1")
def corp_eve_id(), do: get_key(:corp_id, -1)
def subscription_settings(), do: get_key(:subscription_settings)
@decorate cacheable(
cache: WandererApp.Cache,
key: "restrict_maps_creation"
)
def restrict_maps_creation?, do: get_key(:restrict_maps_creation, false)
def restrict_maps_creation?(), do: get_key(:restrict_maps_creation, false)
def sse_enabled? do
@decorate cacheable(
cache: WandererApp.Cache,
key: "restrict_acls_creation"
)
def restrict_acls_creation?(), do: get_key(:restrict_acls_creation, false)
def sse_enabled?() do
Application.get_env(@app, :sse, [])
|> Keyword.get(:enabled, false)
end
def webhooks_enabled? do
def webhooks_enabled?() do
Application.get_env(@app, :external_events, [])
|> Keyword.get(:webhooks_enabled, false)
end
@@ -62,19 +69,19 @@ defmodule WandererApp.Env do
cache: WandererApp.Cache,
key: "map-connection-auto-expire-hours"
)
def map_connection_auto_expire_hours, do: get_key(:map_connection_auto_expire_hours)
def map_connection_auto_expire_hours(), do: get_key(:map_connection_auto_expire_hours)
@decorate cacheable(
cache: WandererApp.Cache,
key: "map-connection-auto-eol-hours"
)
def map_connection_auto_eol_hours, do: get_key(:map_connection_auto_eol_hours)
def map_connection_auto_eol_hours(), do: get_key(:map_connection_auto_eol_hours)
@decorate cacheable(
cache: WandererApp.Cache,
key: "map-connection-eol-expire-timeout-mins"
)
def map_connection_eol_expire_timeout_mins,
def map_connection_eol_expire_timeout_mins(),
do: get_key(:map_connection_eol_expire_timeout_mins)
def get_key(key, default \\ nil), do: Application.get_env(@app, key, default)
@@ -83,7 +90,7 @@ defmodule WandererApp.Env do
A single map containing environment variables
made available to react
"""
def to_client_env do
def to_client_env() do
%{detailedKillsDisabled: not wanderer_kills_service_enabled?()}
end
end

View File

@@ -253,7 +253,7 @@ defmodule WandererApp.Esi.ApiClient do
fn destination ->
get_routes(origin, destination, params, opts)
end,
max_concurrency: 20,
max_concurrency: System.schedulers_online() * 4,
timeout: :timer.seconds(30),
on_timeout: :kill_task
)
@@ -287,8 +287,8 @@ defmodule WandererApp.Esi.ApiClient do
opts: [ttl: @ttl]
)
def get_alliance_info(eve_id, opts \\ []) do
case _get_alliance_info(eve_id, "", opts) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
case get_alliance_info(eve_id, "", opts) do
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
error -> error
end
@@ -309,8 +309,8 @@ defmodule WandererApp.Esi.ApiClient do
opts: [ttl: @ttl]
)
def get_corporation_info(eve_id, opts \\ []) do
case _get_corporation_info(eve_id, "", opts) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
case get_corporation_info(eve_id, "", opts) do
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
error -> error
end
@@ -327,7 +327,7 @@ defmodule WandererApp.Esi.ApiClient do
opts,
@cache_opts
) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
error -> error
end
@@ -434,7 +434,7 @@ defmodule WandererApp.Esi.ApiClient do
defp get_auth_opts(opts), do: [auth: {:bearer, opts[:access_token]}]
defp _get_alliance_info(alliance_eve_id, info_path, opts),
defp get_alliance_info(alliance_eve_id, info_path, opts),
do:
get(
"/alliances/#{alliance_eve_id}/#{info_path}",
@@ -442,7 +442,7 @@ defmodule WandererApp.Esi.ApiClient do
@cache_opts
)
defp _get_corporation_info(corporation_eve_id, info_path, opts),
defp get_corporation_info(corporation_eve_id, info_path, opts),
do:
get(
"/corporations/#{corporation_eve_id}/#{info_path}",

View File

@@ -8,6 +8,19 @@ defmodule WandererApp.Map.Manager do
require Logger
alias WandererApp.Map.Server
alias WandererApp.Map.ServerSupervisor
alias WandererApp.Api.MapSystemSignature
@maps_start_per_second 10
@maps_start_interval 1000
@maps_queue :maps_queue
@garbage_collection_interval :timer.hours(1)
@check_maps_queue_interval :timer.seconds(1)
@signatures_cleanup_interval :timer.minutes(30)
@delete_after_minutes 30
@pings_cleanup_interval :timer.minutes(10)
@pings_expire_minutes 60
# Test-aware async task runner
defp safe_async_task(fun) do
@@ -25,20 +38,6 @@ defmodule WandererApp.Map.Manager do
end
end
alias WandererApp.Map.ServerSupervisor
alias WandererApp.Api.MapSystemSignature
@maps_start_per_second 5
@maps_start_interval 1000
@maps_queue :maps_queue
@garbage_collection_interval :timer.hours(1)
@check_maps_queue_interval :timer.seconds(1)
@signatures_cleanup_interval :timer.minutes(30)
@delete_after_minutes 30
@pings_cleanup_interval :timer.minutes(10)
@pings_expire_minutes 60
def start_map(map_id) when is_binary(map_id),
do: WandererApp.Queue.push_uniq(@maps_queue, map_id)
@@ -247,22 +246,29 @@ defmodule WandererApp.Map.Manager do
Logger.debug(fn -> "All maps started" end)
else
# In production, run async as normal
tasks =
for chunk <- chunks do
task =
Task.async(fn ->
chunk
|> Enum.map(&start_map_server/1)
end)
chunks
|> Task.async_stream(
fn chunk ->
chunk
|> Enum.map(&start_map_server/1)
:timer.sleep(@maps_start_interval)
end,
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.seconds(60)
)
|> Enum.each(fn result ->
case result do
{:ok, _} ->
:ok
task
_ ->
:ok
end
end)
Logger.debug(fn -> "Waiting for maps to start" end)
Task.await_many(tasks)
Logger.debug(fn -> "All maps started" end)
Logger.info(fn -> "All maps started" end)
end
end

View File

@@ -88,11 +88,18 @@ defmodule WandererApp.Map.Server do
|> map_pid!
|> GenServer.cast({&Impl.remove_character/2, [character_id]})
def untrack_characters(map_id, character_ids) when is_binary(map_id),
do:
map_id
|> map_pid!
|> GenServer.cast({&Impl.untrack_characters/2, [character_ids]})
def untrack_characters(map_id, character_ids) when is_binary(map_id) do
map_id
|> map_pid()
|> case do
pid when is_pid(pid) ->
GenServer.cast(pid, {&Impl.untrack_characters/2, [character_ids]})
_ ->
WandererApp.Cache.insert("map_#{map_id}:started", false)
:ok
end
end
def add_system(map_id, system_info, user_id, character_id) when is_binary(map_id),
do:

View File

@@ -49,7 +49,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
@logger.error(Exception.message(e))
end
end,
max_concurrency: 10,
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn _ -> :ok end)

View File

@@ -19,6 +19,7 @@ defmodule WandererApp.Map.Operations.Connections do
@medium_ship_size 1
@large_ship_size 2
@xlarge_ship_size 3
@capital_ship_size 4
# System class constants
@c1_system_class 1
@@ -35,6 +36,12 @@ defmodule WandererApp.Map.Operations.Connections do
do_create(attrs, map_id, char_id)
end
def small_ship_size(), do: @small_ship_size
def medium_ship_size(), do: @medium_ship_size
def large_ship_size(), do: @large_ship_size
def freight_ship_size(), do: @xlarge_ship_size
def capital_ship_size(), do: @capital_ship_size
defp do_create(attrs, map_id, char_id) do
with {:ok, source} <- parse_int(attrs["solar_system_source"], "solar_system_source"),
{:ok, target} <- parse_int(attrs["solar_system_target"], "solar_system_target"),

View File

@@ -16,7 +16,13 @@ defmodule WandererApp.Map.Operations.Signatures do
systems
|> Enum.flat_map(fn sys ->
with {:ok, sigs} <- MapSystemSignature.by_system_id(sys.id) do
sigs
# Add solar_system_id to each signature and remove system_id
Enum.map(sigs, fn sig ->
sig
|> Map.from_struct()
|> Map.put(:solar_system_id, sys.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
end)
else
err ->
Logger.error("[list_signatures] error: #{inspect(err)}")
@@ -32,28 +38,70 @@ defmodule WandererApp.Map.Operations.Signatures do
def create_signature(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
%{"solar_system_id" => _solar_system_id} = params
) do
attrs = Map.put(params, "character_eve_id", char_id)
%{"solar_system_id" => solar_system_id} = params
)
when is_integer(solar_system_id) do
# Convert solar_system_id to system_id for internal use
with {:ok, system} <- MapSystem.by_map_id_and_solar_system_id(map_id, solar_system_id) do
attrs =
params
|> Map.put("character_eve_id", char_id)
|> Map.put("system_id", system.id)
|> Map.delete("solar_system_id")
case Server.update_signatures(map_id, %{
added_signatures: [attrs],
updated_signatures: [],
removed_signatures: [],
solar_system_id: params["solar_system_id"],
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: false
}) do
:ok ->
{:ok, attrs}
case Server.update_signatures(map_id, %{
added_signatures: [attrs],
updated_signatures: [],
removed_signatures: [],
solar_system_id: solar_system_id,
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: false
}) do
:ok ->
# Try to fetch the created signature to return with proper fields
with {:ok, sigs} <-
MapSystemSignature.by_system_id_and_eve_ids(system.id, [attrs["eve_id"]]),
sig when not is_nil(sig) <- List.first(sigs) do
result =
sig
|> Map.from_struct()
|> Map.put(:solar_system_id, system.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
err ->
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
{:ok, result}
else
_ ->
# Fallback: return attrs with solar_system_id added
attrs_result =
attrs
|> Map.put(:solar_system_id, solar_system_id)
|> Map.drop(["system_id"])
{:ok, attrs_result}
end
err ->
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
end
else
_ ->
Logger.error(
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
)
{:error, :system_not_found}
end
end
def create_signature(
%{assigns: %{map_id: _map_id, owner_character_id: _char_id, owner_user_id: _user_id}} =
_conn,
%{"solar_system_id" => _invalid} = _params
),
do: {:error, :missing_params}
def create_signature(_conn, _params), do: {:error, :missing_params}
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
@@ -90,7 +138,18 @@ defmodule WandererApp.Map.Operations.Signatures do
delete_connection_with_sigs: false
})
{:ok, attrs}
# Fetch the updated signature to return with proper fields
with {:ok, updated_sig} <- MapSystemSignature.by_id(sig_id) do
result =
updated_sig
|> Map.from_struct()
|> Map.put(:solar_system_id, system.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
{:ok, result}
else
_ -> {:ok, attrs}
end
else
err ->
Logger.error("[update_signature] Unexpected error: #{inspect(err)}")

View File

@@ -22,7 +22,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
fn acl_id ->
update_acl(acl_id)
end,
max_concurrency: 10,
max_concurrency: System.schedulers_online() * 4,
timeout: :timer.seconds(15)
)
|> Enum.reduce(
@@ -59,6 +59,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
map_update = %{acls: map.acls, scope: map.scope}
WandererApp.Map.update_map(map_id, map_update)
WandererApp.Cache.delete("map_characters-#{map_id}")
broadcast_acl_updates({:ok, result}, map_id)
@@ -66,7 +67,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
end
def handle_acl_updated(map_id, acl_id) do
{:ok, map} =
{:ok, %{acls: acls}} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
@@ -74,8 +75,9 @@ defmodule WandererApp.Map.Server.AclsImpl do
]
)
if map.acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
WandererApp.Map.update_map(map_id, %{acls: map.acls})
if acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
WandererApp.Map.update_map(map_id, %{acls: acls})
WandererApp.Cache.delete("map_characters-#{map_id}")
:ok =
acl_id
@@ -85,7 +87,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
end
def handle_acl_deleted(map_id, _acl_id) do
{:ok, map} =
{:ok, %{acls: acls}} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
@@ -93,7 +95,8 @@ defmodule WandererApp.Map.Server.AclsImpl do
]
)
WandererApp.Map.update_map(map_id, %{acls: map.acls})
WandererApp.Map.update_map(map_id, %{acls: acls})
WandererApp.Cache.delete("map_characters-#{map_id}")
character_ids =
map_id

View File

@@ -59,7 +59,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
def update_tracked_characters(map_id) do
Task.start_link(fn ->
{:ok, map_tracked_character_ids} =
{:ok, all_map_tracked_character_ids} =
map_id
|> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_all()
|> case do
@@ -67,30 +67,19 @@ defmodule WandererApp.Map.Server.CharactersImpl do
_ -> {:ok, []}
end
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
map_active_tracked_characters =
map_tracked_character_ids
|> Enum.filter(fn character -> character in tracked_characters end)
{:ok, old_map_tracked_characters} =
{:ok, actual_map_tracked_characters} =
WandererApp.Cache.lookup("maps:#{map_id}:tracked_characters", [])
characters_to_remove = old_map_tracked_characters -- map_active_tracked_characters
characters_to_remove = actual_map_tracked_characters -- all_map_tracked_character_ids
{:ok, invalidate_character_ids} =
WandererApp.Cache.lookup(
"map_#{map_id}:invalidate_character_ids",
[]
)
WandererApp.Cache.insert(
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
(invalidate_character_ids ++ characters_to_remove) |> Enum.uniq()
characters_to_remove,
fn ids ->
(ids ++ characters_to_remove) |> Enum.uniq()
end
)
WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
:ok
end)
end
@@ -98,7 +87,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
def untrack_characters(map_id, character_ids) do
character_ids
|> Enum.each(fn character_id ->
is_character_map_active?(map_id, character_id)
character_map_active = is_character_map_active?(map_id, character_id)
character_map_active
|> untrack_character(map_id, character_id)
end)
end
@@ -126,16 +117,27 @@ defmodule WandererApp.Map.Server.CharactersImpl do
def cleanup_characters(map_id, owner_id) do
{:ok, invalidate_character_ids} =
WandererApp.Cache.lookup(
WandererApp.Cache.get_and_remove(
"map_#{map_id}:invalidate_character_ids",
[]
)
acls =
map_id
|> WandererApp.Map.get_map!()
|> Map.get(:acls, [])
if Enum.empty?(invalidate_character_ids) do
:ok
else
{:ok, %{acls: acls}} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
)
process_invalidate_characters(invalidate_character_ids, map_id, owner_id, acls)
end
end
defp process_invalidate_characters(invalidate_character_ids, map_id, owner_id, acls) do
invalidate_character_ids
|> Task.async_stream(
fn character_id ->
@@ -172,25 +174,24 @@ defmodule WandererApp.Map.Server.CharactersImpl do
end
end,
timeout: :timer.seconds(60),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, {:remove_character, character_id}} ->
remove_and_untrack_characters(map_id, [character_id])
:ok
|> Enum.reduce([], fn
{:ok, {:remove_character, character_id}}, acc ->
[character_id | acc]
{:ok, _result} ->
:ok
{:ok, _result}, acc ->
acc
{:error, reason} ->
{:error, reason}, acc ->
Logger.error("Error in cleanup_characters: #{inspect(reason)}")
acc
end)
WandererApp.Cache.insert(
"map_#{map_id}:invalidate_character_ids",
[]
)
|> case do
[] -> :ok
character_ids_to_remove -> remove_and_untrack_characters(map_id, character_ids_to_remove)
end
end
defp remove_and_untrack_characters(map_id, character_ids) do
@@ -224,86 +225,100 @@ defmodule WandererApp.Map.Server.CharactersImpl do
end
def update_characters(%{map_id: map_id} = state) do
{:ok, presence_character_ids} =
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
try do
{:ok, presence_character_ids} =
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
WandererApp.Cache.lookup!("maps:#{map_id}:tracked_characters", [])
|> Enum.filter(fn character_id -> character_id in presence_character_ids end)
|> Enum.map(fn character_id ->
Task.start_link(fn ->
character_updates =
maybe_update_online(map_id, character_id) ++
maybe_update_tracking_status(map_id, character_id) ++
maybe_update_location(map_id, character_id) ++
maybe_update_ship(map_id, character_id) ++
maybe_update_alliance(map_id, character_id) ++
maybe_update_corporation(map_id, character_id)
presence_character_ids
|> Task.async_stream(
fn character_id ->
character_updates =
maybe_update_online(map_id, character_id) ++
maybe_update_tracking_status(map_id, character_id) ++
maybe_update_location(map_id, character_id) ++
maybe_update_ship(map_id, character_id) ++
maybe_update_alliance(map_id, character_id) ++
maybe_update_corporation(map_id, character_id)
character_updates
|> Enum.filter(fn update -> update != :skip end)
|> Enum.map(fn update ->
update
|> case do
{:character_location, location_info, old_location_info} ->
update_location(
character_id,
location_info,
old_location_info,
state
)
character_updates
|> Enum.filter(fn update -> update != :skip end)
|> Enum.map(fn update ->
update
|> case do
{:character_location, location_info, old_location_info} ->
update_location(
character_id,
location_info,
old_location_info,
state
)
:broadcast
:broadcast
{:character_ship, _info} ->
:broadcast
{:character_ship, _info} ->
:broadcast
{:character_online, _info} ->
:broadcast
{:character_online, _info} ->
:broadcast
{:character_tracking, _info} ->
:broadcast
{:character_tracking, _info} ->
:broadcast
{:character_alliance, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
[character_id],
fn ids ->
[character_id | ids]
end
)
{:character_alliance, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
[character_id],
fn ids ->
[character_id | ids] |> Enum.uniq()
end
)
:broadcast
:broadcast
{:character_corporation, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
[character_id],
fn ids ->
[character_id | ids]
end
)
{:character_corporation, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
[character_id],
fn ids ->
[character_id | ids] |> Enum.uniq()
end
)
:broadcast
:broadcast
_ ->
:skip
end
end)
|> Enum.filter(fn update -> update != :skip end)
|> Enum.uniq()
|> Enum.each(fn update ->
case update do
:broadcast ->
update_character(map_id, character_id)
_ ->
:skip
end
end)
|> Enum.filter(fn update -> update != :skip end)
|> Enum.uniq()
|> Enum.each(fn update ->
case update do
:broadcast ->
update_character(map_id, character_id)
_ ->
:ok
end
end)
_ ->
:ok
end
end)
:ok
:ok
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> Logger.error("Error in update_characters: #{inspect(reason)}")
end)
end)
rescue
e ->
Logger.error("""
[Map Server] update_characters => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
end
end
defp update_character(map_id, character_id) do
@@ -373,6 +388,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
{:ok, character} =
WandererApp.Character.get_map_character(map_id, character_id, not_present: true)
WandererApp.Cache.delete("character:#{character.id}:tracking_paused")
add_character(%{map_id: map_id}, character, true)
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{

View File

@@ -4,6 +4,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
require Logger
alias WandererApp.Map.Server.Impl
alias WandererApp.Map.Server.SignaturesImpl
# @ccp1 -1
@c1 1
@@ -68,10 +69,38 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
# this class of systems will guaranty that no one real class will take that place
# @unknown 100_100
#
# default (env) setting, not EOL
@connection_time_status_default 0
# EOL 1h
@connection_time_status_eol 1
# EOL 4h
@connection_time_status_eol_4 2
# EOL 4.5h
@connection_time_status_eol_4_5 3
# EOL 16h
@connection_time_status_eol_16 4
# EOL 24h
@connection_time_status_eol_24 5
# EOL 48h
@connection_time_status_eol_48 6
# EOL 1h
@connection_eol_minutes 60
# EOL 4h
@connection_eol_4_minutes 4 * 60
# EOL 4.5h
@connection_eol_4_5_minutes 4.5 * 60
# EOL 16h
@connection_eol_16_minutes 16 * 60
# EOL 24h
@connection_eol_24_minutes 24 * 60
# EOL 48h
@connection_eol_48_minutes 48 * 60
@connection_type_wormhole 0
@connection_type_stargate 1
@connection_type_bridge 2
@medium_ship_size 1
def get_connection_auto_expire_hours(), do: WandererApp.Env.map_connection_auto_expire_hours()
@@ -146,6 +175,18 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
state
end
def update_connection_type(
%{map_id: map_id} = state,
%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id,
character_id: character_id
} = _connection_info,
type
) do
state
end
def get_connection_info(
%{map_id: map_id} = _state,
%{
@@ -174,7 +215,8 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
),
do:
update_connection(state, :update_time_status, [:time_status], connection_update, fn
%{time_status: old_time_status}, %{id: connection_id, time_status: time_status} ->
%{time_status: old_time_status},
%{id: connection_id, time_status: time_status} = updated_connection ->
case time_status == @connection_time_status_eol do
true ->
if old_time_status != @connection_time_status_eol do
@@ -182,6 +224,8 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
DateTime.utc_now()
)
set_start_time(map_id, connection_id, DateTime.utc_now())
end
_ ->
@@ -190,6 +234,10 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
set_start_time(map_id, connection_id, DateTime.utc_now())
end
end
if time_status != old_time_status do
maybe_update_linked_signature_time_status(map_id, updated_connection)
end
end)
def update_connection_type(
@@ -230,35 +278,38 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
state =
map_id
|> WandererApp.Map.list_connections!()
|> Enum.filter(fn %{
id: connection_id,
inserted_at: inserted_at,
solar_system_source: solar_system_source_id,
solar_system_target: solar_system_target_id,
type: type
} ->
connection_start_time = get_start_time(map_id, connection_id)
type != @connection_type_stargate &&
DateTime.diff(DateTime.utc_now(), connection_start_time, :hour) >=
connection_auto_eol_hours &&
is_connection_valid(
:wormholes,
solar_system_source_id,
solar_system_target_id
)
end)
|> Enum.reduce(state, fn %{
id: connection_id,
solar_system_source: solar_system_source_id,
solar_system_target: solar_system_target_id
solar_system_target: solar_system_target_id,
time_status: time_status,
type: type
},
state ->
state
|> update_connection_time_status(%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id,
time_status: @connection_time_status_eol
})
if type == @connection_type_wormhole do
connection_start_time = get_start_time(map_id, connection_id)
new_time_status = get_new_time_status(connection_start_time, time_status)
if new_time_status != time_status &&
is_connection_valid(
:wormholes,
solar_system_source_id,
solar_system_target_id
) do
set_start_time(map_id, connection_id, DateTime.utc_now())
state
|> update_connection_time_status(%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id,
time_status: new_time_status
})
else
state
end
else
state
end
end)
state =
@@ -268,37 +319,38 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
id: connection_id,
solar_system_source: solar_system_source_id,
solar_system_target: solar_system_target_id,
time_status: time_status,
type: type
} ->
connection_mark_eol_time =
get_connection_mark_eol_time(map_id, connection_id)
reverse_connection =
WandererApp.Map.get_connection(
map_id,
solar_system_target_id,
solar_system_source_id
)
is_connection_exist =
is_connection_exist(
map_id,
solar_system_source_id,
solar_system_target_id
) || not is_nil(reverse_connection)
is_connection_valid =
is_connection_valid(
:wormholes,
solar_system_source_id,
solar_system_target_id
)
) ||
not is_nil(
WandererApp.Map.get_connection(
map_id,
solar_system_target_id,
solar_system_source_id
)
)
not is_connection_exist ||
(type != @connection_type_stargate && is_connection_valid &&
DateTime.diff(DateTime.utc_now(), connection_mark_eol_time, :hour) >=
(type == @connection_type_wormhole &&
time_status == @connection_time_status_eol &&
is_connection_valid(
:wormholes,
solar_system_source_id,
solar_system_target_id
) &&
DateTime.diff(
DateTime.utc_now(),
get_connection_mark_eol_time(map_id, connection_id),
:hour
) >=
connection_auto_expire_hours - connection_auto_eol_hours +
+connection_eol_expire_timeout_hours)
connection_eol_expire_timeout_hours)
end)
|> Enum.reduce(state, fn %{
solar_system_source: solar_system_source_id,
@@ -314,30 +366,103 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
state
end
defp maybe_update_linked_signature_time_status(
map_id,
%{
time_status: time_status,
solar_system_source: solar_system_source,
solar_system_target: solar_system_target
} = updated_connection
) do
source_system =
WandererApp.Map.find_system_by_location(
map_id,
%{solar_system_id: solar_system_source}
)
target_system =
WandererApp.Map.find_system_by_location(
map_id,
%{solar_system_id: solar_system_target}
)
source_linked_signatures =
find_linked_signatures(source_system, target_system)
target_linked_signatures = find_linked_signatures(target_system, source_system)
update_signatures_time_status(
map_id,
source_system.solar_system_id,
source_linked_signatures,
time_status
)
update_signatures_time_status(
map_id,
target_system.solar_system_id,
target_linked_signatures,
time_status
)
end
defp find_linked_signatures(
%{id: source_system_id} = _source_system,
%{solar_system_id: solar_system_id, linked_sig_eve_id: linked_sig_eve_id} =
_target_system
)
when not is_nil(linked_sig_eve_id) do
{:ok, signatures} =
WandererApp.Api.MapSystemSignature.by_linked_system_id(solar_system_id)
signatures |> Enum.filter(fn sig -> sig.system_id == source_system_id end)
end
defp find_linked_signatures(_source_system, _target_system), do: []
defp update_signatures_time_status(_map_id, _solar_system_id, [], _time_status), do: :ok
defp update_signatures_time_status(map_id, solar_system_id, signatures, time_status) do
signatures
|> Enum.each(fn %{custom_info: custom_info_json} = sig ->
update_params =
if not is_nil(custom_info_json) do
updated_custom_info =
custom_info_json
|> Jason.decode!()
|> Map.merge(%{"time_status" => time_status})
|> Jason.encode!()
%{custom_info: updated_custom_info}
else
updated_custom_info = Jason.encode!(%{"time_status" => time_status})
%{custom_info: updated_custom_info}
end
SignaturesImpl.apply_update_signature(%{map_id: map_id}, sig, update_params)
end)
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
end
def maybe_add_connection(map_id, location, old_location, character_id, is_manual)
when not is_nil(location) and not is_nil(old_location) and
not is_nil(old_location.solar_system_id) and
location.solar_system_id != old_location.solar_system_id do
{:ok, character} = WandererApp.Character.get_character(character_id)
if not is_manual do
character_id
|> WandererApp.Character.get_character!()
|> case do
nil ->
:ok
:telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{})
character ->
:telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{})
{:ok, _} =
WandererApp.Api.MapChainPassages.new(%{
map_id: map_id,
character_id: character_id,
ship_type_id: character.ship,
ship_name: character.ship_name,
solar_system_source_id: old_location.solar_system_id,
solar_system_target_id: location.solar_system_id
})
end
{:ok, _} =
WandererApp.Api.MapChainPassages.new(%{
map_id: map_id,
character_id: character_id,
ship_type_id: character.ship,
ship_name: character.ship_name,
solar_system_source_id: old_location.solar_system_id,
solar_system_target_id: location.solar_system_id
})
end
case WandererApp.Map.check_connection(map_id, location, old_location) do
@@ -356,37 +481,19 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
@connection_type_wormhole
end
# Check if either system is C1 before creating the connection
{:ok, source_system_info} = get_system_static_info(old_location.solar_system_id)
{:ok, target_system_info} = get_system_static_info(location.solar_system_id)
# Set ship size type based on system classes and special rules
ship_size_type =
get_ship_size_type(
old_location.solar_system_id,
location.solar_system_id,
connection_type
)
time_status =
if connection_type == @connection_type_wormhole do
cond do
# C1 systems always get medium
source_system_info.system_class == @c1 or target_system_info.system_class == @c1 ->
@medium_ship_size
# C13 systems always get frigate
source_system_info.system_class == @c13 or target_system_info.system_class == @c13 ->
@frigate_ship_size
# C4 to null gets frigate (unless C4 is shattered)
(source_system_info.system_class == @c4 and target_system_info.system_class == @ns and
not source_system_info.is_shattered) or
(target_system_info.system_class == @c4 and
source_system_info.system_class == @ns and
not target_system_info.is_shattered) ->
@frigate_ship_size
true ->
# Default to large for other wormhole connections
@large_ship_size
end
@connection_time_status_eol_24
else
# Default to large for non-wormhole connections
@large_ship_size
@connection_time_status_default
end
{:ok, connection} =
@@ -395,7 +502,8 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
solar_system_source: old_location.solar_system_id,
solar_system_target: location.solar_system_id,
type: connection_type,
ship_size_type: ship_size_type
ship_size_type: ship_size_type,
time_status: time_status
})
if connection_type == @connection_type_wormhole do
@@ -422,8 +530,6 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
time_status: connection.time_status
})
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:map_connection_added, %{
character_id: character_id,
@@ -470,12 +576,12 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
end
end
def set_start_time(map_id, connection_id, start_time) do
WandererApp.Cache.put(
"map_#{map_id}:conn_#{connection_id}:start_time",
start_time
)
end
def set_start_time(map_id, connection_id, start_time),
do:
WandererApp.Cache.put(
"map_#{map_id}:conn_#{connection_id}:start_time",
start_time
)
def can_add_location(_scope, nil), do: false
@@ -527,33 +633,30 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
def is_connection_valid(scope, from_solar_system_id, to_solar_system_id)
when not is_nil(from_solar_system_id) and not is_nil(to_solar_system_id) do
{:ok, known_jumps} =
WandererApp.Api.MapSolarSystemJumps.find(%{
before_system_id: from_solar_system_id,
current_system_id: to_solar_system_id
})
{:ok, from_system_static_info} = get_system_static_info(from_solar_system_id)
{:ok, to_system_static_info} = get_system_static_info(to_solar_system_id)
case scope do
:wormholes ->
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
known_jumps |> Enum.empty?()
:stargates ->
# For stargates, we need to check:
# 1. Both systems are in known space (HS, LS, NS)
# 2. There is a known jump between them
# 3. Neither system is prohibited
from_system_static_info.system_class in @known_space and
to_system_static_info.system_class in @known_space and
with {:ok, known_jumps} <- find_solar_system_jump(from_solar_system_id, to_solar_system_id),
{:ok, from_system_static_info} <- get_system_static_info(from_solar_system_id),
{:ok, to_system_static_info} <- get_system_static_info(to_solar_system_id) do
case scope do
:wormholes ->
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (known_jumps |> Enum.empty?())
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
known_jumps |> Enum.empty?()
:stargates ->
# For stargates, we need to check:
# 1. Both systems are in known space (HS, LS, NS)
# 2. There is a known jump between them
# 3. Neither system is prohibited
from_system_static_info.system_class in @known_space and
to_system_static_info.system_class in @known_space and
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (known_jumps |> Enum.empty?())
end
else
_ -> false
end
end
@@ -570,6 +673,13 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
end
end
defp find_solar_system_jump(from_solar_system_id, to_solar_system_id) do
case WandererApp.CachedInfo.get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
{:ok, jump} when not is_nil(jump) -> {:ok, [jump]}
_ -> {:ok, []}
end
end
defp get_system_static_info(solar_system_id) do
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
{:ok, system_static_info} when not is_nil(system_static_info) ->
@@ -664,4 +774,79 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
state
end
end
defp get_ship_size_type(
source_solar_system_id,
target_solar_system_id,
@connection_type_wormhole
) do
# Check if either system is C1 before creating the connection
{:ok, source_system_info} = get_system_static_info(source_solar_system_id)
{:ok, target_system_info} = get_system_static_info(target_solar_system_id)
cond do
# C1 systems always get medium
source_system_info.system_class == @c1 or target_system_info.system_class == @c1 ->
@medium_ship_size
# C13 systems always get frigate
source_system_info.system_class == @c13 or target_system_info.system_class == @c13 ->
@frigate_ship_size
# C4 to null gets frigate (unless C4 is shattered)
(source_system_info.system_class == @c4 and target_system_info.system_class == @ns and
not source_system_info.is_shattered) or
(target_system_info.system_class == @c4 and
source_system_info.system_class == @ns and
not target_system_info.is_shattered) ->
@frigate_ship_size
true ->
# Default to large for other wormhole connections
@large_ship_size
end
end
# Default to large for non-wormhole connections
defp get_ship_size_type(_source_solar_system_id, _target_solar_system_id, _connection_type),
do: @large_ship_size
defp get_new_time_status(_start_time, @connection_time_status_default),
do: @connection_time_status_eol_24
defp get_new_time_status(start_time, old_time_status) do
left_minutes =
get_time_status_minutes(old_time_status) -
DateTime.diff(DateTime.utc_now(), start_time, :minute)
cond do
left_minutes <= @connection_eol_minutes ->
@connection_time_status_eol
left_minutes <= @connection_eol_4_minutes ->
@connection_time_status_eol_4
left_minutes <= @connection_eol_4_5_minutes ->
@connection_time_status_eol_4_5
left_minutes <= @connection_eol_16_minutes ->
@connection_time_status_eol_16
left_minutes <= @connection_eol_24_minutes ->
@connection_time_status_eol_24
left_minutes <= @connection_eol_48_minutes ->
@connection_time_status_eol_48
true ->
@connection_time_status_default
end
end
defp get_time_status_minutes(@connection_time_status_eol), do: @connection_eol_minutes
defp get_time_status_minutes(@connection_time_status_eol_4), do: @connection_eol_4_minutes
defp get_time_status_minutes(@connection_time_status_eol_4_5), do: @connection_eol_4_5_minutes
defp get_time_status_minutes(@connection_time_status_eol_16), do: @connection_eol_16_minutes
defp get_time_status_minutes(@connection_time_status_eol_24), do: @connection_eol_24_minutes
defp get_time_status_minutes(@connection_time_status_eol_48), do: @connection_eol_48_minutes
end

View File

@@ -25,14 +25,14 @@ defmodule WandererApp.Map.Server.Impl do
]
@systems_cleanup_timeout :timer.minutes(30)
@characters_cleanup_timeout :timer.minutes(1)
@connections_cleanup_timeout :timer.minutes(2)
@characters_cleanup_timeout :timer.minutes(5)
@connections_cleanup_timeout :timer.minutes(1)
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
@backup_state_timeout :timer.minutes(1)
@update_presence_timeout :timer.seconds(5)
@update_characters_timeout :timer.seconds(1)
@update_tracked_characters_timeout :timer.seconds(1)
@update_tracked_characters_timeout :timer.minutes(1)
def new(), do: __struct__()
def new(args), do: __struct__(args)
@@ -96,11 +96,17 @@ defmodule WandererApp.Map.Server.Impl do
)
Process.send_after(self(), :update_characters, @update_characters_timeout)
Process.send_after(self(), :update_tracked_characters, 100)
Process.send_after(
self(),
:update_tracked_characters,
@update_tracked_characters_timeout
)
Process.send_after(self(), :update_presence, @update_presence_timeout)
Process.send_after(self(), :cleanup_connections, 5_000)
Process.send_after(self(), :cleanup_connections, @connections_cleanup_timeout)
Process.send_after(self(), :cleanup_systems, 10_000)
Process.send_after(self(), :cleanup_characters, :timer.minutes(5))
Process.send_after(self(), :cleanup_characters, @characters_cleanup_timeout)
Process.send_after(self(), :backup_state, @backup_state_timeout)
WandererApp.Cache.insert("map_#{map_id}:started", true)
@@ -127,6 +133,7 @@ defmodule WandererApp.Map.Server.Impl do
Logger.debug(fn -> "Stopping map server for #{map_id}" end)
WandererApp.Cache.delete("map_#{map_id}:started")
WandererApp.Cache.delete("map_characters-#{map_id}")
:telemetry.execute([:wanderer_app, :map, :stopped], %{count: 1})
@@ -278,7 +285,7 @@ defmodule WandererApp.Map.Server.Impl do
end
def handle_event({:acl_deleted, %{acl_id: acl_id}}, %{map_id: map_id} = state) do
AclsImpl.handle_acl_updated(map_id, acl_id)
AclsImpl.handle_acl_deleted(map_id, acl_id)
state
end
@@ -580,18 +587,27 @@ defmodule WandererApp.Map.Server.Impl do
{:ok, presence_character_ids} =
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
characters_ids =
map_id
|> WandererApp.Map.get_map!()
|> Map.get(:characters, [])
{:ok, old_presence_character_ids} =
WandererApp.Cache.lookup("map_#{map_id}:old_presence_character_ids", [])
new_present_character_ids =
presence_character_ids
|> Enum.filter(fn character_id ->
not Enum.member?(old_presence_character_ids, character_id)
end)
not_present_character_ids =
characters_ids
old_presence_character_ids
|> Enum.filter(fn character_id ->
not Enum.member?(presence_character_ids, character_id)
end)
CharactersImpl.track_characters(map_id, presence_character_ids)
WandererApp.Cache.insert(
"map_#{map_id}:old_presence_character_ids",
presence_character_ids
)
CharactersImpl.track_characters(map_id, new_present_character_ids)
CharactersImpl.untrack_characters(map_id, not_present_character_ids)
broadcast!(

View File

@@ -17,51 +17,53 @@ defmodule WandererApp.Map.Server.PingsImpl do
user_id: user_id
} = ping_info
) do
{:ok, character} = WandererApp.Character.get_character(character_id)
with {:ok, character} <- WandererApp.Character.get_character(character_id),
system <-
WandererApp.Map.find_system_by_location(map_id, %{
solar_system_id: solar_system_id |> String.to_integer()
}),
{:ok, ping} <-
WandererApp.MapPingsRepo.create(%{
map_id: map_id,
character_id: character_id,
system_id: system.id,
message: message,
type: type
}) do
Impl.broadcast!(
map_id,
:ping_added,
ping |> Map.merge(%{character_eve_id: character.eve_id, solar_system_id: solar_system_id})
)
system =
WandererApp.Map.find_system_by_location(map_id, %{
solar_system_id: solar_system_id |> String.to_integer()
})
# Broadcast rally point events to external clients (webhooks/SSE)
if type == 1 do
WandererApp.ExternalEvents.broadcast(map_id, :rally_point_added, %{
rally_point_id: ping.id,
solar_system_id: solar_system_id,
system_id: system.id,
character_id: character_id,
character_name: character.name,
character_eve_id: character.eve_id,
system_name: system.name,
message: message,
created_at: ping.inserted_at
})
end
{:ok, ping} =
WandererApp.MapPingsRepo.create(%{
WandererApp.User.ActivityTracker.track_map_event(:map_rally_added, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
character_id: character_id,
system_id: system.id,
message: message,
type: type
solar_system_id: "#{solar_system_id}"
})
Impl.broadcast!(
map_id,
:ping_added,
ping |> Map.merge(%{character_eve_id: character.eve_id, solar_system_id: solar_system_id})
)
# Broadcast rally point events to external clients (webhooks/SSE)
if type == 1 do
WandererApp.ExternalEvents.broadcast(map_id, :rally_point_added, %{
rally_point_id: ping.id,
solar_system_id: solar_system_id,
system_id: system.id,
character_id: character_id,
character_name: character.name,
character_eve_id: character.eve_id,
system_name: system.name,
message: message,
created_at: ping.inserted_at
})
state
else
error ->
Logger.error("Failed to add_ping: #{inspect(error, pretty: true)}")
state
end
WandererApp.User.ActivityTracker.track_map_event(:map_rally_added, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_id: "#{solar_system_id}"
})
state
end
def cancel_ping(
@@ -73,39 +75,42 @@ defmodule WandererApp.Map.Server.PingsImpl do
type: type
} = ping_info
) do
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, %{system: %{id: system_id, name: system_name, solar_system_id: solar_system_id}} = ping} =
WandererApp.MapPingsRepo.get_by_id(ping_id)
:ok = WandererApp.MapPingsRepo.destroy(ping)
Impl.broadcast!(map_id, :ping_cancelled, %{
id: ping_id,
solar_system_id: solar_system_id,
type: type
})
# Broadcast rally point removal events to external clients (webhooks/SSE)
if type == 1 do
WandererApp.ExternalEvents.broadcast(map_id, :rally_point_removed, %{
with {:ok, character} <- WandererApp.Character.get_character(character_id),
{:ok,
%{system: %{id: system_id, name: system_name, solar_system_id: solar_system_id}} = ping} <-
WandererApp.MapPingsRepo.get_by_id(ping_id),
:ok <- WandererApp.MapPingsRepo.destroy(ping) do
Impl.broadcast!(map_id, :ping_cancelled, %{
id: ping_id,
solar_system_id: solar_system_id,
system_id: system_id,
character_id: character_id,
character_name: character.name,
character_eve_id: character.eve_id,
system_name: system_name
type: type
})
# Broadcast rally point removal events to external clients (webhooks/SSE)
if type == 1 do
WandererApp.ExternalEvents.broadcast(map_id, :rally_point_removed, %{
id: ping_id,
solar_system_id: solar_system_id,
system_id: system_id,
character_id: character_id,
character_name: character.name,
character_eve_id: character.eve_id,
system_name: system_name
})
end
WandererApp.User.ActivityTracker.track_map_event(:map_rally_cancelled, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_id: solar_system_id
})
state
else
error ->
Logger.error("Failed to cancel_ping: #{inspect(error, pretty: true)}")
state
end
WandererApp.User.ActivityTracker.track_map_event(:map_rally_cancelled, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_id: solar_system_id
})
state
end
end

View File

@@ -7,6 +7,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
alias WandererApp.Character
alias WandererApp.User.ActivityTracker
alias WandererApp.Map.Server.{Impl, ConnectionsImpl, SystemsImpl}
alias WandererApp.Utils.EVEUtil
@doc """
Public entrypoint for updating signatures on a map system.
@@ -92,7 +93,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|> Enum.filter(&(&1.eve_id in updated_ids))
|> Enum.each(fn existing ->
update = Enum.find(updated_sigs, &(&1.eve_id == existing.eve_id))
apply_update_signature(existing, update)
apply_update_signature(state, existing, update)
end)
# 3. Additions & restorations
@@ -209,13 +210,19 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
MapSystemSignature.update!(sig, %{deleted: true})
end
defp apply_update_signature(%MapSystemSignature{} = existing, update_params)
when not is_nil(update_params) do
def apply_update_signature(
state,
%MapSystemSignature{} = existing,
update_params
)
when not is_nil(update_params) do
case MapSystemSignature.update(
existing,
update_params |> Map.put(:update_forced_at, DateTime.utc_now())
) do
{:ok, _updated} ->
{:ok, updated} ->
maybe_update_connection_time_status(state, existing, updated)
maybe_update_connection_mass_status(state, existing, updated)
:ok
{:error, reason} ->
@@ -223,6 +230,52 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
end
end
defp maybe_update_connection_time_status(
state,
%{custom_info: old_custom_info} = old_sig,
%{custom_info: new_custom_info, system_id: system_id, linked_system_id: linked_system_id} =
updated_sig
)
when not is_nil(linked_system_id) do
old_time_status = get_time_status(old_custom_info)
new_time_status = get_time_status(new_custom_info)
if old_time_status != new_time_status do
{:ok, source_system} = MapSystem.by_id(system_id)
ConnectionsImpl.update_connection_time_status(state, %{
solar_system_source_id: source_system.solar_system_id,
solar_system_target_id: linked_system_id,
time_status: new_time_status
})
end
end
defp maybe_update_connection_time_status(_state, _old_sig, _updated_sig), do: :ok
defp maybe_update_connection_mass_status(
state,
%{type: old_type} = old_sig,
%{type: new_type, system_id: system_id, linked_system_id: linked_system_id} =
updated_sig
)
when not is_nil(linked_system_id) do
if old_type != new_type do
{:ok, source_system} = MapSystem.by_id(system_id)
signature_ship_size_type = EVEUtil.get_wh_size(new_type)
if not is_nil(signature_ship_size_type) do
ConnectionsImpl.update_connection_ship_size_type(state, %{
solar_system_source_id: source_system.solar_system_id,
solar_system_target_id: linked_system_id,
ship_size_type: signature_ship_size_type
})
end
end
end
defp maybe_update_connection_mass_status(_state, _old_sig, _updated_sig), do: :ok
defp track_activity(event, map_id, solar_system_id, user_id, character_id, signatures) do
ActivityTracker.track_map_event(event, %{
map_id: map_id,
@@ -251,4 +304,12 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
}
end)
end
defp get_time_status(nil), do: nil
defp get_time_status(custom_info_json) do
custom_info_json
|> Jason.decode!()
|> Map.get("time_status")
end
end

View File

@@ -41,7 +41,7 @@ defmodule WandererApp.Maps do
system |> Map.take(@minimum_route_attrs)
end
end,
max_concurrency: 10
max_concurrency: System.schedulers_online() * 4
)
|> Enum.map(fn {:ok, val} -> val end)
@@ -94,13 +94,22 @@ defmodule WandererApp.Maps do
end
end
def load_characters(map, character_settings, user_id) do
def load_characters(map, user_id) when not is_nil(map) do
{:ok, user_characters} =
WandererApp.Api.Character.active_by_user(%{user_id: user_id})
characters =
map_available_characters =
map
|> get_map_available_characters(user_characters)
{:ok, character_settings} =
WandererApp.MapCharacterSettingsRepo.get_by_map_filtered(
map.id,
map_available_characters |> Enum.map(& &1.id)
)
characters =
map_available_characters
|> Enum.map(fn c ->
map_character(c, character_settings |> Enum.find(&(&1.character_id == c.id)))
end)
@@ -108,6 +117,8 @@ defmodule WandererApp.Maps do
{:ok, %{characters: characters}}
end
def load_characters(_map, _user_id), do: {:ok, %{characters: []}}
def map_character(
%{
name: name,
@@ -176,48 +187,57 @@ defmodule WandererApp.Maps do
tracked: tracked
}
@decorate cacheable(
cache: WandererApp.Cache,
key: "map_characters-#{map_id}",
opts: [ttl: :timer.seconds(2)]
)
defp _get_map_characters(%{id: map_id} = map) do
map_acls =
map.acls
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
defp get_map_characters(%{id: map_id} = map) do
WandererApp.Cache.lookup!("map_characters-#{map_id}")
|> case do
nil ->
map_acls =
map.acls
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
map_acl_owner_ids =
map_acls
|> Enum.map(fn acl -> acl.owner_id end)
map_acl_owner_ids =
map_acls
|> Enum.map(fn acl -> acl.owner_id end)
map_members =
map_acls
|> Enum.map(fn acl -> acl.members end)
|> List.flatten()
|> Enum.filter(fn member -> member.role != :blocked end)
map_members =
map_acls
|> Enum.map(fn acl -> acl.members end)
|> List.flatten()
|> Enum.filter(fn member -> member.role != :blocked end)
map_member_eve_ids =
map_members
|> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
|> Enum.map(fn member -> member.eve_character_id end)
map_member_eve_ids =
map_members
|> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
|> Enum.map(fn member -> member.eve_character_id end)
map_member_corporation_ids =
map_members
|> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
|> Enum.map(fn member -> member.eve_corporation_id end)
map_member_corporation_ids =
map_members
|> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
|> Enum.map(fn member -> member.eve_corporation_id end)
map_member_alliance_ids =
map_members
|> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
|> Enum.map(fn member -> member.eve_alliance_id end)
map_member_alliance_ids =
map_members
|> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
|> Enum.map(fn member -> member.eve_alliance_id end)
{:ok,
%{
map_acl_owner_ids: map_acl_owner_ids,
map_member_eve_ids: map_member_eve_ids,
map_member_corporation_ids: map_member_corporation_ids,
map_member_alliance_ids: map_member_alliance_ids
}}
map_characters =
%{
map_acl_owner_ids: map_acl_owner_ids,
map_member_eve_ids: map_member_eve_ids,
map_member_corporation_ids: map_member_corporation_ids,
map_member_alliance_ids: map_member_alliance_ids
}
WandererApp.Cache.insert(
"map_characters-#{map_id}",
map_characters
)
{:ok, map_characters}
map_characters ->
{:ok, map_characters}
end
end
defp get_map_available_characters(map, user_characters) do
@@ -227,7 +247,7 @@ defmodule WandererApp.Maps do
map_member_eve_ids: map_member_eve_ids,
map_member_corporation_ids: map_member_corporation_ids,
map_member_alliance_ids: map_member_alliance_ids
}} = _get_map_characters(map)
}} = get_map_characters(map)
user_characters
|> Enum.filter(fn c ->

View File

@@ -29,15 +29,24 @@ defmodule WandererApp.Metrics.PromExPlugin do
@impl true
def event_metrics(_opts) do
[
base_metrics = [
user_event_metrics(),
character_event_metrics(),
map_event_metrics(),
map_subscription_metrics(),
map_subscription_metrics()
]
advanced_metrics = [
character_event_metrics(),
characters_distribution_event_metrics(),
esi_event_metrics(),
json_api_metrics()
]
if WandererApp.Env.base_metrics_only() do
base_metrics
else
base_metrics ++ advanced_metrics
end
end
defp user_event_metrics do
@@ -227,8 +236,8 @@ defmodule WandererApp.Metrics.PromExPlugin do
defp get_esi_error_tag_values(metadata) do
%{
endpoint: Map.get(metadata, :endpoint, "unknown"),
error_type: to_string(Map.get(metadata, :error_type, "unknown")),
tracking_pool: Map.get(metadata, :tracking_pool, "unknown")
error_type: inspect(Map.get(metadata, :error_type, "unknown")),
tracking_pool: Map.get(metadata, :tracking_pool, "default")
}
end

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