mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-02-08 06:56:02 +00:00
Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d9fa3c6b5 | ||
|
|
25a3d8951e | ||
|
|
89d7df0ba2 | ||
|
|
ba0c10d2e4 | ||
|
|
996c88d839 | ||
|
|
80e998cf79 | ||
|
|
d2bcb89fa1 | ||
|
|
922f296f17 | ||
|
|
71dc20c933 | ||
|
|
80f7d34d3d | ||
|
|
113fe1c695 | ||
|
|
5550844912 | ||
|
|
0228e68a1d | ||
|
|
3424667af1 | ||
|
|
6c7b28a6c1 | ||
|
|
3988079cd3 | ||
|
|
f5d407fee0 | ||
|
|
a857422c46 | ||
|
|
ec6717d0ef | ||
|
|
56dacdcbbd | ||
|
|
c8e17b1691 | ||
|
|
19c7fe59ee | ||
|
|
682100c231 | ||
|
|
f9ac79cdcc | ||
|
|
f09f220645 | ||
|
|
e585cdfd20 | ||
|
|
3a3180f7b3 | ||
|
|
53abc580e5 | ||
|
|
8710d172a0 | ||
|
|
301a380a4b | ||
|
|
8c911f89e0 | ||
|
|
d7e09fc94e | ||
|
|
3b7e191898 | ||
|
|
f351fbaf20 | ||
|
|
016e793ba7 | ||
|
|
db483fd253 | ||
|
|
911ba231cd | ||
|
|
b3053f325d | ||
|
|
4ab47334fc | ||
|
|
e163f02526 | ||
|
|
9e22dba8f1 | ||
|
|
9631406def | ||
|
|
f6ae448c3b | ||
|
|
46345ef596 | ||
|
|
1625f16c8f | ||
|
|
b4ef9ae983 | ||
|
|
3b9c2dd996 | ||
|
|
8a0f9a58d0 | ||
|
|
5fe8caac0d | ||
|
|
f18f567727 | ||
|
|
91acc49980 | ||
|
|
ae3873a225 | ||
|
|
b351c6cc26 | ||
|
|
698244d945 | ||
|
|
2c7dd9dc5b | ||
|
|
36934cce0b | ||
|
|
b7da7e4ecb | ||
|
|
6471ea5590 | ||
|
|
b46bcac642 | ||
|
|
52d90361e9 | ||
|
|
1c902d3319 | ||
|
|
8f671a359b | ||
|
|
840c416684 | ||
|
|
56e29ad30a | ||
|
|
cd8f8b5801 | ||
|
|
70e013fa3d | ||
|
|
d6bfaf8008 | ||
|
|
95944199a0 | ||
|
|
3bd5db8cf3 | ||
|
|
a245330ada | ||
|
|
1226b6abf3 | ||
|
|
7a1f5c0966 | ||
|
|
e5afa1d5bc | ||
|
|
1473fe8646 | ||
|
|
7039ced11e | ||
|
|
42b5bb337f | ||
|
|
1dbb24f6ec | ||
|
|
c242f510e0 | ||
|
|
c59d51636e | ||
|
|
c5a8aa1b4d | ||
|
|
cba050a9e7 | ||
|
|
59fcbef3b1 | ||
|
|
2f1eb6eeaa | ||
|
|
71ae326cf7 | ||
|
|
07829caf0f | ||
|
|
a5850b5a8d | ||
|
|
9f6849209b | ||
|
|
7bd295cbad | ||
|
|
078e5fc19e | ||
|
|
3877e121c3 | ||
|
|
dcb2a0cdb2 | ||
|
|
f5294eee84 | ||
|
|
a5c87b6fa4 | ||
|
|
eae275f515 | ||
|
|
68ae6706dd | ||
|
|
a34b30af15 | ||
|
|
38b49266ed | ||
|
|
049884bb4c | ||
|
|
3c75b2b59f | ||
|
|
4ad5d191a3 | ||
|
|
2499c24cc1 | ||
|
|
6f0043205c | ||
|
|
597741fa60 | ||
|
|
d313ae8cd2 | ||
|
|
06d5d8072e | ||
|
|
f2d112df5c | ||
|
|
716604fa84 | ||
|
|
cae958a1e6 | ||
|
|
283b36c882 | ||
|
|
051e71f1a6 | ||
|
|
20a50e8db0 | ||
|
|
79d7f7ce7d | ||
|
|
6c4b65c446 | ||
|
|
2b07af5e12 | ||
|
|
d0901eecb4 | ||
|
|
ee85d29c54 | ||
|
|
a237d6513d | ||
|
|
02979588c1 | ||
|
|
3abe40855f | ||
|
|
d0d9418a89 | ||
|
|
3ce742eb01 | ||
|
|
ae566fb907 | ||
|
|
fa32c62f63 | ||
|
|
6880be11c5 | ||
|
|
5289893264 | ||
|
|
f15370a3df | ||
|
|
2cb2dc526c | ||
|
|
c3de3c4e35 | ||
|
|
4585c3a94b | ||
|
|
46a1898be9 | ||
|
|
e7219e0eec |
@@ -16,3 +16,8 @@ export WANDERER_SSE_ENABLED="true"
|
||||
export WANDERER_WEBHOOKS_ENABLED="true"
|
||||
export WANDERER_SSE_MAX_CONNECTIONS="1000"
|
||||
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
|
||||
|
||||
# Promo codes for map subscriptions (optional)
|
||||
# Format: CODE:DISCOUNT_PERCENT,CODE2:DISCOUNT_PERCENT2
|
||||
# Codes are case-insensitive, discounts stack with period discounts
|
||||
# export WANDERER_PROMO_CODES="PROMO2025:10,NEWUSER:20"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,6 +17,9 @@ repomix*
|
||||
/priv/static/images/
|
||||
/priv/static/*.js
|
||||
/priv/static/*.css
|
||||
/priv/static/*-*.png
|
||||
/priv/static/*-*.webp
|
||||
/priv/static/*-*.webmanifest
|
||||
|
||||
# Dialyzer PLT files
|
||||
/priv/plts/
|
||||
|
||||
263
CHANGELOG.md
263
CHANGELOG.md
@@ -2,6 +2,269 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.92.0](https://github.com/wanderer-industries/wanderer/compare/v1.91.11...v1.92.0) (2026-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Added ability to select a range of wh classes for k162.
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Show c1/c2/c3 or c4/c5 or link signature modal
|
||||
|
||||
## [v1.91.11](https://github.com/wanderer-industries/wanderer/compare/v1.91.10...v1.91.11) (2026-01-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* allow sig api when map relay is off
|
||||
|
||||
## [v1.91.10](https://github.com/wanderer-industries/wanderer/compare/v1.91.9...v1.91.10) (2026-01-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* remove actor context requirement from sig api
|
||||
|
||||
## [v1.91.9](https://github.com/wanderer-industries/wanderer/compare/v1.91.8...v1.91.9) (2026-01-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed rally point cancel logic
|
||||
|
||||
## [v1.91.8](https://github.com/wanderer-industries/wanderer/compare/v1.91.7...v1.91.8) (2026-01-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed rally point cancel logic
|
||||
|
||||
## [v1.91.7](https://github.com/wanderer-industries/wanderer/compare/v1.91.6...v1.91.7) (2026-01-05)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.91.6](https://github.com/wanderer-industries/wanderer/compare/v1.91.5...v1.91.6) (2026-01-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed new connections got deleted after linked signature cleanup
|
||||
|
||||
## [v1.91.5](https://github.com/wanderer-industries/wanderer/compare/v1.91.4...v1.91.5) (2025-12-30)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.91.4](https://github.com/wanderer-industries/wanderer/compare/v1.91.3...v1.91.4) (2025-12-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed connections create between k-space systems (considered as wh connection)
|
||||
|
||||
## [v1.91.3](https://github.com/wanderer-industries/wanderer/compare/v1.91.2...v1.91.3) (2025-12-28)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.91.2](https://github.com/wanderer-industries/wanderer/compare/v1.91.1...v1.91.2) (2025-12-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes updates & logic
|
||||
|
||||
## [v1.91.1](https://github.com/wanderer-industries/wanderer/compare/v1.91.0...v1.91.1) (2025-12-25)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.91.0](https://github.com/wanderer-industries/wanderer/compare/v1.90.13...v1.91.0) (2025-12-24)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* admin: added maps administration view with basic info, search, restore/delete, acls view and edit options
|
||||
|
||||
## [v1.90.13](https://github.com/wanderer-industries/wanderer/compare/v1.90.12...v1.90.13) (2025-12-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed welcome page
|
||||
|
||||
## [v1.90.12](https://github.com/wanderer-industries/wanderer/compare/v1.90.11...v1.90.12) (2025-12-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed permissions update after character corp updates
|
||||
|
||||
## [v1.90.11](https://github.com/wanderer-industries/wanderer/compare/v1.90.10...v1.90.11) (2025-12-18)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.90.10](https://github.com/wanderer-industries/wanderer/compare/v1.90.9...v1.90.10) (2025-12-18)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.90.9](https://github.com/wanderer-industries/wanderer/compare/v1.90.8...v1.90.9) (2025-12-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: reduce chracters untrack grace period to 15 mins (after change/close/disconnect from map)
|
||||
|
||||
## [v1.90.8](https://github.com/wanderer-industries/wanderer/compare/v1.90.7...v1.90.8) (2025-12-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: skip systems or connections cleanup for not started maps
|
||||
|
||||
## [v1.90.7](https://github.com/wanderer-industries/wanderer/compare/v1.90.6...v1.90.7) (2025-12-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed scopes
|
||||
|
||||
## [v1.90.6](https://github.com/wanderer-industries/wanderer/compare/v1.90.5...v1.90.6) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes
|
||||
|
||||
## [v1.90.5](https://github.com/wanderer-industries/wanderer/compare/v1.90.4...v1.90.5) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes
|
||||
|
||||
## [v1.90.4](https://github.com/wanderer-industries/wanderer/compare/v1.90.3...v1.90.4) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes & signatures clean up behaviour
|
||||
|
||||
## [v1.90.3](https://github.com/wanderer-industries/wanderer/compare/v1.90.2...v1.90.3) (2025-12-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: added pagination for long ACL lists
|
||||
|
||||
## [v1.90.2](https://github.com/wanderer-industries/wanderer/compare/v1.90.1...v1.90.2) (2025-12-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: added system position updates to SSE
|
||||
|
||||
## [v1.90.1](https://github.com/wanderer-industries/wanderer/compare/v1.90.0...v1.90.1) (2025-12-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed connections and signatures remove issues, added comprehensive audit log for auto removed connections and signatures
|
||||
|
||||
## [v1.90.0](https://github.com/wanderer-industries/wanderer/compare/v1.89.6...v1.90.0) (2025-12-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* core: Added several map scopes support (Wh, Hi, Low, Null, Pochven)
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed clean up for linked signatures
|
||||
|
||||
* core: fixed issue with default select mode
|
||||
|
||||
* apiV1 default fields updates
|
||||
|
||||
## [v1.89.6](https://github.com/wanderer-industries/wanderer/compare/v1.89.5...v1.89.6) (2025-12-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* kills: fixed zkb links (added "allow-popups-to-escape-sandbox" to CSP)
|
||||
|
||||
## [v1.89.5](https://github.com/wanderer-industries/wanderer/compare/v1.89.4...v1.89.5) (2025-12-02)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.89.4](https://github.com/wanderer-industries/wanderer/compare/v1.89.3...v1.89.4) (2025-12-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed acl character update issues
|
||||
|
||||
## [v1.89.3](https://github.com/wanderer-industries/wanderer/compare/v1.89.2...v1.89.3) (2025-11-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed tracking issues
|
||||
|
||||
## [v1.89.2](https://github.com/wanderer-industries/wanderer/compare/v1.89.1...v1.89.2) (2025-11-30)
|
||||
|
||||
|
||||
|
||||
50
Makefile
50
Makefile
@@ -32,6 +32,56 @@ format f:
|
||||
test t:
|
||||
MIX_ENV=test mix test
|
||||
|
||||
# Run tests in 4 parallel partitions (useful for CI or faster local runs)
|
||||
test-parallel tp:
|
||||
@echo "Running tests in 4 parallel partitions..."
|
||||
@mkdir -p /tmp/wanderer_test_results
|
||||
@rm -f /tmp/wanderer_test_results/partition_*.txt /tmp/wanderer_test_results/exit_*.txt
|
||||
@for i in 1 2 3 4; do \
|
||||
(MIX_TEST_PARTITION=$$i MIX_ENV=test mix test --partitions 4 2>&1; echo $$? > /tmp/wanderer_test_results/exit_$$i.txt) | \
|
||||
tee /tmp/wanderer_test_results/partition_$$i.txt | sed "s/^/[P$$i] /" & \
|
||||
done; \
|
||||
wait
|
||||
@echo ""
|
||||
@echo "========================================"
|
||||
@echo " TEST RESULTS SUMMARY"
|
||||
@echo "========================================"
|
||||
@total_tests=0; total_failures=0; total_excluded=0; all_passed=true; \
|
||||
for i in 1 2 3 4; do \
|
||||
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
|
||||
if [ "$$exit_code" != "0" ]; then all_passed=false; fi; \
|
||||
summary=$$(grep -E "^[0-9]+ (tests?|doctest)" /tmp/wanderer_test_results/partition_$$i.txt | tail -1 || echo "No results"); \
|
||||
tests=$$(echo "$$summary" | grep -oE "^[0-9]+" || echo "0"); \
|
||||
failures=$$(echo "$$summary" | grep -oE "[0-9]+ failures?" | grep -oE "^[0-9]+" || echo "0"); \
|
||||
excluded=$$(echo "$$summary" | grep -oE "[0-9]+ excluded" | grep -oE "^[0-9]+" || echo "0"); \
|
||||
total_tests=$$((total_tests + tests)); \
|
||||
total_failures=$$((total_failures + failures)); \
|
||||
total_excluded=$$((total_excluded + excluded)); \
|
||||
if [ "$$exit_code" = "0" ]; then \
|
||||
echo "Partition $$i: ✓ $$summary"; \
|
||||
else \
|
||||
echo "Partition $$i: ✗ $$summary (exit code: $$exit_code)"; \
|
||||
fi; \
|
||||
done; \
|
||||
echo "========================================"; \
|
||||
echo "TOTAL: $$total_tests tests, $$total_failures failures, $$total_excluded excluded"; \
|
||||
echo "========================================"; \
|
||||
if [ "$$all_passed" = "true" ]; then \
|
||||
echo "✓ All partitions passed!"; \
|
||||
else \
|
||||
echo "✗ Some partitions failed. Details below:"; \
|
||||
echo ""; \
|
||||
for i in 1 2 3 4; do \
|
||||
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
|
||||
if [ "$$exit_code" != "0" ]; then \
|
||||
echo "======== PARTITION $$i FAILURES ========"; \
|
||||
grep -A 50 "Failures:" /tmp/wanderer_test_results/partition_$$i.txt 2>/dev/null || cat /tmp/wanderer_test_results/partition_$$i.txt; \
|
||||
echo ""; \
|
||||
fi; \
|
||||
done; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
coverage cover co:
|
||||
MIX_ENV=test mix test --cover
|
||||
|
||||
|
||||
@@ -1001,3 +1001,27 @@ body > div:first-of-type {
|
||||
.verticalTabsContainer .p-tabview-panel {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Blog post CTA links - only in main post content */
|
||||
.post-content a {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);
|
||||
color: white !important;
|
||||
padding: 0.5rem 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
text-decoration: none !important;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(236, 72, 153, 0.3);
|
||||
}
|
||||
|
||||
.post-content a:hover {
|
||||
background: linear-gradient(135deg, #db2777 0%, #7c3aed 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(236, 72, 153, 0.4);
|
||||
}
|
||||
|
||||
.post-content a:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(236, 72, 153, 0.3);
|
||||
}
|
||||
|
||||
@@ -8,3 +8,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ContextMenu {
|
||||
width: max-content;
|
||||
min-width: unset;
|
||||
|
||||
:global {
|
||||
.p-submenu-list {
|
||||
width: max-content;
|
||||
min-width: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import React, { RefObject, useMemo } from 'react';
|
||||
import React, { RefObject, useCallback, useMemo } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
|
||||
import { CharacterTypeRaw, SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
|
||||
import classes from './ContextMenuSystemInfo.module.scss';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components';
|
||||
import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
import { Route } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { Route, RouteStationSummary } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
|
||||
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
|
||||
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
|
||||
import { useGetOwnOnlineCharacters } from '@/hooks/Mapper/components/hooks/useGetOwnOnlineCharacters.ts';
|
||||
import { sortStationsByDistance } from './sortStationsByDistance.ts';
|
||||
|
||||
export interface ContextMenuSystemInfoProps {
|
||||
systemStatics: Map<number, SolarSystemStaticInfoRaw>;
|
||||
hubs: string[];
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
systemId: string | undefined;
|
||||
systemIdFrom?: string | undefined;
|
||||
@@ -37,11 +39,109 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
onWaypointSet,
|
||||
systemId,
|
||||
systemIdFrom,
|
||||
hubs,
|
||||
routes,
|
||||
}) => {
|
||||
const getWaypointMenu = useWaypointMenu(onWaypointSet);
|
||||
const getJumpPlannerMenu = useJumpPlannerMenu(systems, systemIdFrom);
|
||||
const { toggleHubCommand, hubs } = useRouteProvider();
|
||||
const getOwnOnlineCharacters = useGetOwnOnlineCharacters();
|
||||
|
||||
const getStationWaypointItems = useCallback(
|
||||
(destinationId: string, chars: CharacterTypeRaw[]): MenuItem[] => [
|
||||
{
|
||||
label: 'Set Destination',
|
||||
icon: PrimeIcons.SEND,
|
||||
command: () => {
|
||||
onWaypointSet({
|
||||
fromBeginning: true,
|
||||
clearWay: true,
|
||||
destination: destinationId,
|
||||
charIds: chars.map(char => char.eve_id),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Add Waypoint',
|
||||
icon: PrimeIcons.DIRECTIONS_ALT,
|
||||
command: () => {
|
||||
onWaypointSet({
|
||||
fromBeginning: false,
|
||||
clearWay: false,
|
||||
destination: destinationId,
|
||||
charIds: chars.map(char => char.eve_id),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Add Waypoint Front',
|
||||
icon: PrimeIcons.DIRECTIONS,
|
||||
command: () => {
|
||||
onWaypointSet({
|
||||
fromBeginning: true,
|
||||
clearWay: false,
|
||||
destination: destinationId,
|
||||
charIds: chars.map(char => char.eve_id),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
[onWaypointSet],
|
||||
);
|
||||
|
||||
const getStationsMenu = useCallback(
|
||||
(stations: RouteStationSummary[]) => {
|
||||
const chars = getOwnOnlineCharacters().filter(x => x.online);
|
||||
const sortedStations = sortStationsByDistance(stations);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('JOipP', `sortedStations`, sortedStations);
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Stations',
|
||||
icon: PrimeIcons.MAP_MARKER,
|
||||
items: sortedStations.map(station => {
|
||||
const destinationId = station.station_id.toString();
|
||||
const specialClass = station.special ? '[&_.p-menuitem-text]:text-orange-400' : '';
|
||||
|
||||
if (chars.length === 0) {
|
||||
return {
|
||||
label: station.station_name,
|
||||
className: specialClass || undefined,
|
||||
items: [{ label: 'No online characters', disabled: true }],
|
||||
};
|
||||
}
|
||||
|
||||
if (chars.length === 1) {
|
||||
return {
|
||||
label: station.station_name,
|
||||
className: specialClass || undefined,
|
||||
items: getStationWaypointItems(destinationId, chars.slice(0, 1)),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: station.station_name,
|
||||
className: `${specialClass} w-[500px]`.trim(),
|
||||
items: [
|
||||
{
|
||||
label: 'All',
|
||||
icon: PrimeIcons.USERS,
|
||||
items: getStationWaypointItems(destinationId, chars),
|
||||
},
|
||||
...chars.map(char => ({
|
||||
label: char.name,
|
||||
icon: PrimeIcons.USER,
|
||||
items: getStationWaypointItems(destinationId, [char]),
|
||||
})),
|
||||
],
|
||||
};
|
||||
}),
|
||||
},
|
||||
];
|
||||
},
|
||||
[getOwnOnlineCharacters, getStationWaypointItems],
|
||||
);
|
||||
|
||||
const items: MenuItem[] = useMemo(() => {
|
||||
const system = systemId ? systemStatics.get(parseInt(systemId)) : undefined;
|
||||
@@ -50,6 +150,10 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
if (!systemId || !system) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const route = routes.find(x => x.destination?.toString() === systemId);
|
||||
const stationItems = route?.stations?.length ? getStationsMenu(route.stations) : [];
|
||||
|
||||
return [
|
||||
{
|
||||
className: classes.FastActions,
|
||||
@@ -69,15 +173,20 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
{ separator: true },
|
||||
...getJumpPlannerMenu(system, routes),
|
||||
...getWaypointMenu(systemId, system.system_class),
|
||||
{
|
||||
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
|
||||
icon: !hubs.includes(systemId) ? (
|
||||
<MapAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
command: onHubToggle,
|
||||
},
|
||||
...stationItems,
|
||||
...(toggleHubCommand
|
||||
? [
|
||||
{
|
||||
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
|
||||
icon: !hubs.includes(systemId) ? (
|
||||
<MapAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
command: onHubToggle,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!systemOnMap
|
||||
? [
|
||||
{
|
||||
@@ -94,15 +203,18 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
systems,
|
||||
getJumpPlannerMenu,
|
||||
getWaypointMenu,
|
||||
getStationsMenu,
|
||||
hubs,
|
||||
onHubToggle,
|
||||
onAddSystem,
|
||||
onOpenSettings,
|
||||
toggleHubCommand,
|
||||
routes,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu model={items} ref={contextMenuRef} breakpoint="767px" />
|
||||
<ContextMenu className={classes.ContextMenu} model={items} ref={contextMenuRef} breakpoint="767px" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const useContextMenuSystemInfoHandlers = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current.toggleHubCommand(system);
|
||||
ref.current.toggleHubCommand?.(system);
|
||||
setSystem(undefined);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export const useDetectSettingsChanged = () => {
|
||||
storedSettings: {
|
||||
interfaceSettings,
|
||||
settingsRoutes,
|
||||
settingsRoutesBy,
|
||||
settingsLocal,
|
||||
settingsSignatures,
|
||||
settingsOnTheMap,
|
||||
@@ -16,7 +17,15 @@ export const useDetectSettingsChanged = () => {
|
||||
|
||||
useEffect(
|
||||
() => setCounter(x => x + 1),
|
||||
[interfaceSettings, settingsRoutes, settingsLocal, settingsSignatures, settingsOnTheMap, settingsKills],
|
||||
[
|
||||
interfaceSettings,
|
||||
settingsRoutes,
|
||||
settingsRoutesBy,
|
||||
settingsLocal,
|
||||
settingsSignatures,
|
||||
settingsOnTheMap,
|
||||
settingsKills,
|
||||
],
|
||||
);
|
||||
|
||||
return counter;
|
||||
|
||||
@@ -72,7 +72,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
|
||||
const {
|
||||
storedSettings: { interfaceSettings },
|
||||
data: { systemSignatures: mapSystemSignatures },
|
||||
data: { systemSignatures: mapSystemSignatures, pings },
|
||||
} = useMapRootState();
|
||||
|
||||
const systemStaticInfo = useMemo(() => {
|
||||
@@ -108,7 +108,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
visibleNodes,
|
||||
showKSpaceBG,
|
||||
isThickConnections,
|
||||
pings,
|
||||
systemHighlighted,
|
||||
},
|
||||
outCommand,
|
||||
|
||||
@@ -121,6 +121,7 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!ping) {
|
||||
setIsShow(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,27 +162,26 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
};
|
||||
}, [interfaceSettings]);
|
||||
|
||||
if (!ping) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isShowSelectedSystem = selectedSystem != null && selectedSystem !== ping.solar_system_id;
|
||||
const isShowSelectedSystem = ping && selectedSystem != null && selectedSystem !== ping.solar_system_id;
|
||||
|
||||
// Only render Toast when there's a ping
|
||||
return (
|
||||
<>
|
||||
<Toast
|
||||
position={placement as never}
|
||||
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
|
||||
ref={toast}
|
||||
content={({ message }) => (
|
||||
<section
|
||||
className={clsx(
|
||||
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
|
||||
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
|
||||
{ping && (
|
||||
<Toast
|
||||
key={ping.id}
|
||||
position={placement as never}
|
||||
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
|
||||
ref={toast}
|
||||
content={({ message }) => (
|
||||
<section
|
||||
className={clsx(
|
||||
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
|
||||
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
@@ -253,28 +253,33 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
{/*/>*/}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
></Toast>
|
||||
)}
|
||||
></Toast>
|
||||
)}
|
||||
|
||||
<WdButton
|
||||
icon="pi pi-bell"
|
||||
severity="warning"
|
||||
aria-label="Notification"
|
||||
size="small"
|
||||
className="w-[33px] h-[33px]"
|
||||
outlined
|
||||
onClick={handleClickShow}
|
||||
disabled={isShow}
|
||||
/>
|
||||
{ping && (
|
||||
<>
|
||||
<WdButton
|
||||
icon="pi pi-bell"
|
||||
severity="warning"
|
||||
aria-label="Notification"
|
||||
size="small"
|
||||
className="w-[33px] h-[33px]"
|
||||
outlined
|
||||
onClick={handleClickShow}
|
||||
disabled={isShow}
|
||||
/>
|
||||
|
||||
<ConfirmPopup
|
||||
target={cfRef.current}
|
||||
visible={cfVisible}
|
||||
onHide={cfHide}
|
||||
message="Are you sure you want to delete ping?"
|
||||
icon="pi pi-exclamation-triangle text-orange-400"
|
||||
accept={removePing}
|
||||
/>
|
||||
<ConfirmPopup
|
||||
target={cfRef.current}
|
||||
visible={cfVisible}
|
||||
onHide={cfHide}
|
||||
message="Are you sure you want to delete ping?"
|
||||
icon="pi pi-exclamation-triangle text-orange-400"
|
||||
accept={removePing}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { useSystemInfo } from '@/hooks/Mapper/components/hooks';
|
||||
import {
|
||||
SOLAR_SYSTEM_CLASS_IDS,
|
||||
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
|
||||
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
|
||||
SOLAR_SYSTEM_CLASS_IDS,
|
||||
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
|
||||
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
@@ -91,7 +91,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
|
||||
if (k162TypeInfo) {
|
||||
// Check if the k162Type matches our target system class
|
||||
return customInfo.k162Type === targetSystemClassGroup;
|
||||
return k162TypeInfo.value.includes(targetSystemClassGroup);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
SystemStructures,
|
||||
WRoutesPublic,
|
||||
WRoutesUser,
|
||||
WRoutesBy,
|
||||
WSystemKills,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
|
||||
@@ -18,6 +19,7 @@ export enum WidgetsIds {
|
||||
signatures = 'signatures',
|
||||
local = 'local',
|
||||
routes = 'routes',
|
||||
routesBy = 'routesBy',
|
||||
structures = 'structures',
|
||||
kills = 'kills',
|
||||
comments = 'comments',
|
||||
@@ -60,6 +62,13 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
|
||||
zIndex: 0,
|
||||
content: () => <WRoutesPublic />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.routesBy,
|
||||
position: { x: 10, y: 740 },
|
||||
size: { width: 510, height: 200 },
|
||||
zIndex: 0,
|
||||
content: () => <WRoutesBy />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.userRoutes,
|
||||
position: { x: 10, y: 10 },
|
||||
@@ -112,6 +121,10 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
|
||||
id: WidgetsIds.routes,
|
||||
label: 'Routes',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.routesBy,
|
||||
label: 'Routes By',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.userRoutes,
|
||||
label: 'User Routes',
|
||||
|
||||
@@ -86,6 +86,13 @@ export const RoutesWidgetContent = () => {
|
||||
[handleClick],
|
||||
);
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.log('JOipP', `loading`, loading);
|
||||
// }, [loading]);
|
||||
|
||||
|
||||
if (isRestricted && !isSubscriptionActive) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
@@ -108,6 +115,7 @@ export const RoutesWidgetContent = () => {
|
||||
return <div className="w-full h-full flex justify-center items-center select-none">Routes not set</div>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingWrapper loading={loading}>
|
||||
@@ -129,7 +137,6 @@ export const RoutesWidgetContent = () => {
|
||||
offset: 10,
|
||||
}}
|
||||
/>
|
||||
|
||||
<SystemView
|
||||
systemId={route.destination.toString()}
|
||||
className={clsx('select-none text-center cursor-context-menu')}
|
||||
@@ -138,7 +145,7 @@ export const RoutesWidgetContent = () => {
|
||||
showCustomName
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right pl-1">{route.has_connection ? route.systems?.length ?? 2 : ''}</div>
|
||||
<div className="text-right pl-1">{route.has_connection ? (route.systems?.length ?? 2) : ''}</div>
|
||||
<div className="pl-2 pb-0.5">
|
||||
<RoutesList data={route} onContextMenu={handleContextMenu} />
|
||||
</div>
|
||||
@@ -147,9 +154,7 @@ export const RoutesWidgetContent = () => {
|
||||
})}
|
||||
</div>
|
||||
</LoadingWrapper>
|
||||
|
||||
<ContextMenuSystemInfo
|
||||
hubs={hubs}
|
||||
routes={preparedRoutes}
|
||||
systems={systems}
|
||||
systemStatics={systemStatics}
|
||||
@@ -162,9 +167,10 @@ export const RoutesWidgetContent = () => {
|
||||
|
||||
type RoutesWidgetCompProps = {
|
||||
title: ReactNode | string;
|
||||
renderContent?: (content: ReactNode, compact: boolean) => ReactNode;
|
||||
};
|
||||
|
||||
export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps) => {
|
||||
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
|
||||
const { data, update, addHubCommand } = useRouteProvider();
|
||||
|
||||
@@ -183,7 +189,7 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
|
||||
|
||||
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
|
||||
async item => addHubCommand(item.value.toString()),
|
||||
async item => addHubCommand?.(item.value.toString()),
|
||||
[addHubCommand],
|
||||
);
|
||||
|
||||
@@ -191,15 +197,17 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
<Widget
|
||||
label={
|
||||
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
|
||||
<span className="select-none">{title}</span>
|
||||
<div className="select-none flex items-center gap-2">{title}</div>
|
||||
<LayoutEventBlocker className="flex items-center gap-2">
|
||||
<WdImgButton
|
||||
className={PrimeIcons.PLUS_CIRCLE}
|
||||
onClick={onAddSystem}
|
||||
tooltip={{
|
||||
content: 'Click here to add new system to routes',
|
||||
}}
|
||||
/>
|
||||
{addHubCommand && (
|
||||
<WdImgButton
|
||||
className={PrimeIcons.PLUS_CIRCLE}
|
||||
onClick={onAddSystem}
|
||||
tooltip={{
|
||||
content: 'Click here to add new system to routes',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<WdTooltipWrapper content="Show shortest route" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
@@ -223,24 +231,38 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<RoutesWidgetContent />
|
||||
{renderContent ? (
|
||||
renderContent(
|
||||
<div className="h-full overflow-auto bg-opacity-5 custom-scrollbar">
|
||||
<RoutesWidgetContent />
|
||||
</div>,
|
||||
compact,
|
||||
)
|
||||
) : (
|
||||
<div className="h-full overflow-auto bg-opacity-5 custom-scrollbar">
|
||||
<RoutesWidgetContent />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RoutesSettingsDialog visible={routeSettingsVisible} setVisible={setRouteSettingsVisible} />
|
||||
|
||||
<AddSystemDialog
|
||||
title="Add system to routes"
|
||||
visible={openAddSystem}
|
||||
setVisible={() => setOpenAddSystem(false)}
|
||||
onSubmit={handleSubmitAddSystem}
|
||||
/>
|
||||
{addHubCommand && (
|
||||
<AddSystemDialog
|
||||
title="Add system to routes"
|
||||
visible={openAddSystem}
|
||||
setVisible={() => setOpenAddSystem(false)}
|
||||
onSubmit={handleSubmitAddSystem}
|
||||
/>
|
||||
)}
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export const RoutesWidget = forwardRef<RoutesImperativeHandle, RoutesWidgetProps & RoutesWidgetCompProps>(
|
||||
({ title, ...props }, ref) => {
|
||||
({ title, renderContent, ...props }, ref) => {
|
||||
return (
|
||||
<RoutesProvider {...props} ref={ref}>
|
||||
<RoutesWidgetComp title={title} />
|
||||
<RoutesWidgetComp title={title} renderContent={renderContent} />
|
||||
</RoutesProvider>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './useLoadRoutes';
|
||||
export * from './useLoadRoutesBy';
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { flattenValues } from '@/hooks/Mapper/utils/flattenValues.ts';
|
||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||
import { Commands } from '@/hooks/Mapper/types';
|
||||
|
||||
function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
type UseLoadRoutesByProps = {
|
||||
loadRoutesCommand: LoadRoutesCommand;
|
||||
routesList: RoutesList | undefined;
|
||||
data: RoutesType;
|
||||
deps?: unknown[];
|
||||
};
|
||||
|
||||
export const useLoadRoutesBy = ({
|
||||
data: routesSettings,
|
||||
loadRoutesCommand,
|
||||
routesList,
|
||||
deps = [],
|
||||
}: UseLoadRoutesByProps) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const {
|
||||
data: { selectedSystems },
|
||||
} = useMapRootState();
|
||||
|
||||
const prevSys = usePrevious(selectedSystems);
|
||||
const ref = useRef({ prevSys, selectedSystems });
|
||||
ref.current = { prevSys, selectedSystems };
|
||||
|
||||
const loadRoutes = useCallback(
|
||||
(systemId: string, settings: RoutesType) => {
|
||||
loadRoutesCommand(systemId, settings);
|
||||
setLoading(true);
|
||||
},
|
||||
[loadRoutesCommand],
|
||||
);
|
||||
|
||||
useMapEventListener(event => {
|
||||
if (event.name === Commands.routesListBy) {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, [routesList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSystems.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
loadRoutes(systemId, routesSettings);
|
||||
}, [loadRoutes, selectedSystems, ...flattenValues(routesSettings), ...deps]);
|
||||
|
||||
return { loading, loadRoutes, setLoading };
|
||||
};
|
||||
@@ -12,8 +12,8 @@ export type RoutesWidgetProps = {
|
||||
routesList: RoutesList | undefined;
|
||||
loading: boolean;
|
||||
|
||||
addHubCommand: AddHubCommand;
|
||||
toggleHubCommand: ToggleHubCommand;
|
||||
addHubCommand?: AddHubCommand;
|
||||
toggleHubCommand?: ToggleHubCommand;
|
||||
isRestricted?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { RoutesWidget } from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
|
||||
import { useLoadRoutesBy } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/hooks';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { SelectItemOptionsType } from 'primereact/selectitem';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||
import clsx from 'clsx';
|
||||
import { RoutesByCategoryType, RoutesByScopeType, RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { DEFAULT_ROUTES_SETTINGS } from '@/hooks/Mapper/mapRootProvider/constants.ts';
|
||||
|
||||
export type RoutesByType = RoutesByCategoryType;
|
||||
export type RoutesBySecurityType = RoutesByScopeType;
|
||||
|
||||
type WRoutesByProps = {
|
||||
type?: RoutesByType;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const ROUTES_BY_OPTIONS: SelectItemOptionsType = [
|
||||
{
|
||||
label: 'Blue Loot',
|
||||
value: 'blueLoot',
|
||||
icon: 'images/30747_64.png',
|
||||
},
|
||||
{
|
||||
label: 'Red Loot',
|
||||
value: 'redLoot',
|
||||
icon: 'images/89219_64.png',
|
||||
},
|
||||
{
|
||||
label: 'Thera',
|
||||
value: 'thera',
|
||||
icon: 'images/map.png',
|
||||
},
|
||||
{
|
||||
label: 'Turnur',
|
||||
value: 'turnur',
|
||||
icon: 'images/map.png',
|
||||
},
|
||||
{
|
||||
label: 'Security Office',
|
||||
value: 'so_cleaning',
|
||||
icon: 'images/concord-so.png',
|
||||
},
|
||||
{
|
||||
label: 'Trade Hubs',
|
||||
value: 'trade_hubs',
|
||||
icon: 'images/market.png',
|
||||
},
|
||||
];
|
||||
const ROUTES_BY_SECURITY_OPTIONS = [
|
||||
{ label: 'All', value: 'ALL' },
|
||||
{ label: 'High', value: 'HIGH' },
|
||||
];
|
||||
|
||||
export const WRoutesBy = ({ type = 'blueLoot', title = 'Routes By' }: WRoutesByProps) => {
|
||||
const {
|
||||
outCommand,
|
||||
storedSettings: { settingsRoutesBy, settingsRoutesByUpdate },
|
||||
data,
|
||||
} = useMapRootState();
|
||||
|
||||
const criteriaType = settingsRoutesBy.type ?? type;
|
||||
const securityType = settingsRoutesBy.scope ?? 'ALL';
|
||||
const routesSettings = settingsRoutesBy.routes ?? DEFAULT_ROUTES_SETTINGS;
|
||||
const routesListBy = data.routesListBy;
|
||||
|
||||
const loadRoutesCommand: LoadRoutesCommand = useCallback(
|
||||
async (systemId, currentRoutesSettings) => {
|
||||
await outCommand({
|
||||
type: OutCommand.getRoutesBy,
|
||||
data: {
|
||||
system_id: systemId,
|
||||
type: criteriaType,
|
||||
securityType: securityType === 'HIGH' ? 'high' : 'both',
|
||||
routes_settings: currentRoutesSettings,
|
||||
},
|
||||
});
|
||||
},
|
||||
[outCommand, criteriaType, securityType],
|
||||
);
|
||||
|
||||
const hubs = useMemo(() => routesListBy?.routes?.map(route => route.destination.toString()) ?? [], [routesListBy]);
|
||||
|
||||
const { loading: internalLoading } = useLoadRoutesBy({
|
||||
data: routesSettings,
|
||||
loadRoutesCommand,
|
||||
routesList: routesListBy,
|
||||
deps: [criteriaType, securityType],
|
||||
});
|
||||
|
||||
const updateRoutesSettings = useCallback(
|
||||
(next: Partial<RoutesType> | ((prev: RoutesType) => Partial<RoutesType>)) =>
|
||||
settingsRoutesByUpdate(prev => ({
|
||||
routes: {
|
||||
...prev.routes,
|
||||
...(typeof next === 'function' ? next(prev.routes) : next),
|
||||
},
|
||||
})),
|
||||
[settingsRoutesByUpdate],
|
||||
);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const compactSmall = useMaxWidth(ref, 180);
|
||||
const compactMiddle = useMaxWidth(ref, 245);
|
||||
|
||||
return (
|
||||
<RoutesWidget
|
||||
title={title}
|
||||
renderContent={(content /*, compact*/) => (
|
||||
<div className="h-full grid grid-rows-[1fr_auto]" ref={ref}>
|
||||
{content}
|
||||
<div className="flex items-center gap-2 justify-end mb-2 px-2 pt-2">
|
||||
{!compactSmall && (
|
||||
<Dropdown
|
||||
value={securityType}
|
||||
options={ROUTES_BY_SECURITY_OPTIONS}
|
||||
onChange={e => settingsRoutesByUpdate({ scope: e.value as RoutesBySecurityType })}
|
||||
className="w-[90px] [&_span]:!text-[12px]"
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
value={criteriaType}
|
||||
itemTemplate={e => (
|
||||
<div className="flex items-center gap-2">
|
||||
{e.icon && <img src={e.icon} height="18" width="18" />}
|
||||
<span className="text-[12px]">{e.label}</span>
|
||||
</div>
|
||||
)}
|
||||
valueTemplate={e => {
|
||||
if (!e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (compactMiddle) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-[50px]">
|
||||
{e.icon ? <img src={e.icon} height="18" width="18" /> : <span>{e.label}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{e.icon && <img src={e.icon} height="18" width="18" />}
|
||||
<span className="text-[12px]">{e.label}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
options={ROUTES_BY_OPTIONS}
|
||||
onChange={e => settingsRoutesByUpdate({ type: e.value as RoutesByType })}
|
||||
className={clsx({
|
||||
['w-[130px]']: !compactMiddle,
|
||||
['w-[65px]']: compactMiddle,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
data={routesSettings}
|
||||
update={updateRoutesSettings}
|
||||
hubs={hubs}
|
||||
routesList={routesListBy}
|
||||
loading={internalLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { WRoutesBy } from './WRoutesBy';
|
||||
export type { RoutesByType } from './WRoutesBy';
|
||||
@@ -6,4 +6,5 @@ export * from './SystemStructures';
|
||||
export * from './WSystemKills';
|
||||
export * from './WRoutesUser';
|
||||
export * from './WRoutesPublic';
|
||||
export * from './WRoutesBy';
|
||||
export * from './CommentsWidget';
|
||||
|
||||
@@ -38,9 +38,11 @@ export const OldSettingsDialog = () => {
|
||||
localWidget: createSettings(widgetLocal, {}),
|
||||
widgets: createSettings(widgetsOld, {}),
|
||||
routes: createSettings(widgetRoutes, {}),
|
||||
routesBy: createSettings(widgetRoutes, {}),
|
||||
onTheMap: createSettings(onTheMapOld, {}),
|
||||
signaturesWidget: createSettings(signatures, {}),
|
||||
interface: createSettings(interfaceSettings, {}),
|
||||
map: createSettings(null, { viewport: { zoom: 1, x: 0, y: 0 } }),
|
||||
};
|
||||
|
||||
if (asFile) {
|
||||
|
||||
@@ -13,6 +13,26 @@ export const renderK162Type = (option: K162Type) => {
|
||||
return renderNoValue();
|
||||
}
|
||||
|
||||
if (['c1_c2_c3', 'c4_c5'].includes(value)) {
|
||||
const arr = whClassName.split('_');
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 items-center">
|
||||
{arr.map(x => (
|
||||
<WHClassView
|
||||
key={x}
|
||||
classNameWh="!text-[11px] !font-bold"
|
||||
hideWhClassName
|
||||
hideTooltip
|
||||
whClassName={x}
|
||||
noOffset
|
||||
useShortTitle
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WHClassView
|
||||
classNameWh="!text-[11px] !font-bold"
|
||||
|
||||
@@ -13,7 +13,7 @@ export type SystemViewProps = {
|
||||
|
||||
export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomName, ...rest }: SystemViewProps) => {
|
||||
const memSystems = useMemo(() => [systemId], [systemId]);
|
||||
const { systems, loading } = useLoadSystemStatic({ systems: memSystems });
|
||||
const { systems, lastUpdateKey, loading } = useLoadSystemStatic({ systems: memSystems });
|
||||
|
||||
const {
|
||||
data: { systems: mapSystems },
|
||||
@@ -23,9 +23,10 @@ export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomN
|
||||
if (!systemId) {
|
||||
return customSystemInfo;
|
||||
}
|
||||
|
||||
return systems.get(parseInt(systemId));
|
||||
// eslint-disable-next-line
|
||||
}, [customSystemInfo, systemId, systems, loading]);
|
||||
}, [customSystemInfo, systemId, systems, lastUpdateKey, loading]);
|
||||
|
||||
const mapSystemInfo = useMemo(() => {
|
||||
if (!showCustomName) {
|
||||
|
||||
@@ -88,6 +88,16 @@ export const K162_TYPES: K162Type[] = [
|
||||
value: 'ns',
|
||||
whClassName: 'C248',
|
||||
},
|
||||
{
|
||||
label: 'C1/C2/C3',
|
||||
value: 'c1_c2_c3',
|
||||
whClassName: 'E004_D382_L477',
|
||||
},
|
||||
{
|
||||
label: 'C4/C5',
|
||||
value: 'c4_c5',
|
||||
whClassName: 'M001_L614',
|
||||
},
|
||||
{
|
||||
label: 'C1',
|
||||
value: 'c1',
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
MapUnionTypes,
|
||||
OutCommandHandler,
|
||||
SolarSystemConnection,
|
||||
StringBoolean,
|
||||
TrackingCharacter,
|
||||
UseCharactersCacheData,
|
||||
UseCommentsData,
|
||||
@@ -28,12 +27,14 @@ import {
|
||||
MapSettings,
|
||||
MapUserSettings,
|
||||
OnTheMapSettingsType,
|
||||
RoutesByType,
|
||||
RoutesType,
|
||||
} from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import {
|
||||
DEFAULT_KILLS_WIDGET_SETTINGS,
|
||||
DEFAULT_MAP_SETTINGS,
|
||||
DEFAULT_ON_THE_MAP_SETTINGS,
|
||||
DEFAULT_ROUTES_BY_SETTINGS,
|
||||
DEFAULT_ROUTES_SETTINGS,
|
||||
DEFAULT_WIDGET_LOCAL_SETTINGS,
|
||||
STORED_INTERFACE_DEFAULT_VALUES,
|
||||
@@ -76,6 +77,7 @@ const INITIAL_DATA: MapRootData = {
|
||||
userHubs: [],
|
||||
routes: undefined,
|
||||
userRoutes: undefined,
|
||||
routesListBy: undefined,
|
||||
kills: [],
|
||||
connections: [],
|
||||
detailedKills: {},
|
||||
@@ -132,6 +134,8 @@ export interface MapRootContextProps {
|
||||
setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>;
|
||||
settingsRoutes: RoutesType;
|
||||
settingsRoutesUpdate: Dispatch<SetStateAction<RoutesType>>;
|
||||
settingsRoutesBy: RoutesByType;
|
||||
settingsRoutesByUpdate: Dispatch<SetStateAction<RoutesByType>>;
|
||||
settingsLocal: LocalWidgetSettings;
|
||||
settingsLocalUpdate: Dispatch<SetStateAction<LocalWidgetSettings>>;
|
||||
settingsSignatures: SignatureSettingsType;
|
||||
@@ -179,6 +183,8 @@ const MapRootContext = createContext<MapRootContextProps>({
|
||||
setInterfaceSettings: () => null,
|
||||
settingsRoutes: DEFAULT_ROUTES_SETTINGS,
|
||||
settingsRoutesUpdate: () => null,
|
||||
settingsRoutesBy: { ...DEFAULT_ROUTES_BY_SETTINGS, routes: { ...DEFAULT_ROUTES_BY_SETTINGS.routes } },
|
||||
settingsRoutesByUpdate: () => null,
|
||||
settingsLocal: DEFAULT_WIDGET_LOCAL_SETTINGS,
|
||||
settingsLocalUpdate: () => null,
|
||||
settingsSignatures: DEFAULT_SIGNATURE_SETTINGS,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MiniMapPlacement,
|
||||
OnTheMapSettingsType,
|
||||
PingsPlacement,
|
||||
RoutesByType,
|
||||
RoutesType,
|
||||
} from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { DEFAULT_WIDGETS, STORED_VISIBLE_WIDGETS_DEFAULT } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
@@ -43,6 +44,12 @@ export const DEFAULT_WIDGET_LOCAL_SETTINGS: LocalWidgetSettings = {
|
||||
showShipName: false,
|
||||
};
|
||||
|
||||
export const DEFAULT_ROUTES_BY_SETTINGS: RoutesByType = {
|
||||
routes: DEFAULT_ROUTES_SETTINGS,
|
||||
scope: 'ALL',
|
||||
type: 'blueLoot',
|
||||
};
|
||||
|
||||
export const DEFAULT_ON_THE_MAP_SETTINGS: OnTheMapSettingsType = {
|
||||
hideOffline: false,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DEFAULT_KILLS_WIDGET_SETTINGS,
|
||||
DEFAULT_MAP_SETTINGS,
|
||||
DEFAULT_ON_THE_MAP_SETTINGS,
|
||||
DEFAULT_ROUTES_BY_SETTINGS,
|
||||
DEFAULT_ROUTES_SETTINGS,
|
||||
DEFAULT_WIDGET_LOCAL_SETTINGS,
|
||||
getDefaultWidgetProps,
|
||||
@@ -17,6 +18,11 @@ export const createWidgetSettings = <T>(settings: T) => {
|
||||
};
|
||||
|
||||
export const createDefaultStoredSettings = (): MapUserSettings => {
|
||||
const defaultRoutesBy = {
|
||||
...DEFAULT_ROUTES_BY_SETTINGS,
|
||||
routes: { ...DEFAULT_ROUTES_BY_SETTINGS.routes },
|
||||
};
|
||||
|
||||
return {
|
||||
version: STORED_SETTINGS_VERSION,
|
||||
migratedFromOld: false,
|
||||
@@ -24,6 +30,7 @@ export const createDefaultStoredSettings = (): MapUserSettings => {
|
||||
localWidget: createWidgetSettings(DEFAULT_WIDGET_LOCAL_SETTINGS),
|
||||
widgets: createWidgetSettings(getDefaultWidgetProps()),
|
||||
routes: createWidgetSettings(DEFAULT_ROUTES_SETTINGS),
|
||||
routesBy: createWidgetSettings(defaultRoutesBy),
|
||||
onTheMap: createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS),
|
||||
signaturesWidget: createWidgetSettings(DEFAULT_SIGNATURE_SETTINGS),
|
||||
interface: createWidgetSettings(STORED_INTERFACE_DEFAULT_VALUES),
|
||||
@@ -43,6 +50,11 @@ export const getDefaultSettingsByType = (type: SettingsTypes): SettingsWrapper<a
|
||||
return createWidgetSettings(getDefaultWidgetProps());
|
||||
case SettingsTypes.routes:
|
||||
return createWidgetSettings(DEFAULT_ROUTES_SETTINGS);
|
||||
case SettingsTypes.routesBy:
|
||||
return createWidgetSettings({
|
||||
...DEFAULT_ROUTES_BY_SETTINGS,
|
||||
routes: { ...DEFAULT_ROUTES_BY_SETTINGS.routes },
|
||||
});
|
||||
case SettingsTypes.onTheMap:
|
||||
return createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS);
|
||||
case SettingsTypes.signaturesWidget:
|
||||
|
||||
@@ -10,3 +10,4 @@ export * from './useCommandComments';
|
||||
export * from './useGetCacheCharacter';
|
||||
export * from './useCommandsActivity';
|
||||
export * from './useCommandPings';
|
||||
export * from './useCommandPingBlocked';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useToast } from '@/hooks/Mapper/ToastProvider';
|
||||
import { CommandPingBlocked } from '@/hooks/Mapper/types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useCommandPingBlocked = () => {
|
||||
const { show } = useToast();
|
||||
|
||||
const pingBlocked = useCallback(
|
||||
({ message }: CommandPingBlocked) => {
|
||||
show({
|
||||
severity: 'warn',
|
||||
summary: 'Cannot create ping',
|
||||
detail: message,
|
||||
life: 5000,
|
||||
});
|
||||
},
|
||||
[show],
|
||||
);
|
||||
|
||||
return { pingBlocked };
|
||||
};
|
||||
@@ -14,8 +14,8 @@ export const useCommandPings = () => {
|
||||
ref.current.update({ pings });
|
||||
}, []);
|
||||
|
||||
const pingCancelled = useCallback(({ type, id }: CommandPingCancelled) => {
|
||||
const newPings = ref.current.pings.filter(x => x.id !== id && x.type !== type);
|
||||
const pingCancelled = useCallback(({ id }: CommandPingCancelled) => {
|
||||
const newPings = ref.current.pings.filter(x => x.id !== id);
|
||||
ref.current.update({ pings: newPings });
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -112,3 +112,23 @@ export const useUserRoutes = () => {
|
||||
update({ userRoutes: value });
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useRoutesListBy = () => {
|
||||
const {
|
||||
update,
|
||||
data: { routesListBy },
|
||||
} = useMapRootState();
|
||||
|
||||
const ref = useRef({ update, routesListBy });
|
||||
ref.current = { update, routesListBy };
|
||||
|
||||
return useCallback((value: CommandRoutes) => {
|
||||
const { update, routesListBy } = ref.current;
|
||||
|
||||
if (areRoutesListsEqual(routesListBy, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
update({ routesListBy: value });
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -63,7 +63,6 @@ export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData =
|
||||
|
||||
const removeComment = useCallback((systemId: number, commentId: string) => {
|
||||
const cSystem = commentBySystemsRef.current.get(systemId);
|
||||
console.log('cSystem', cSystem);
|
||||
if (!cSystem) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CommandLinkSignatureToSystem,
|
||||
CommandMapUpdated,
|
||||
CommandPingAdded,
|
||||
CommandPingBlocked,
|
||||
CommandPingCancelled,
|
||||
CommandPresentCharacters,
|
||||
CommandRemoveConnections,
|
||||
@@ -29,6 +30,7 @@ import { ForwardedRef, useImperativeHandle } from 'react';
|
||||
|
||||
import {
|
||||
useCommandComments,
|
||||
useCommandPingBlocked,
|
||||
useCommandPings,
|
||||
useCommandsCharacters,
|
||||
useCommandsConnections,
|
||||
@@ -36,6 +38,7 @@ import {
|
||||
useMapInit,
|
||||
useMapUpdated,
|
||||
useRoutes,
|
||||
useRoutesListBy,
|
||||
useUserRoutes,
|
||||
} from './api';
|
||||
|
||||
@@ -59,8 +62,10 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const mapUpdated = useMapUpdated();
|
||||
const mapRoutes = useRoutes();
|
||||
const mapUserRoutes = useUserRoutes();
|
||||
const mapRoutesListBy = useRoutesListBy();
|
||||
const { addComment, removeComment } = useCommandComments();
|
||||
const { pingAdded, pingCancelled } = useCommandPings();
|
||||
const { pingBlocked } = useCommandPingBlocked();
|
||||
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
@@ -112,6 +117,9 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
case Commands.userRoutes:
|
||||
mapUserRoutes(data as CommandRoutes);
|
||||
break;
|
||||
case Commands.routesListBy:
|
||||
mapRoutesListBy(data as CommandRoutes);
|
||||
break;
|
||||
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
@@ -172,6 +180,9 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
case Commands.pingCancelled:
|
||||
pingCancelled(data as CommandPingCancelled);
|
||||
break;
|
||||
case Commands.pingBlocked:
|
||||
pingBlocked(data as CommandPingBlocked);
|
||||
break;
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
|
||||
@@ -56,6 +56,12 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
|
||||
map_slug,
|
||||
'routes',
|
||||
);
|
||||
const [settingsRoutesBy, settingsRoutesByUpdate] = useSettingsValueAndSetter(
|
||||
mapUserSettings,
|
||||
setMapUserSettings,
|
||||
map_slug,
|
||||
'routesBy',
|
||||
);
|
||||
|
||||
const [settingsLocal, settingsLocalUpdate] = useSettingsValueAndSetter(
|
||||
mapUserSettings,
|
||||
@@ -188,6 +194,8 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
|
||||
setInterfaceSettings,
|
||||
settingsRoutes,
|
||||
settingsRoutesUpdate,
|
||||
settingsRoutesBy,
|
||||
settingsRoutesByUpdate,
|
||||
settingsLocal,
|
||||
settingsLocalUpdate,
|
||||
settingsSignatures,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { to_1 } from './to_1.ts';
|
||||
import { to_2 } from './to_2.ts';
|
||||
import { to_3 } from './to_3.ts';
|
||||
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
|
||||
export default [to_1, to_2] as MigrationStructure[];
|
||||
export default [to_1, to_2, to_3] as MigrationStructure[];
|
||||
|
||||
@@ -47,6 +47,22 @@ export type RoutesType = {
|
||||
avoid: number[];
|
||||
};
|
||||
|
||||
export type RoutesByCategoryType =
|
||||
| 'blueLoot'
|
||||
| 'redLoot'
|
||||
| 'thera'
|
||||
| 'turnur'
|
||||
| 'so_cleaning'
|
||||
| 'trade_hubs';
|
||||
|
||||
export type RoutesByScopeType = 'ALL' | 'HIGH';
|
||||
|
||||
export type RoutesByType = {
|
||||
routes: RoutesType;
|
||||
scope: RoutesByScopeType;
|
||||
type: RoutesByCategoryType;
|
||||
};
|
||||
|
||||
export type LocalWidgetSettings = {
|
||||
compact: boolean;
|
||||
showOffline: boolean;
|
||||
@@ -79,6 +95,7 @@ export type MapUserSettings = {
|
||||
interface: SettingsWrapper<InterfaceStoredSettings>;
|
||||
onTheMap: SettingsWrapper<OnTheMapSettingsType>;
|
||||
routes: SettingsWrapper<RoutesType>;
|
||||
routesBy: SettingsWrapper<RoutesByType>;
|
||||
localWidget: SettingsWrapper<LocalWidgetSettings>;
|
||||
signaturesWidget: SettingsWrapper<SignatureSettingsType>;
|
||||
killsWidget: SettingsWrapper<KillsWidgetSettings>;
|
||||
@@ -98,6 +115,7 @@ export enum SettingsTypes {
|
||||
localWidget = 'localWidget',
|
||||
widgets = 'widgets',
|
||||
routes = 'routes',
|
||||
routesBy = 'routesBy',
|
||||
onTheMap = 'onTheMap',
|
||||
signaturesWidget = 'signaturesWidget',
|
||||
interface = 'interface',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const STORED_SETTINGS_VERSION = 2;
|
||||
export const STORED_SETTINGS_VERSION = 3;
|
||||
|
||||
export const LS_KEY_LEGASY = 'map-user-settings';
|
||||
export const LS_KEY = 'map-user-settings-v3';
|
||||
|
||||
@@ -25,6 +25,7 @@ export enum Commands {
|
||||
detailedKillsUpdated = 'detailed_kills_updated',
|
||||
routes = 'routes',
|
||||
userRoutes = 'user_routes',
|
||||
routesListBy = 'routes_list_by',
|
||||
centerSystem = 'center_system',
|
||||
selectSystem = 'select_system',
|
||||
selectSystems = 'select_systems',
|
||||
@@ -41,6 +42,7 @@ export enum Commands {
|
||||
refreshTrackingData = 'refresh_tracking_data',
|
||||
pingAdded = 'ping_added',
|
||||
pingCancelled = 'ping_cancelled',
|
||||
pingBlocked = 'ping_blocked',
|
||||
}
|
||||
|
||||
export type Command =
|
||||
@@ -61,6 +63,7 @@ export type Command =
|
||||
| Commands.detailedKillsUpdated
|
||||
| Commands.routes
|
||||
| Commands.userRoutes
|
||||
| Commands.routesListBy
|
||||
| Commands.selectSystem
|
||||
| Commands.selectSystems
|
||||
| Commands.centerSystem
|
||||
@@ -77,7 +80,8 @@ export type Command =
|
||||
| Commands.showTracking
|
||||
| Commands.refreshTrackingData
|
||||
| Commands.pingAdded
|
||||
| Commands.pingCancelled;
|
||||
| Commands.pingCancelled
|
||||
| Commands.pingBlocked;
|
||||
|
||||
export type CommandInit = {
|
||||
systems: SolarSystemRawType[];
|
||||
@@ -119,6 +123,7 @@ export type CommandSignaturesUpdated = string;
|
||||
export type CommandMapUpdated = Partial<CommandInit>;
|
||||
export type CommandRoutes = RoutesList;
|
||||
export type CommandUserRoutes = RoutesList;
|
||||
export type CommandRoutesListBy = RoutesList;
|
||||
export type CommandKillsUpdated = Kill[];
|
||||
export type CommandDetailedKillsUpdated = Record<string, DetailedKill[]>;
|
||||
export type CommandSelectSystem = string | undefined;
|
||||
@@ -161,6 +166,10 @@ export type CommandUpdateTracking = {
|
||||
};
|
||||
export type CommandPingAdded = PingData[];
|
||||
export type CommandPingCancelled = Pick<PingData, 'type' | 'id'>;
|
||||
export type CommandPingBlocked = {
|
||||
reason: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export interface UserSettings {
|
||||
primaryCharacterId?: string;
|
||||
@@ -193,6 +202,7 @@ export interface CommandData {
|
||||
[Commands.mapUpdated]: CommandMapUpdated;
|
||||
[Commands.routes]: CommandRoutes;
|
||||
[Commands.userRoutes]: CommandUserRoutes;
|
||||
[Commands.routesListBy]: CommandRoutesListBy;
|
||||
[Commands.killsUpdated]: CommandKillsUpdated;
|
||||
[Commands.detailedKillsUpdated]: CommandDetailedKillsUpdated;
|
||||
[Commands.selectSystem]: CommandSelectSystem;
|
||||
@@ -212,6 +222,7 @@ export interface CommandData {
|
||||
[Commands.refreshTrackingData]: CommandRefreshTrackingData;
|
||||
[Commands.pingAdded]: CommandPingAdded;
|
||||
[Commands.pingCancelled]: CommandPingCancelled;
|
||||
[Commands.pingBlocked]: CommandPingBlocked;
|
||||
}
|
||||
|
||||
export interface MapHandlers {
|
||||
@@ -225,6 +236,7 @@ export enum OutCommand {
|
||||
deleteUserHub = 'delete_user_hub',
|
||||
getRoutes = 'get_routes',
|
||||
getUserRoutes = 'get_user_routes',
|
||||
getRoutesBy = 'get_routes_by',
|
||||
getCharacterJumps = 'get_character_jumps',
|
||||
getStructures = 'get_structures',
|
||||
getSignatures = 'get_signatures',
|
||||
|
||||
@@ -20,6 +20,7 @@ export type MapUnionTypes = {
|
||||
systemSignatures: Record<string, SystemSignature[]>;
|
||||
routes?: RoutesList;
|
||||
userRoutes?: RoutesList;
|
||||
routesListBy?: RoutesList;
|
||||
kills: Record<number, number>;
|
||||
connections: SolarSystemConnection[];
|
||||
userPermissions: Partial<UserPermissions>;
|
||||
|
||||
@@ -13,12 +13,19 @@ export type SystemStaticInfoShort = Pick<
|
||||
|
||||
type MappedSystem = SolarSystemStaticInfoRaw | undefined;
|
||||
|
||||
export type RouteStationSummary = {
|
||||
station_id: number;
|
||||
station_name: string;
|
||||
special?: boolean;
|
||||
};
|
||||
|
||||
export type Route = {
|
||||
destination: number;
|
||||
has_connection: boolean;
|
||||
origin: number;
|
||||
systems?: number[];
|
||||
mapped_systems?: MappedSystem[];
|
||||
stations?: RouteStationSummary[];
|
||||
success?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export default {
|
||||
};
|
||||
|
||||
refreshZone.addEventListener('click', handleUpdate);
|
||||
refreshZone.addEventListener('mouseover', handleUpdate);
|
||||
// refreshZone.addEventListener('mouseover', handleUpdate);
|
||||
|
||||
this.updated();
|
||||
},
|
||||
|
||||
BIN
assets/static/images/news/2026/01-01-roadmap/cover.webp
Normal file
BIN
assets/static/images/news/2026/01-01-roadmap/cover.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
assets/static/images/news/2026/01-05-weekly-giveaway/cover.webp
Normal file
BIN
assets/static/images/news/2026/01-05-weekly-giveaway/cover.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -67,6 +67,10 @@ wanderer_kills_base_url =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_KILLS_BASE_URL", "ws://wanderer-kills:4004")
|
||||
|
||||
route_builder_base_url =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_ROUTE_BUILDER_BASE_URL", "http://localhost:2001")
|
||||
|
||||
map_subscriptions_enabled =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_MAP_SUBSCRIPTIONS_ENABLED", "false")
|
||||
@@ -92,6 +96,31 @@ map_subscription_extra_hubs_10_price =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_HUBS_10_PRICE", 10_000_000)
|
||||
|
||||
# Parse promo codes from environment variable
|
||||
# Format: "CODE1:10,CODE2:20" where numbers are discount percentages
|
||||
promo_codes =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_PROMO_CODES", "")
|
||||
|> case do
|
||||
"" ->
|
||||
%{}
|
||||
|
||||
codes_string ->
|
||||
codes_string
|
||||
|> String.split(",")
|
||||
|> Enum.map(fn entry ->
|
||||
case String.split(String.trim(entry), ":") do
|
||||
[code, discount] ->
|
||||
{String.upcase(String.trim(code)), String.to_integer(String.trim(discount))}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
map_connection_auto_expire_hours =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_AUTO_EXPIRE_HOURS", 24)
|
||||
@@ -149,6 +178,7 @@ config :wanderer_app,
|
||||
character_api_disabled: character_api_disabled,
|
||||
wanderer_kills_service_enabled: wanderer_kills_service_enabled,
|
||||
wanderer_kills_base_url: wanderer_kills_base_url,
|
||||
route_builder_base_url: route_builder_base_url,
|
||||
map_subscriptions_enabled: map_subscriptions_enabled,
|
||||
map_connection_auto_expire_hours: map_connection_auto_expire_hours,
|
||||
map_connection_auto_eol_hours: map_connection_auto_eol_hours,
|
||||
@@ -176,7 +206,8 @@ config :wanderer_app,
|
||||
}
|
||||
],
|
||||
extra_characters_50: map_subscription_extra_characters_50_price,
|
||||
extra_hubs_10: map_subscription_extra_hubs_10_price
|
||||
extra_hubs_10: map_subscription_extra_hubs_10_price,
|
||||
promo_codes: promo_codes
|
||||
},
|
||||
# Finch pool configuration - separate pools for different services
|
||||
# ESI Character Tracking pool - high capacity for bulk character operations
|
||||
@@ -264,7 +295,7 @@ config :logger,
|
||||
case config_env() do
|
||||
:prod -> "info"
|
||||
:dev -> "info"
|
||||
:test -> "debug"
|
||||
:test -> "warning"
|
||||
end
|
||||
)
|
||||
)
|
||||
|
||||
@@ -16,6 +16,11 @@ defmodule WandererApp.Api.AccessList do
|
||||
|
||||
includes([:owner, :members])
|
||||
|
||||
default_fields([
|
||||
:name,
|
||||
:description
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -79,12 +84,15 @@ defmodule WandererApp.Api.AccessList do
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :description, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
# Note: api_key intentionally not public for security
|
||||
attribute :api_key, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
@@ -16,6 +16,14 @@ defmodule WandererApp.Api.AccessListMember do
|
||||
|
||||
includes([:access_list])
|
||||
|
||||
default_fields([
|
||||
:name,
|
||||
:eve_character_id,
|
||||
:eve_corporation_id,
|
||||
:eve_alliance_id,
|
||||
:role
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -89,22 +97,27 @@ defmodule WandererApp.Api.AccessListMember do
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :eve_character_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :eve_corporation_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :eve_alliance_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :role, :atom do
|
||||
default "viewer"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
|
||||
@@ -19,9 +19,10 @@ defmodule WandererApp.Api.Changes.InjectMapFromActor do
|
||||
_other ->
|
||||
# nil or unexpected return shape - check for direct map_id
|
||||
# Check params (input), arguments, and attributes (in that order)
|
||||
map_id = Map.get(changeset.params, :map_id) ||
|
||||
Ash.Changeset.get_argument(changeset, :map_id) ||
|
||||
Ash.Changeset.get_attribute(changeset, :map_id)
|
||||
map_id =
|
||||
Map.get(changeset.params, :map_id) ||
|
||||
Ash.Changeset.get_argument(changeset, :map_id) ||
|
||||
Ash.Changeset.get_attribute(changeset, :map_id)
|
||||
|
||||
case map_id do
|
||||
nil ->
|
||||
|
||||
@@ -13,6 +13,8 @@ defmodule WandererApp.Api.Map do
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("maps_v1")
|
||||
|
||||
migration_defaults scopes: "'{wormholes}'"
|
||||
end
|
||||
|
||||
json_api do
|
||||
@@ -65,6 +67,8 @@ defmodule WandererApp.Api.Map do
|
||||
)
|
||||
|
||||
define(:duplicate, action: :duplicate)
|
||||
define(:admin_all, action: :admin_all)
|
||||
define(:restore, action: :restore)
|
||||
end
|
||||
|
||||
calculations do
|
||||
@@ -105,12 +109,19 @@ defmodule WandererApp.Api.Map do
|
||||
prepare WandererApp.Api.Preparations.FilterMapsByRoles
|
||||
end
|
||||
|
||||
read :admin_all do
|
||||
# Admin-only action that bypasses FilterMapsByRoles
|
||||
# Returns ALL maps including soft-deleted ones with owner and ACLs loaded
|
||||
prepare build(load: [:owner, :acls])
|
||||
end
|
||||
|
||||
create :new do
|
||||
accept [
|
||||
:name,
|
||||
:slug,
|
||||
:description,
|
||||
:scope,
|
||||
:scopes,
|
||||
:only_tracked_characters,
|
||||
:owner_id,
|
||||
:sse_enabled
|
||||
@@ -135,6 +146,7 @@ defmodule WandererApp.Api.Map do
|
||||
:slug,
|
||||
:description,
|
||||
:scope,
|
||||
:scopes,
|
||||
:only_tracked_characters,
|
||||
:owner_id,
|
||||
:sse_enabled
|
||||
@@ -190,6 +202,14 @@ defmodule WandererApp.Api.Map do
|
||||
change(set_attribute(:deleted, true))
|
||||
end
|
||||
|
||||
update :restore do
|
||||
# Admin-only action to restore a soft-deleted map
|
||||
accept([])
|
||||
require_atomic? false
|
||||
|
||||
change(set_attribute(:deleted, false))
|
||||
end
|
||||
|
||||
update :update_api_key do
|
||||
accept [:public_api_key]
|
||||
require_atomic? false
|
||||
@@ -209,7 +229,7 @@ defmodule WandererApp.Api.Map do
|
||||
end
|
||||
|
||||
create :duplicate do
|
||||
accept [:name, :description, :scope, :only_tracked_characters]
|
||||
accept [:name, :description, :scope, :scopes, :only_tracked_characters]
|
||||
argument :source_map_id, :uuid, allow_nil?: false
|
||||
argument :copy_acls, :boolean, default: true
|
||||
argument :copy_user_settings, :boolean, default: true
|
||||
@@ -225,9 +245,14 @@ defmodule WandererApp.Api.Map do
|
||||
description =
|
||||
Ash.Changeset.get_attribute(changeset, :description) || source_map.description
|
||||
|
||||
# Use provided scopes or fall back to source map scopes
|
||||
scopes =
|
||||
Ash.Changeset.get_attribute(changeset, :scopes) || source_map.scopes
|
||||
|
||||
changeset
|
||||
|> Ash.Changeset.change_attribute(:description, description)
|
||||
|> Ash.Changeset.change_attribute(:scope, source_map.scope)
|
||||
|> Ash.Changeset.change_attribute(:scopes, scopes)
|
||||
|> Ash.Changeset.change_attribute(
|
||||
:only_tracked_characters,
|
||||
source_map.only_tracked_characters
|
||||
@@ -359,6 +384,24 @@ defmodule WandererApp.Api.Map do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :scopes, {:array, :atom} do
|
||||
default([:wormholes])
|
||||
allow_nil?(true)
|
||||
public?(true)
|
||||
|
||||
constraints(
|
||||
items: [
|
||||
one_of: [
|
||||
:wormholes,
|
||||
:hi,
|
||||
:low,
|
||||
:null,
|
||||
:pochven
|
||||
]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
end
|
||||
|
||||
@@ -27,6 +27,11 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
|
||||
includes([:map, :character])
|
||||
|
||||
default_fields([
|
||||
:tracked,
|
||||
:followed
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -219,14 +224,17 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
|
||||
attribute :tracked, :boolean do
|
||||
default false
|
||||
public? true
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :followed, :boolean do
|
||||
default false
|
||||
public? true
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
# Note: These attributes are encrypted (AshCloak) and intentionally not public
|
||||
attribute :solar_system_id, :integer
|
||||
attribute :structure_id, :integer
|
||||
attribute :station_id, :integer
|
||||
|
||||
@@ -22,6 +22,19 @@ defmodule WandererApp.Api.MapConnection do
|
||||
|
||||
includes([:map])
|
||||
|
||||
default_fields([
|
||||
:solar_system_source,
|
||||
:solar_system_target,
|
||||
:mass_status,
|
||||
:time_status,
|
||||
:ship_size_type,
|
||||
:type,
|
||||
:wormhole_type,
|
||||
:count_of_passage,
|
||||
:locked,
|
||||
:custom_info
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -197,15 +210,20 @@ defmodule WandererApp.Api.MapConnection do
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :solar_system_source, :integer
|
||||
attribute :solar_system_target, :integer
|
||||
attribute :solar_system_source, :integer do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :solar_system_target, :integer do
|
||||
public? true
|
||||
end
|
||||
|
||||
# where 0 - greater than half
|
||||
# where 1 - less than half
|
||||
# where 2 - critical less than 10%
|
||||
attribute :mass_status, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
@@ -218,7 +236,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
# 6 - EOL 48h
|
||||
attribute :time_status, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
@@ -229,7 +247,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
# where 4 - Capital
|
||||
attribute :ship_size_type, :integer do
|
||||
default(2)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
@@ -238,21 +256,26 @@ defmodule WandererApp.Api.MapConnection do
|
||||
# where 2 - Bridge
|
||||
attribute :type, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :wormhole_type, :string
|
||||
attribute :wormhole_type, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :count_of_passage, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :locked, :boolean
|
||||
attribute :locked, :boolean do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :custom_info, :string do
|
||||
public? true
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ defmodule WandererApp.Api.MapDefaultSettings do
|
||||
:updated_by
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:settings
|
||||
])
|
||||
|
||||
routes do
|
||||
base("/map_default_settings")
|
||||
|
||||
@@ -93,6 +97,7 @@ defmodule WandererApp.Api.MapDefaultSettings do
|
||||
|
||||
attribute :settings, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
constraints min_length: 2
|
||||
description "JSON string containing the default map settings"
|
||||
end
|
||||
|
||||
@@ -80,6 +80,10 @@ defmodule WandererApp.Api.MapPing do
|
||||
|
||||
filter(expr(inserted_at <= ^arg(:inserted_before)))
|
||||
end
|
||||
|
||||
# Admin action for cleanup - no actor filtering
|
||||
read :all_pings do
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
||||
@@ -18,6 +18,15 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
:map
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:plan,
|
||||
:status,
|
||||
:characters_limit,
|
||||
:hubs_limit,
|
||||
:active_till,
|
||||
:auto_renew?
|
||||
])
|
||||
|
||||
# Enable automatic filtering and sorting
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
@@ -135,6 +144,7 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
attribute :plan, :atom do
|
||||
default "alpha"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -150,6 +160,7 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
attribute :status, :atom do
|
||||
default "active"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -164,22 +175,24 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
attribute :characters_limit, :integer do
|
||||
default(100)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :hubs_limit, :integer do
|
||||
default(10)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :active_till, :utc_datetime do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :auto_renew?, :boolean do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
|
||||
@@ -19,6 +19,10 @@ defmodule WandererApp.Api.MapSystemComment do
|
||||
:character
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:text
|
||||
])
|
||||
|
||||
routes do
|
||||
base("/map_system_comments")
|
||||
|
||||
@@ -73,6 +77,7 @@ defmodule WandererApp.Api.MapSystemComment do
|
||||
|
||||
attribute :text, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
|
||||
@@ -16,6 +16,20 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
|
||||
includes([:system])
|
||||
|
||||
default_fields([
|
||||
:eve_id,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:description,
|
||||
:temporary_name,
|
||||
:type,
|
||||
:linked_system_id,
|
||||
:kind,
|
||||
:group,
|
||||
:custom_info,
|
||||
:deleted
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -184,42 +198,56 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
|
||||
attribute :eve_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :character_eve_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :description, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :temporary_name, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :type, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :linked_system_id, :integer do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :kind, :string
|
||||
attribute :group, :string
|
||||
attribute :kind, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :group, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :custom_info, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :deleted, :boolean do
|
||||
allow_nil? false
|
||||
default false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :update_forced_at, :utc_datetime do
|
||||
|
||||
@@ -41,6 +41,21 @@ defmodule WandererApp.Api.MapSystemStructure do
|
||||
:system
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
])
|
||||
|
||||
# Enable automatic filtering and sorting
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
@@ -151,50 +166,62 @@ defmodule WandererApp.Api.MapSystemStructure do
|
||||
|
||||
attribute :structure_type_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :structure_type, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :character_eve_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :solar_system_name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :solar_system_id, :integer do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :notes, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :owner_name, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :owner_ticker, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :owner_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :status, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :end_time, :utc_datetime_usec do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp :inserted_at
|
||||
|
||||
@@ -24,6 +24,13 @@ defmodule WandererApp.Api.MapUserSettings do
|
||||
:user
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:settings,
|
||||
:main_character_eve_id,
|
||||
:following_character_eve_id,
|
||||
:hubs
|
||||
])
|
||||
|
||||
routes do
|
||||
base("/map_user_settings")
|
||||
|
||||
@@ -85,19 +92,22 @@ defmodule WandererApp.Api.MapUserSettings do
|
||||
|
||||
attribute :settings, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :main_character_eve_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :following_character_eve_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :hubs, {:array, :string} do
|
||||
allow_nil?(true)
|
||||
|
||||
public? true
|
||||
default([])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,6 +31,13 @@ defmodule WandererApp.Api.UserActivity do
|
||||
|
||||
includes([:character, :user])
|
||||
|
||||
default_fields([
|
||||
:entity_id,
|
||||
:entity_type,
|
||||
:event_type,
|
||||
:event_data
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -86,10 +93,12 @@ defmodule WandererApp.Api.UserActivity do
|
||||
|
||||
attribute :entity_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :entity_type, :atom do
|
||||
default "map"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -104,6 +113,7 @@ defmodule WandererApp.Api.UserActivity do
|
||||
|
||||
attribute :event_type, :atom do
|
||||
default "custom"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -153,7 +163,9 @@ defmodule WandererApp.Api.UserActivity do
|
||||
allow_nil?(false)
|
||||
end
|
||||
|
||||
attribute :event_data, :string
|
||||
attribute :event_data, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
|
||||
@@ -112,6 +112,7 @@ defmodule WandererApp.Application do
|
||||
WandererApp.Scheduler,
|
||||
WandererApp.Server.ServerStatusTracker,
|
||||
WandererApp.Server.TheraDataFetcher,
|
||||
WandererApp.Server.TurnurDataFetcher,
|
||||
{WandererApp.Character.TrackerPoolSupervisor, []},
|
||||
{WandererApp.Map.MapPoolSupervisor, []},
|
||||
WandererApp.Character.TrackerManager,
|
||||
|
||||
@@ -45,7 +45,7 @@ defmodule WandererApp.Cache do
|
||||
def insert({id, key}, value, opts) when is_binary(id) and (is_binary(key) or is_atom(key)),
|
||||
do: insert("#{id}:#{key}", value, opts)
|
||||
|
||||
def insert(key, nil, opts) when is_binary(key) or is_atom(key), do: delete(key)
|
||||
def insert(key, nil, _opts) when is_binary(key) or is_atom(key), do: delete(key)
|
||||
def insert(key, value, opts) when is_binary(key) or is_atom(key), do: put(key, value, opts)
|
||||
|
||||
def insert_or_update(key, value, update_fn, opts \\ [])
|
||||
|
||||
@@ -598,9 +598,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
{:error, :skipped}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :skipped}
|
||||
end
|
||||
|
||||
_ ->
|
||||
@@ -734,6 +731,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:character_alliance, {character_id, character_update}}
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
# This ensures users are kicked off maps they no longer have access to
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{alliance_id: nil})
|
||||
end
|
||||
@@ -772,6 +777,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:character_alliance, {character_id, character_update}}
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
# This ensures users are kicked off maps they no longer have access to
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{alliance_id: alliance_id})
|
||||
|
||||
@@ -799,7 +812,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
corporation_id
|
||||
|> WandererApp.Esi.get_corporation_info()
|
||||
|> case do
|
||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
|
||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker}} ->
|
||||
{:ok, character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
@@ -826,6 +839,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
}}}
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
# This ensures users are kicked off maps they no longer have access to
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{corporation_id: corporation_id})
|
||||
|
||||
@@ -1002,7 +1023,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
defp maybe_update_active_maps(
|
||||
%{character_id: character_id, active_maps: active_maps} =
|
||||
state,
|
||||
%{map_id: map_id, track: true} = track_settings
|
||||
%{map_id: map_id, track: true}
|
||||
) do
|
||||
if not Enum.member?(active_maps, map_id) do
|
||||
WandererApp.Cache.put(
|
||||
|
||||
@@ -40,10 +40,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
|
||||
Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
|
||||
|
||||
Logger.debug("[TrackerManager] Initialized with intervals: " <>
|
||||
"garbage_collection=#{div(@garbage_collection_interval, 60_000)}min, " <>
|
||||
"untrack=#{div(@untrack_characters_interval, 60_000)}min, " <>
|
||||
"inactive_timeout=#{div(@inactive_character_timeout, 60_000)}min")
|
||||
Logger.debug(
|
||||
"[TrackerManager] Initialized with intervals: " <>
|
||||
"garbage_collection=#{div(@garbage_collection_interval, 60_000)}min, " <>
|
||||
"untrack=#{div(@untrack_characters_interval, 60_000)}min, " <>
|
||||
"inactive_timeout=#{div(@inactive_character_timeout, 60_000)}min"
|
||||
)
|
||||
|
||||
%{
|
||||
characters: [],
|
||||
@@ -57,7 +59,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
WandererApp.Cache.insert("tracked_characters", [])
|
||||
|
||||
if length(tracked_characters) > 0 do
|
||||
Logger.debug("[TrackerManager] Restoring #{length(tracked_characters)} tracked characters from cache")
|
||||
Logger.debug(
|
||||
"[TrackerManager] Restoring #{length(tracked_characters)} tracked characters from cache"
|
||||
)
|
||||
end
|
||||
|
||||
tracked_characters
|
||||
@@ -197,6 +201,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
[],
|
||||
fn untrack_queue ->
|
||||
original_length = length(untrack_queue)
|
||||
|
||||
filtered =
|
||||
untrack_queue
|
||||
|> Enum.reject(fn {m_id, c_id} -> m_id == map_id and c_id == character_id end)
|
||||
|
||||
@@ -88,15 +88,4 @@ defmodule WandererApp.Character.TrackerPoolDynamicSupervisor do
|
||||
{:ok, pid}
|
||||
end
|
||||
end
|
||||
|
||||
defp stop_child(uuid) do
|
||||
case Registry.lookup(@registry, uuid) do
|
||||
[{pid, _}] ->
|
||||
GenServer.cast(pid, :stop)
|
||||
|
||||
_ ->
|
||||
Logger.warn("Unable to locate pool assigned to #{inspect(uuid)}")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,7 +38,7 @@ defmodule WandererApp.Character.TrackingConfigUtils do
|
||||
%{id: "default", title: "Default", value: default_count}
|
||||
]
|
||||
|
||||
{:ok, pools_count} =
|
||||
{:ok, _pools_count} =
|
||||
Cachex.get(
|
||||
:esi_auth_cache,
|
||||
"configs_total_count"
|
||||
|
||||
@@ -56,13 +56,7 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
Only includes characters that have actual tracking permission.
|
||||
"""
|
||||
def build_tracking_data(map_id, current_user_id) do
|
||||
with {:ok, map} <-
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
),
|
||||
with {:ok, map} <- WandererApp.MapRepo.get(map_id),
|
||||
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
|
||||
{:ok, %{characters: characters_with_access}} <-
|
||||
WandererApp.Maps.load_characters(map, current_user_id) do
|
||||
@@ -75,7 +69,11 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
build_character_tracking_data(characters_with_tracking_permission)
|
||||
|
||||
{:ok, main_character} =
|
||||
get_main_character(user_settings, characters_with_tracking_permission, characters_with_tracking_permission)
|
||||
get_main_character(
|
||||
user_settings,
|
||||
characters_with_tracking_permission,
|
||||
characters_with_tracking_permission
|
||||
)
|
||||
|
||||
following_character_eve_id =
|
||||
case user_settings do
|
||||
@@ -195,7 +193,13 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
{true, settings_result} ->
|
||||
case check_character_tracking_permission(character, map_id) do
|
||||
{:ok, :allowed} ->
|
||||
do_update_character_tracking_impl(character, map_id, track, caller_pid, settings_result)
|
||||
do_update_character_tracking_impl(
|
||||
character,
|
||||
map_id,
|
||||
track,
|
||||
caller_pid,
|
||||
settings_result
|
||||
)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning(
|
||||
|
||||
@@ -14,6 +14,7 @@ defmodule WandererApp.Env do
|
||||
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 route_builder_base_url(), do: get_key(:route_builder_base_url, "http://localhost:2001")
|
||||
def invites(), do: get_key(:invites, false)
|
||||
|
||||
def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)
|
||||
@@ -42,6 +43,35 @@ defmodule WandererApp.Env do
|
||||
def corp_eve_id(), do: get_key(:corp_id, -1)
|
||||
def subscription_settings(), do: get_key(:subscription_settings)
|
||||
|
||||
@doc """
|
||||
Returns the promo code configuration map.
|
||||
Keys are uppercase code strings, values are discount percentages.
|
||||
"""
|
||||
def promo_codes() do
|
||||
case subscription_settings() do
|
||||
%{promo_codes: codes} when is_map(codes) -> codes
|
||||
_ -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates a promo code and returns the discount percentage.
|
||||
Returns {:ok, discount_percent} if valid, {:error, :invalid_code} otherwise.
|
||||
Codes are case-insensitive.
|
||||
"""
|
||||
def validate_promo_code(nil), do: {:error, :invalid_code}
|
||||
def validate_promo_code(""), do: {:error, :invalid_code}
|
||||
|
||||
def validate_promo_code(code) when is_binary(code) do
|
||||
normalized = String.upcase(String.trim(code))
|
||||
|
||||
case Map.get(promo_codes(), normalized) do
|
||||
nil -> {:error, :invalid_code}
|
||||
discount when is_integer(discount) and discount > 0 and discount <= 100 -> {:ok, discount}
|
||||
_ -> {:error, :invalid_code}
|
||||
end
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "restrict_maps_creation"
|
||||
|
||||
@@ -8,7 +8,6 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
@ttl :timer.hours(1)
|
||||
|
||||
@wanderrer_user_agent "(wanderer-industries@proton.me; +https://github.com/wanderer-industries/wanderer)"
|
||||
@req_esi_options [base_url: "https://esi.evetech.net", finch: WandererApp.Finch]
|
||||
|
||||
@cache_opts [cache: true]
|
||||
@retry_opts [retry: false, retry_log_level: :warning]
|
||||
@@ -74,7 +73,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|> Keyword.merge(@timeout_opts)
|
||||
)
|
||||
|
||||
def get_routes_eve(hubs, origin, params, opts),
|
||||
def get_routes_eve(hubs, origin, _params, _opts),
|
||||
do:
|
||||
{:ok,
|
||||
hubs
|
||||
@@ -101,33 +100,6 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end)}
|
||||
|
||||
defp do_get_routes_eve(origin, destination, params, opts) do
|
||||
esi_params =
|
||||
Map.merge(params, %{
|
||||
connections: params.connections |> Enum.join(","),
|
||||
avoid: params.avoid |> Enum.join(",")
|
||||
})
|
||||
|
||||
do_get(
|
||||
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
|> case do
|
||||
{:ok, result} ->
|
||||
%{
|
||||
"origin" => origin,
|
||||
"destination" => destination,
|
||||
"systems" => result,
|
||||
"success" => true
|
||||
}
|
||||
|
||||
error ->
|
||||
Logger.warning("Error getting routes: #{inspect(error)}")
|
||||
%{"origin" => origin, "destination" => destination, "systems" => [], "success" => false}
|
||||
end
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "group-info-#{group_id}",
|
||||
@@ -273,6 +245,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
defp get_search(character_eve_id, search_val, categories_val, merged_opts) do
|
||||
# Note: search_val and categories_val are used by the @decorate cacheable annotation above
|
||||
_unused = {search_val, categories_val}
|
||||
get_character_auth_data(character_eve_id, "search", merged_opts)
|
||||
end
|
||||
|
||||
@@ -348,7 +322,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
defp with_cache_opts(opts),
|
||||
do: opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
|
||||
|
||||
defp do_get(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
|
||||
defp do_get(path, api_opts, opts, pool \\ @general_pool) do
|
||||
case Cachex.get(:api_cache, path) do
|
||||
{:ok, cached_data} when not is_nil(cached_data) ->
|
||||
{:ok, cached_data}
|
||||
@@ -358,7 +332,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_request(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
|
||||
defp do_get_request(path, api_opts, opts, pool) do
|
||||
try do
|
||||
req_options_for_pool(pool)
|
||||
|> Req.new()
|
||||
@@ -448,7 +422,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
{:ok, %{status: status} = _error} when status in [401, 403] ->
|
||||
do_get_retry(path, api_opts, opts)
|
||||
|
||||
{:ok, %{status: status, headers: headers}} ->
|
||||
{:ok, %{status: status}} ->
|
||||
{:error, "Unexpected status: #{status}"}
|
||||
|
||||
{:error, %Mint.TransportError{reason: :timeout}} ->
|
||||
@@ -832,10 +806,10 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
defp handle_refresh_token_result(
|
||||
{:error, %OAuth2.Error{reason: :econnrefused} = error},
|
||||
character,
|
||||
_character,
|
||||
character_id,
|
||||
expires_at,
|
||||
scopes
|
||||
_scopes
|
||||
) do
|
||||
expires_at_datetime = DateTime.from_unix!(expires_at)
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
|
||||
|
||||
@@ -7,7 +7,8 @@ defmodule WandererApp.EveDataService do
|
||||
|
||||
alias WandererApp.Utils.JSONUtil
|
||||
|
||||
@eve_db_dump_url "https://www.fuzzwork.co.uk/dump/latest"
|
||||
# @eve_db_dump_url "https://www.fuzzwork.co.uk/dump/latest"
|
||||
@eve_db_dump_url "https://wanderer-industries.github.io/wanderer-assets/sde-files"
|
||||
|
||||
@dump_file_names [
|
||||
"invGroups.csv",
|
||||
@@ -393,9 +394,6 @@ defmodule WandererApp.EveDataService do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_solar_system_name(solar_system_name, wormhole_class) do
|
||||
end
|
||||
|
||||
defp get_triglavian_data(default_data, triglavian_systems, solar_system_id) do
|
||||
case Enum.find(triglavian_systems, fn system -> system.solar_system_id == solar_system_id end) do
|
||||
nil ->
|
||||
@@ -413,8 +411,12 @@ defmodule WandererApp.EveDataService do
|
||||
|
||||
defp get_security(security) do
|
||||
case security do
|
||||
nil -> {:ok, ""}
|
||||
_ -> {:ok, String.to_float(security) |> get_true_security() |> Float.to_string(decimals: 1)}
|
||||
nil ->
|
||||
{:ok, ""}
|
||||
|
||||
_ ->
|
||||
{:ok,
|
||||
String.to_float(security) |> get_true_security() |> :erlang.float_to_binary(decimals: 1)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -496,23 +498,23 @@ defmodule WandererApp.EveDataService do
|
||||
do: {:ok, 10_100}
|
||||
|
||||
defp get_wormhole_class_id(systems, region_id, constellation_id, solar_system_id) do
|
||||
with region <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == region_id
|
||||
end),
|
||||
constellation <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == constellation_id
|
||||
end),
|
||||
solar_system <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == solar_system_id
|
||||
end),
|
||||
wormhole_class_id <- get_wormhole_class_id(region, constellation, solar_system) do
|
||||
{:ok, wormhole_class_id}
|
||||
else
|
||||
_ -> {:ok, -1}
|
||||
end
|
||||
region =
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == region_id
|
||||
end)
|
||||
|
||||
constellation =
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == constellation_id
|
||||
end)
|
||||
|
||||
solar_system =
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == solar_system_id
|
||||
end)
|
||||
|
||||
wormhole_class_id = get_wormhole_class_id(region, constellation, solar_system)
|
||||
{:ok, wormhole_class_id}
|
||||
end
|
||||
|
||||
defp get_wormhole_class_id(_region, _constellation, solar_system)
|
||||
|
||||
@@ -178,6 +178,10 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
end
|
||||
end
|
||||
|
||||
defp serialize_payload(payload, visited) when is_map(payload) do
|
||||
Map.new(payload, fn {k, v} -> {to_string(k), serialize_value(v, visited)} end)
|
||||
end
|
||||
|
||||
# Get allowed fields based on struct type
|
||||
defp get_allowed_fields(module) do
|
||||
module_name = module |> Module.split() |> List.last()
|
||||
@@ -192,10 +196,6 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
end
|
||||
end
|
||||
|
||||
defp serialize_payload(payload, visited) when is_map(payload) do
|
||||
Map.new(payload, fn {k, v} -> {to_string(k), serialize_value(v, visited)} end)
|
||||
end
|
||||
|
||||
defp serialize_fields(fields, visited) do
|
||||
Enum.reduce(fields, %{}, fn {k, v}, acc ->
|
||||
if is_nil(v) do
|
||||
|
||||
@@ -98,8 +98,8 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
|
||||
"id" => payload["system_id"] || payload[:system_id],
|
||||
"attributes" => %{
|
||||
"locked" => payload["locked"] || payload[:locked],
|
||||
"x" => payload["x"] || payload[:x],
|
||||
"y" => payload["y"] || payload[:y],
|
||||
"position_x" => payload["position_x"] || payload[:position_x],
|
||||
"position_y" => payload["position_y"] || payload[:position_y],
|
||||
"updated_at" => event.timestamp
|
||||
},
|
||||
"relationships" => %{
|
||||
|
||||
@@ -182,7 +182,7 @@ defmodule WandererApp.Kills.Client do
|
||||
end
|
||||
|
||||
# Guard against duplicate disconnection events
|
||||
def handle_info({:disconnected, reason}, %{connected: false, connecting: false} = state) do
|
||||
def handle_info({:disconnected, _reason}, %{connected: false, connecting: false} = state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@@ -566,7 +566,7 @@ defmodule WandererApp.Kills.Client do
|
||||
end
|
||||
end
|
||||
|
||||
defp check_health(%{socket_pid: pid} = state) do
|
||||
defp check_health(%{socket_pid: pid}) do
|
||||
if socket_alive?(pid) do
|
||||
:healthy
|
||||
else
|
||||
@@ -590,22 +590,6 @@ defmodule WandererApp.Kills.Client do
|
||||
Process.send_after(self(), :health_check, @health_check_interval)
|
||||
end
|
||||
|
||||
defp handle_connection_lost(%{connected: false} = _state) do
|
||||
Logger.debug("[Client] Connection already lost, skipping cleanup")
|
||||
end
|
||||
|
||||
defp handle_connection_lost(state) do
|
||||
Logger.warning("[Client] Connection lost, cleaning up and reconnecting")
|
||||
|
||||
# Clean up existing socket
|
||||
if state.socket_pid do
|
||||
disconnect_socket(state.socket_pid)
|
||||
end
|
||||
|
||||
# Reset state and trigger reconnection
|
||||
send(self(), {:disconnected, :connection_lost})
|
||||
end
|
||||
|
||||
# Handler module for WebSocket events
|
||||
defmodule Handler do
|
||||
@moduledoc """
|
||||
@@ -640,7 +624,7 @@ defmodule WandererApp.Kills.Client do
|
||||
}
|
||||
|
||||
case GenSocketClient.join(transport, "killmails:lobby", join_params) do
|
||||
{:ok, response} ->
|
||||
{:ok, _response} ->
|
||||
send(state.parent, {:connected, self()})
|
||||
# Reset disconnected flag on successful connection
|
||||
{:ok, %{state | disconnected: false}}
|
||||
|
||||
@@ -46,7 +46,7 @@ defmodule WandererApp.Kills.MapEventListener do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(%{event: :map_server_started, payload: map_info}, state) do
|
||||
def handle_info(%{event: :map_server_started, payload: _map_info}, state) do
|
||||
{:noreply, schedule_subscription_update(state)}
|
||||
end
|
||||
|
||||
@@ -191,7 +191,7 @@ defmodule WandererApp.Kills.MapEventListener do
|
||||
# Client is not connected, retry with backoff
|
||||
schedule_retry_update(state)
|
||||
|
||||
error ->
|
||||
_error ->
|
||||
schedule_retry_update(state)
|
||||
end
|
||||
rescue
|
||||
|
||||
@@ -12,6 +12,7 @@ defmodule WandererApp.Map do
|
||||
defstruct map_id: nil,
|
||||
name: nil,
|
||||
scope: :none,
|
||||
scopes: nil,
|
||||
owner_id: nil,
|
||||
characters: [],
|
||||
systems: Map.new(),
|
||||
@@ -22,11 +23,18 @@ defmodule WandererApp.Map do
|
||||
characters_limit: nil,
|
||||
hubs_limit: nil
|
||||
|
||||
def new(%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs}) do
|
||||
def new(
|
||||
%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs} =
|
||||
input
|
||||
) do
|
||||
# Extract the new scopes array field if present (nil if not set)
|
||||
scopes = Map.get(input, :scopes)
|
||||
|
||||
map =
|
||||
struct!(__MODULE__,
|
||||
map_id: map_id,
|
||||
scope: scope,
|
||||
scopes: scopes,
|
||||
owner_id: owner_id,
|
||||
name: name,
|
||||
acls: acls,
|
||||
@@ -177,7 +185,7 @@ defmodule WandererApp.Map do
|
||||
end
|
||||
|
||||
def list_hubs(map_id, hubs) do
|
||||
{:ok, map} = map_id |> get_map()
|
||||
{:ok, _map} = map_id |> get_map()
|
||||
|
||||
{:ok, hubs}
|
||||
end
|
||||
@@ -315,7 +323,7 @@ defmodule WandererApp.Map do
|
||||
end
|
||||
end
|
||||
|
||||
def update_subscription_settings!(%{map_id: map_id} = map, %{
|
||||
def update_subscription_settings!(%{map_id: map_id} = _map, %{
|
||||
characters_limit: characters_limit,
|
||||
hubs_limit: hubs_limit
|
||||
}) do
|
||||
@@ -326,7 +334,7 @@ defmodule WandererApp.Map do
|
||||
|> get_map!()
|
||||
end
|
||||
|
||||
def update_options!(%{map_id: map_id} = map, options) do
|
||||
def update_options!(%{map_id: map_id} = _map, options) do
|
||||
map_id
|
||||
|> update_map(%{options: options})
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ defmodule WandererApp.Map.Manager do
|
||||
@maps_queue :maps_queue
|
||||
@check_maps_queue_interval :timer.seconds(1)
|
||||
|
||||
@pings_cleanup_interval :timer.minutes(10)
|
||||
@pings_cleanup_interval :timer.minutes(5)
|
||||
@pings_expire_minutes 60
|
||||
|
||||
# Test-aware async task runner
|
||||
@@ -99,6 +99,7 @@ defmodule WandererApp.Map.Manager do
|
||||
def handle_info(:cleanup_pings, state) do
|
||||
try do
|
||||
cleanup_expired_pings()
|
||||
cleanup_orphaned_pings()
|
||||
{:noreply, state}
|
||||
rescue
|
||||
e ->
|
||||
@@ -141,6 +142,51 @@ defmodule WandererApp.Map.Manager do
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_orphaned_pings() do
|
||||
case WandererApp.MapPingsRepo.get_orphaned_pings() do
|
||||
{:ok, []} ->
|
||||
:ok
|
||||
|
||||
{:ok, orphaned_pings} ->
|
||||
Logger.info(
|
||||
"[cleanup_orphaned_pings] Found #{length(orphaned_pings)} orphaned pings, cleaning up..."
|
||||
)
|
||||
|
||||
Enum.each(orphaned_pings, fn %{id: ping_id, map_id: map_id, type: type, system: system} = ping ->
|
||||
reason =
|
||||
cond do
|
||||
is_nil(ping.system) -> "system deleted"
|
||||
is_nil(ping.character) -> "character deleted"
|
||||
is_nil(ping.map) -> "map deleted"
|
||||
not is_nil(system) and system.visible == false -> "system hidden (visible=false)"
|
||||
true -> "unknown"
|
||||
end
|
||||
|
||||
Logger.warning(
|
||||
"[cleanup_orphaned_pings] Destroying orphaned ping #{ping_id} (map_id: #{map_id}, reason: #{reason})"
|
||||
)
|
||||
|
||||
# Broadcast cancellation if map_id is still valid
|
||||
if map_id do
|
||||
Server.Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: nil,
|
||||
type: type
|
||||
})
|
||||
end
|
||||
|
||||
Ash.destroy!(ping)
|
||||
end)
|
||||
|
||||
Logger.info("[cleanup_orphaned_pings] Cleaned up #{length(orphaned_pings)} orphaned pings")
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("Failed to fetch orphaned pings: #{inspect(error)}")
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_maps() do
|
||||
chunks =
|
||||
@maps_queue
|
||||
|
||||
@@ -76,11 +76,6 @@ defmodule WandererApp.Map.Operations do
|
||||
{:ok, map()} | {:skip, :exists} | {:error, String.t()}
|
||||
defdelegate create_connection(map_id, attrs, char_id), to: Connections
|
||||
|
||||
@doc "Create a connection from a Plug.Conn"
|
||||
@spec create_connection(Plug.Conn.t(), map()) ::
|
||||
{:ok, :created} | {:skip, :exists} | {:error, atom()}
|
||||
defdelegate create_connection(conn, attrs), to: Connections
|
||||
|
||||
@doc "Update a connection"
|
||||
@spec update_connection(String.t(), String.t(), map()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
|
||||
@@ -329,6 +329,9 @@ defmodule WandererApp.Map.MapPool do
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:error, _, state), do: {:stop, :error, :ok, state}
|
||||
|
||||
defp do_start_map(map_id, %{map_ids: map_ids, uuid: uuid} = state) do
|
||||
if map_id in map_ids do
|
||||
# Map already started
|
||||
@@ -344,8 +347,6 @@ defmodule WandererApp.Map.MapPool do
|
||||
[map_id | r_map_ids]
|
||||
end)
|
||||
|
||||
completed_operations = [:registry | completed_operations]
|
||||
|
||||
case registry_result do
|
||||
{new_value, _old_value} when is_list(new_value) ->
|
||||
:ok
|
||||
@@ -363,13 +364,9 @@ defmodule WandererApp.Map.MapPool do
|
||||
raise "Failed to add to cache: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
completed_operations = [:cache | completed_operations]
|
||||
|
||||
# Step 3: Start the map server using extracted helper
|
||||
do_initialize_map_server(map_id)
|
||||
|
||||
completed_operations = [:map_server | completed_operations]
|
||||
|
||||
# Step 4: Update GenServer state (last, as this is in-memory and fast)
|
||||
new_state = %{state | map_ids: [map_id | map_ids]}
|
||||
|
||||
@@ -445,8 +442,6 @@ defmodule WandererApp.Map.MapPool do
|
||||
r_map_ids |> Enum.reject(fn id -> id == map_id end)
|
||||
end)
|
||||
|
||||
completed_operations = [:registry | completed_operations]
|
||||
|
||||
case registry_result do
|
||||
{new_value, _old_value} when is_list(new_value) ->
|
||||
:ok
|
||||
@@ -464,14 +459,10 @@ defmodule WandererApp.Map.MapPool do
|
||||
raise "Failed to delete from cache: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
completed_operations = [:cache | completed_operations]
|
||||
|
||||
# Step 3: Stop the map server (clean up all map resources)
|
||||
map_id
|
||||
|> Server.Impl.stop_map()
|
||||
|
||||
completed_operations = [:map_server | completed_operations]
|
||||
|
||||
# Step 4: Update GenServer state (last, as this is in-memory and fast)
|
||||
new_state = %{state | map_ids: map_ids |> Enum.reject(fn id -> id == map_id end)}
|
||||
|
||||
@@ -560,9 +551,6 @@ defmodule WandererApp.Map.MapPool do
|
||||
# and the cleanup operations are safe to leave in a "stopped" state
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:error, _, state), do: {:stop, :error, :ok, state}
|
||||
|
||||
@impl true
|
||||
def handle_info(:backup_state, %{map_ids: map_ids, uuid: uuid} = state) do
|
||||
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
||||
|
||||
@@ -179,15 +179,4 @@ defmodule WandererApp.Map.MapPoolDynamicSupervisor do
|
||||
{:ok, pid}
|
||||
end
|
||||
end
|
||||
|
||||
defp stop_child(uuid) do
|
||||
case Registry.lookup(@registry, uuid) do
|
||||
[{pid, _}] ->
|
||||
GenServer.cast(pid, :stop)
|
||||
|
||||
_ ->
|
||||
Logger.warn("Unable to locate pool assigned to #{inspect(uuid)}")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,7 +77,7 @@ defmodule WandererApp.Map.Routes do
|
||||
end
|
||||
end
|
||||
|
||||
def find(_map_id, hubs, origin, routes_settings, true) do
|
||||
def find(_map_id, hubs, origin, _routes_settings, true) do
|
||||
origin = origin |> String.to_integer()
|
||||
hubs = hubs |> Enum.map(&(&1 |> String.to_integer()))
|
||||
|
||||
|
||||
@@ -72,28 +72,36 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
:ok
|
||||
end
|
||||
|
||||
def estimate_price(params, renew?, promo_code \\ nil)
|
||||
|
||||
def estimate_price(
|
||||
%{
|
||||
"period" => period,
|
||||
"characters_limit" => characters_limit,
|
||||
"hubs_limit" => hubs_limit
|
||||
},
|
||||
renew?
|
||||
} = params,
|
||||
renew?,
|
||||
promo_code
|
||||
)
|
||||
when is_binary(characters_limit),
|
||||
do:
|
||||
estimate_price(
|
||||
%{
|
||||
period: period |> String.to_integer(),
|
||||
characters_limit: characters_limit |> String.to_integer(),
|
||||
hubs_limit: hubs_limit |> String.to_integer()
|
||||
},
|
||||
renew?
|
||||
)
|
||||
when is_binary(characters_limit) do
|
||||
# Extract promo_code from params if passed there (from form)
|
||||
promo_code = promo_code || Map.get(params, "promo_code")
|
||||
|
||||
estimate_price(
|
||||
%{
|
||||
period: period |> String.to_integer(),
|
||||
characters_limit: characters_limit |> String.to_integer(),
|
||||
hubs_limit: hubs_limit |> String.to_integer()
|
||||
},
|
||||
renew?,
|
||||
promo_code
|
||||
)
|
||||
end
|
||||
|
||||
def estimate_price(
|
||||
%{characters_limit: characters_limit, hubs_limit: hubs_limit} = params,
|
||||
renew?
|
||||
renew?,
|
||||
promo_code
|
||||
) do
|
||||
%{
|
||||
plans: plans,
|
||||
@@ -136,7 +144,7 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
|
||||
total_price = estimated_price * period
|
||||
|
||||
{:ok, discount} =
|
||||
{:ok, period_discount} =
|
||||
calc_discount(
|
||||
period,
|
||||
total_price,
|
||||
@@ -144,13 +152,27 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
renew?
|
||||
)
|
||||
|
||||
{:ok, total_price, discount}
|
||||
# Calculate promo discount on price after period discount
|
||||
price_after_period_discount = total_price - period_discount
|
||||
|
||||
{:ok, promo_discount, promo_valid?} =
|
||||
calc_promo_discount(promo_code, price_after_period_discount)
|
||||
|
||||
total_discount = period_discount + promo_discount
|
||||
|
||||
{:ok, total_price, total_discount, promo_valid?}
|
||||
end
|
||||
|
||||
def calc_additional_price(params, selected_subscription, promo_code \\ nil)
|
||||
|
||||
def calc_additional_price(
|
||||
%{"characters_limit" => characters_limit, "hubs_limit" => hubs_limit},
|
||||
selected_subscription
|
||||
%{"characters_limit" => characters_limit, "hubs_limit" => hubs_limit} = params,
|
||||
selected_subscription,
|
||||
promo_code
|
||||
) do
|
||||
# Extract promo_code from params if passed there (from form)
|
||||
promo_code = promo_code || Map.get(params, "promo_code")
|
||||
|
||||
%{
|
||||
plans: plans,
|
||||
extra_characters_50: extra_characters_50,
|
||||
@@ -189,7 +211,7 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
|
||||
total_price = additional_price * period
|
||||
|
||||
{:ok, discount} =
|
||||
{:ok, period_discount} =
|
||||
calc_discount(
|
||||
period,
|
||||
total_price,
|
||||
@@ -197,7 +219,15 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
false
|
||||
)
|
||||
|
||||
{:ok, total_price, discount}
|
||||
# Calculate promo discount on price after period discount
|
||||
price_after_period_discount = total_price - period_discount
|
||||
|
||||
{:ok, promo_discount, promo_valid?} =
|
||||
calc_promo_discount(promo_code, price_after_period_discount)
|
||||
|
||||
total_discount = period_discount + promo_discount
|
||||
|
||||
{:ok, total_price, total_discount, promo_valid?}
|
||||
end
|
||||
|
||||
defp get_active_months(subscription) do
|
||||
@@ -255,6 +285,22 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
when period >= 3,
|
||||
do: {:ok, round(total_price * month_3_discount)}
|
||||
|
||||
# Calculates the promo code discount amount.
|
||||
# Returns {:ok, discount_amount, is_valid?}
|
||||
defp calc_promo_discount(nil, _price), do: {:ok, 0, false}
|
||||
defp calc_promo_discount("", _price), do: {:ok, 0, false}
|
||||
|
||||
defp calc_promo_discount(promo_code, price) when is_binary(promo_code) do
|
||||
case WandererApp.Env.validate_promo_code(promo_code) do
|
||||
{:ok, discount_percent} ->
|
||||
discount_amount = round(price * discount_percent / 100)
|
||||
{:ok, discount_amount, true}
|
||||
|
||||
{:error, :invalid_code} ->
|
||||
{:ok, 0, false}
|
||||
end
|
||||
end
|
||||
|
||||
def get_balance(map) do
|
||||
map
|
||||
|> WandererApp.MapRepo.load_relationships([
|
||||
@@ -302,7 +348,8 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
|
||||
defp renew_subscription(%{auto_renew?: true, map: map} = subscription)
|
||||
when is_map(subscription) do
|
||||
with {:ok, estimated_price, discount} <- estimate_price(subscription, true),
|
||||
# No promo code for auto-renewals, ignore the promo_valid? return value
|
||||
with {:ok, estimated_price, discount, _promo_valid?} <- estimate_price(subscription, true),
|
||||
{:ok, map_balance} <- get_balance(map) do
|
||||
case map_balance >= estimated_price do
|
||||
true ->
|
||||
|
||||
@@ -93,10 +93,8 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines the ship size for a connection, applying wormhole‑specific rules
|
||||
for C1, C13, and C4⇄NS links, falling back to the caller’s provided size or Large.
|
||||
"""
|
||||
# Determines the ship size for a connection, applying wormhole-specific rules
|
||||
# for C1, C13, and C4⇄NS links, falling back to the caller's provided size or Large.
|
||||
defp resolve_ship_size(type_val, ship_size_val, src_info, tgt_info) do
|
||||
case parse_type(type_val) do
|
||||
@connection_type_wormhole ->
|
||||
|
||||
@@ -12,7 +12,6 @@ defmodule WandererApp.Map.Operations.Duplication do
|
||||
"""
|
||||
|
||||
require Logger
|
||||
import Ash.Query, only: [filter: 2]
|
||||
|
||||
alias WandererApp.Api
|
||||
alias WandererApp.Api.{MapSystem, MapConnection, MapSystemSignature, MapCharacterSettings}
|
||||
|
||||
@@ -78,7 +78,8 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
)
|
||||
when is_integer(solar_system_id) do
|
||||
with {:ok, validated_char_uuid} <- validate_character_eve_id(params, char_id),
|
||||
{:ok, system} <- MapSystem.by_map_id_and_solar_system_id(map_id, solar_system_id) do
|
||||
{:ok, system} <-
|
||||
MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: solar_system_id}) do
|
||||
attrs =
|
||||
params
|
||||
|> Map.put("system_id", system.id)
|
||||
|
||||
297
lib/wanderer_app/map/routes_by.ex
Normal file
297
lib/wanderer_app/map/routes_by.ex
Normal file
@@ -0,0 +1,297 @@
|
||||
defmodule WandererApp.Map.RoutesBy do
|
||||
@moduledoc """
|
||||
Routes-by helper that uses the local route builder service.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
@minimum_route_attrs [
|
||||
:system_class,
|
||||
:class_title,
|
||||
:security,
|
||||
:triglavian_invasion_status,
|
||||
:solar_system_id,
|
||||
:solar_system_name,
|
||||
:region_name,
|
||||
:is_shattered
|
||||
]
|
||||
|
||||
@default_routes_settings %{
|
||||
path_type: "shortest",
|
||||
include_mass_crit: true,
|
||||
include_eol: false,
|
||||
include_frig: true,
|
||||
include_cruise: true,
|
||||
avoid_wormholes: false,
|
||||
avoid_pochven: false,
|
||||
avoid_edencom: false,
|
||||
avoid_triglavian: false,
|
||||
include_thera: true,
|
||||
avoid: []
|
||||
}
|
||||
|
||||
@zarzakh_system 30_100_000
|
||||
@default_avoid_systems [@zarzakh_system]
|
||||
@get_link_pairs_advanced_params [
|
||||
:include_mass_crit,
|
||||
:include_eol,
|
||||
:include_frig
|
||||
]
|
||||
|
||||
def find(map_id, origin, routes_settings, type) do
|
||||
origin = parse_origin(origin)
|
||||
routes_settings = @default_routes_settings |> Map.merge(routes_settings || %{})
|
||||
|
||||
connections = build_connections(map_id, routes_settings)
|
||||
|
||||
avoidance_list = build_avoidance_list(routes_settings)
|
||||
|
||||
security_type =
|
||||
routes_settings
|
||||
|> Map.get(:security_type, "both")
|
||||
|> normalize_security_type()
|
||||
|
||||
payload = %{
|
||||
origin: origin,
|
||||
flag: routes_settings.path_type,
|
||||
connections: connections,
|
||||
avoid: avoidance_list,
|
||||
count: 40,
|
||||
type: type,
|
||||
security_type: security_type,
|
||||
routes_settings: routes_settings
|
||||
}
|
||||
|
||||
stations_by_system = WandererApp.RouteBuilderClient.stations_for(type)
|
||||
|
||||
case WandererApp.RouteBuilderClient.find_closest(payload) do
|
||||
{:ok, body} ->
|
||||
routes = normalize_routes(body, origin)
|
||||
routes = attach_stations(routes, stations_by_system)
|
||||
systems_static_data = fetch_systems_static_data(routes)
|
||||
{:ok, %{routes: routes, systems_static_data: systems_static_data}}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("[RoutesBy] Failed to fetch routes by: #{inspect(reason)}")
|
||||
{:ok, %{routes: [], systems_static_data: []}}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_origin(origin) when is_integer(origin), do: origin
|
||||
|
||||
defp parse_origin(origin) when is_binary(origin) do
|
||||
case Integer.parse(origin) do
|
||||
{id, _} -> id
|
||||
:error -> 0
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_origin(_), do: 0
|
||||
|
||||
defp normalize_routes(%{"routes" => routes}, origin) when is_list(routes),
|
||||
do: normalize_routes(routes, origin)
|
||||
|
||||
defp normalize_routes(routes, _origin) when is_list(routes) do
|
||||
routes
|
||||
|> Enum.map(&map_route_info/1)
|
||||
|> Enum.filter(fn route_info -> not is_nil(route_info) end)
|
||||
end
|
||||
|
||||
defp normalize_routes(_body, _origin), do: []
|
||||
|
||||
defp attach_stations(routes, stations_by_system) do
|
||||
Enum.map(routes, fn route ->
|
||||
system_key = to_string(route.destination)
|
||||
stations = Map.get(stations_by_system, system_key, [])
|
||||
|
||||
normalized_stations =
|
||||
stations
|
||||
|> Enum.filter(&is_map/1)
|
||||
|> Enum.map(fn station ->
|
||||
%{
|
||||
station_id: Map.get(station, "station_id") || Map.get(station, :station_id),
|
||||
station_name: Map.get(station, "name") || Map.get(station, :name),
|
||||
special: Map.get(station, "special") || Map.get(station, :special) || false
|
||||
}
|
||||
end)
|
||||
|> Enum.filter(fn station ->
|
||||
is_integer(station.station_id) and is_binary(station.station_name)
|
||||
end)
|
||||
|
||||
Map.put(route, :stations, normalized_stations)
|
||||
end)
|
||||
end
|
||||
|
||||
defp map_route_info(%{
|
||||
"origin" => origin,
|
||||
"destination" => destination,
|
||||
"systems" => result_systems,
|
||||
"success" => success
|
||||
}) do
|
||||
map_route_info(%{
|
||||
origin: origin,
|
||||
destination: destination,
|
||||
systems: result_systems,
|
||||
success: success
|
||||
})
|
||||
end
|
||||
|
||||
defp map_route_info(
|
||||
%{origin: origin, destination: destination, systems: result_systems, success: success} =
|
||||
_route_info
|
||||
) do
|
||||
systems =
|
||||
case result_systems do
|
||||
[] -> []
|
||||
_ -> result_systems |> Enum.reject(fn system_id -> system_id == origin end)
|
||||
end
|
||||
|
||||
%{
|
||||
has_connection: result_systems != [],
|
||||
systems: systems,
|
||||
origin: origin,
|
||||
destination: destination,
|
||||
success: success
|
||||
}
|
||||
end
|
||||
|
||||
defp map_route_info(_), do: nil
|
||||
|
||||
defp fetch_systems_static_data(routes) do
|
||||
routes
|
||||
|> Enum.map(fn route_info -> route_info.systems end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|> Task.async_stream(
|
||||
fn system_id ->
|
||||
case WandererApp.CachedInfo.get_system_static_info(system_id) do
|
||||
{:ok, nil} -> nil
|
||||
{:ok, system} -> system |> Map.take(@minimum_route_attrs)
|
||||
end
|
||||
end,
|
||||
max_concurrency: System.schedulers_online() * 4
|
||||
)
|
||||
|> Enum.map(fn {:ok, val} -> val end)
|
||||
end
|
||||
|
||||
defp build_avoidance_list(routes_settings) do
|
||||
{:ok, trig_systems} = WandererApp.CachedInfo.get_trig_systems()
|
||||
|
||||
pochven_solar_systems =
|
||||
trig_systems
|
||||
|> Enum.filter(fn s -> s.triglavian_invasion_status == "Final" end)
|
||||
|> Enum.map(& &1.solar_system_id)
|
||||
|
||||
triglavian_solar_systems =
|
||||
trig_systems
|
||||
|> Enum.filter(fn s -> s.triglavian_invasion_status == "Triglavian" end)
|
||||
|> Enum.map(& &1.solar_system_id)
|
||||
|
||||
edencom_solar_systems =
|
||||
trig_systems
|
||||
|> Enum.filter(fn s -> s.triglavian_invasion_status == "Edencom" end)
|
||||
|> Enum.map(& &1.solar_system_id)
|
||||
|
||||
avoidance_list =
|
||||
case routes_settings.avoid_edencom do
|
||||
true -> edencom_solar_systems
|
||||
false -> []
|
||||
end
|
||||
|
||||
avoidance_list =
|
||||
case routes_settings.avoid_triglavian do
|
||||
true -> [avoidance_list | triglavian_solar_systems]
|
||||
false -> avoidance_list
|
||||
end
|
||||
|
||||
avoidance_list =
|
||||
case routes_settings.avoid_pochven do
|
||||
true -> [avoidance_list | pochven_solar_systems]
|
||||
false -> avoidance_list
|
||||
end
|
||||
|
||||
(@default_avoid_systems ++ [routes_settings.avoid | avoidance_list])
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp normalize_security_type("high"), do: "high"
|
||||
defp normalize_security_type(:high), do: "high"
|
||||
defp normalize_security_type("low"), do: "low"
|
||||
defp normalize_security_type(:low), do: "low"
|
||||
defp normalize_security_type(_), do: "both"
|
||||
|
||||
defp build_connections(map_id, routes_settings) do
|
||||
if routes_settings.avoid_wormholes do
|
||||
[]
|
||||
else
|
||||
map_chains =
|
||||
routes_settings
|
||||
|> Map.take(@get_link_pairs_advanced_params)
|
||||
|> Map.put_new(:map_id, map_id)
|
||||
|> WandererApp.Api.MapConnection.get_link_pairs_advanced!()
|
||||
|> Enum.map(fn %{
|
||||
solar_system_source: solar_system_source,
|
||||
solar_system_target: solar_system_target
|
||||
} ->
|
||||
%{
|
||||
first: solar_system_source,
|
||||
second: solar_system_target
|
||||
}
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, thera_chains} =
|
||||
case routes_settings.include_thera do
|
||||
true ->
|
||||
WandererApp.Server.TheraDataFetcher.get_chain_pairs(routes_settings)
|
||||
|
||||
false ->
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
chains = remove_intersection([map_chains | thera_chains] |> List.flatten())
|
||||
|
||||
chains =
|
||||
case routes_settings.include_cruise do
|
||||
false ->
|
||||
{:ok, wh_class_a_systems} = WandererApp.CachedInfo.get_wh_class_a_systems()
|
||||
|
||||
chains
|
||||
|> Enum.filter(fn x ->
|
||||
not Enum.member?(wh_class_a_systems, x.first) and
|
||||
not Enum.member?(wh_class_a_systems, x.second)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
chains
|
||||
end
|
||||
|
||||
chains
|
||||
|> Enum.map(fn chain ->
|
||||
["#{chain.first}|#{chain.second}", "#{chain.second}|#{chain.first}"]
|
||||
end)
|
||||
|> List.flatten()
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_intersection(pairs_arr) do
|
||||
tuples = pairs_arr |> Enum.map(fn x -> {x.first, x.second} end)
|
||||
|
||||
tuples
|
||||
|> Enum.reduce([], fn {first, second} = x, acc ->
|
||||
if Enum.member?(tuples, {second, first}) do
|
||||
acc
|
||||
else
|
||||
[x | acc]
|
||||
end
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.map(fn {first, second} ->
|
||||
%{
|
||||
first: first,
|
||||
second: second
|
||||
}
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -56,7 +56,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
end
|
||||
)
|
||||
|
||||
map_update = %{acls: map.acls, scope: map.scope}
|
||||
map_update = %{acls: map.acls, scope: map.scope, scopes: map.scopes}
|
||||
|
||||
WandererApp.Map.update_map(map_id, map_update)
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
@@ -569,6 +569,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
broadcast_permission_update(character_id)
|
||||
|
||||
:has_update
|
||||
|
||||
{:character_corporation, _info} ->
|
||||
@@ -580,6 +583,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
broadcast_permission_update(character_id)
|
||||
|
||||
:has_update
|
||||
|
||||
_ ->
|
||||
@@ -814,21 +820,33 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
do: :ok
|
||||
|
||||
defp update_location(
|
||||
%{map: %{scope: scope}, map_id: map_id, map_opts: map_opts} =
|
||||
%{map: map, map_id: map_id, map_opts: map_opts} =
|
||||
_state,
|
||||
character_id,
|
||||
location,
|
||||
old_location
|
||||
) do
|
||||
ConnectionsImpl.is_connection_valid(
|
||||
scope,
|
||||
old_location.solar_system_id,
|
||||
location.solar_system_id
|
||||
scopes = get_effective_scopes(map)
|
||||
|
||||
is_valid =
|
||||
ConnectionsImpl.is_connection_valid(
|
||||
scopes,
|
||||
old_location.solar_system_id,
|
||||
location.solar_system_id
|
||||
)
|
||||
|
||||
Logger.debug(
|
||||
"[CharacterTracking] update_location: map=#{map_id}, " <>
|
||||
"from=#{old_location.solar_system_id}, to=#{location.solar_system_id}, " <>
|
||||
"scopes=#{inspect(scopes)}, map.scopes=#{inspect(map[:scopes])}, " <>
|
||||
"map.scope=#{inspect(map[:scope])}, is_valid=#{is_valid}"
|
||||
)
|
||||
|> case do
|
||||
|
||||
case is_valid do
|
||||
true ->
|
||||
# Add new location system
|
||||
case SystemsImpl.maybe_add_system(map_id, location, old_location, map_opts) do
|
||||
# Connection is valid (at least one system matches scopes)
|
||||
# Add systems that match the map's scopes - individual system filtering by maybe_add_system
|
||||
case SystemsImpl.maybe_add_system(map_id, location, old_location, map_opts, scopes) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
@@ -838,8 +856,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
)
|
||||
end
|
||||
|
||||
# Add old location system (in case it wasn't on map)
|
||||
case SystemsImpl.maybe_add_system(map_id, old_location, location, map_opts) do
|
||||
# Add old location system (in case it wasn't on map) - only if it matches scopes
|
||||
case SystemsImpl.maybe_add_system(map_id, old_location, location, map_opts, scopes) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
@@ -879,6 +897,24 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
defp is_character_in_space?(%{station_id: station_id, structure_id: structure_id} = _location),
|
||||
do: is_nil(structure_id) && is_nil(station_id)
|
||||
|
||||
@doc """
|
||||
Get effective scopes from map, with fallback to legacy scope.
|
||||
Returns the scopes array that should be used for filtering.
|
||||
"""
|
||||
def get_effective_scopes(%{scopes: scopes}) when is_list(scopes) and scopes != [], do: scopes
|
||||
|
||||
def get_effective_scopes(%{scope: scope}) when is_atom(scope),
|
||||
do: legacy_scope_to_scopes(scope)
|
||||
|
||||
def get_effective_scopes(_), do: [:wormholes]
|
||||
|
||||
# Legacy scope to new scopes array conversion
|
||||
defp legacy_scope_to_scopes(:wormholes), do: [:wormholes]
|
||||
defp legacy_scope_to_scopes(:stargates), do: [:hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:all), do: [:wormholes, :hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:none), do: []
|
||||
defp legacy_scope_to_scopes(_), do: [:wormholes]
|
||||
|
||||
defp add_character(
|
||||
map_id,
|
||||
%{id: character_id} = map_character,
|
||||
@@ -923,4 +959,21 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
track: true
|
||||
})
|
||||
end
|
||||
|
||||
# Broadcasts permission update to trigger LiveView refresh for the character's user.
|
||||
# This is called when a character's corporation or alliance changes, ensuring
|
||||
# users are kicked off maps they no longer have access to.
|
||||
defp broadcast_permission_update(character_id) do
|
||||
case WandererApp.Character.get_character(character_id) do
|
||||
{:ok, %{eve_id: eve_id}} when not is_nil(eve_id) ->
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
alias WandererApp.Map.Server.Impl
|
||||
alias WandererApp.Map.Server.SignaturesImpl
|
||||
alias WandererApp.Map.Server.SystemsImpl
|
||||
|
||||
# @ccp1 -1
|
||||
@c1 1
|
||||
@@ -57,6 +58,12 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
@known_space [@hs, @ls, @ns, @pochven]
|
||||
|
||||
# Individual space type lists for granular scope matching
|
||||
@hi_space [@hs]
|
||||
@low_space [@ls]
|
||||
@null_space [@ns]
|
||||
@pochven_space [@pochven]
|
||||
|
||||
@prohibited_systems [@jita]
|
||||
@prohibited_system_classes [
|
||||
@a1,
|
||||
@@ -100,7 +107,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
@connection_type_wormhole 0
|
||||
@connection_type_stargate 1
|
||||
@connection_type_bridge 2
|
||||
# @connection_type_bridge 2 # reserved for future use
|
||||
@medium_ship_size 1
|
||||
|
||||
def get_connection_auto_expire_hours(), do: WandererApp.Env.map_connection_auto_expire_hours()
|
||||
@@ -290,6 +297,30 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
do: update_connection(map_id, :update_custom_info, [:custom_info], connection_update)
|
||||
|
||||
def cleanup_connections(map_id) do
|
||||
# Defensive check: Skip cleanup if cache appears invalid
|
||||
# This prevents incorrectly deleting connections when cache is empty due to
|
||||
# race conditions during map restart or cache corruption
|
||||
case WandererApp.Map.get_map(map_id) do
|
||||
{:error, :not_found} ->
|
||||
Logger.warning(
|
||||
"[cleanup_connections] Skipping map #{map_id} - cache miss detected, " <>
|
||||
"map data not found in cache"
|
||||
)
|
||||
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :cleanup_connections, :cache_miss],
|
||||
%{system_time: System.system_time()},
|
||||
%{map_id: map_id}
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
{:ok, _map} ->
|
||||
do_cleanup_connections(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_cleanup_connections(map_id) do
|
||||
connection_auto_expire_hours = get_connection_auto_expire_hours()
|
||||
connection_auto_eol_hours = get_connection_auto_eol_hours()
|
||||
connection_eol_expire_timeout_hours = get_eol_expire_timeout_mins() / 60
|
||||
@@ -343,6 +374,27 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
solar_system_source: solar_system_source_id,
|
||||
solar_system_target: solar_system_target_id
|
||||
} ->
|
||||
# Emit telemetry for connection auto-deletion
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :connection_cleanup, :delete],
|
||||
%{system_time: System.system_time()},
|
||||
%{
|
||||
map_id: map_id,
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id,
|
||||
reason: :auto_cleanup
|
||||
}
|
||||
)
|
||||
|
||||
# Log auto-deletion for audit trail (no user/character context for auto-cleanup)
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_connection_removed, %{
|
||||
character_id: nil,
|
||||
user_id: nil,
|
||||
map_id: map_id,
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id
|
||||
})
|
||||
|
||||
delete_connection(map_id, %{
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id
|
||||
@@ -403,7 +455,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
time_status: time_status,
|
||||
solar_system_source: solar_system_source,
|
||||
solar_system_target: solar_system_target
|
||||
} = updated_connection
|
||||
} = _updated_connection
|
||||
) do
|
||||
with source_system when not is_nil(source_system) <-
|
||||
WandererApp.Map.find_system_by_location(
|
||||
@@ -644,31 +696,49 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
start_time
|
||||
)
|
||||
|
||||
def can_add_location(_scope, nil), do: false
|
||||
def can_add_location(_scopes, nil), do: false
|
||||
|
||||
def can_add_location(:none, _solar_system_id), do: false
|
||||
def can_add_location([], _solar_system_id), do: false
|
||||
|
||||
def can_add_location(scope, solar_system_id) do
|
||||
def can_add_location(scopes, solar_system_id) when is_list(scopes) do
|
||||
{:ok, system_static_info} = get_system_static_info(solar_system_id)
|
||||
|
||||
case scope do
|
||||
:wormholes ->
|
||||
not is_prohibited_system_class?(system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(solar_system_id)) and
|
||||
@wh_space |> Enum.member?(system_static_info.system_class)
|
||||
|
||||
:stargates ->
|
||||
not is_prohibited_system_class?(system_static_info.system_class) and
|
||||
@known_space |> Enum.member?(system_static_info.system_class)
|
||||
|
||||
:all ->
|
||||
not is_prohibited_system_class?(system_static_info.system_class)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
not is_prohibited_system_class?(system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(solar_system_id)) and
|
||||
system_matches_any_scope?(system_static_info.system_class, scopes)
|
||||
end
|
||||
|
||||
# Legacy support for single scope atom
|
||||
def can_add_location(:none, _solar_system_id), do: false
|
||||
|
||||
def can_add_location(scope, solar_system_id) when is_atom(scope) do
|
||||
can_add_location(legacy_scope_to_scopes(scope), solar_system_id)
|
||||
end
|
||||
|
||||
# Helper function to check if a system class matches any of the selected scopes
|
||||
defp system_matches_any_scope?(_system_class, []), do: false
|
||||
|
||||
defp system_matches_any_scope?(system_class, scopes) do
|
||||
Enum.any?(scopes, fn scope ->
|
||||
system_matches_scope?(system_class, scope)
|
||||
end)
|
||||
end
|
||||
|
||||
# Individual scope matching functions
|
||||
defp system_matches_scope?(system_class, :wormholes), do: system_class in @wh_space
|
||||
defp system_matches_scope?(system_class, :hi), do: system_class in @hi_space
|
||||
defp system_matches_scope?(system_class, :low), do: system_class in @low_space
|
||||
defp system_matches_scope?(system_class, :null), do: system_class in @null_space
|
||||
defp system_matches_scope?(system_class, :pochven), do: system_class in @pochven_space
|
||||
defp system_matches_scope?(_system_class, _), do: false
|
||||
|
||||
# Legacy scope to new scopes array conversion
|
||||
defp legacy_scope_to_scopes(:wormholes), do: [:wormholes]
|
||||
defp legacy_scope_to_scopes(:stargates), do: [:hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:all), do: [:wormholes, :hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:none), do: []
|
||||
defp legacy_scope_to_scopes(_), do: [:wormholes]
|
||||
|
||||
def is_prohibited_system_class?(system_class) do
|
||||
@prohibited_system_classes |> Enum.member?(system_class)
|
||||
end
|
||||
@@ -688,17 +758,81 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
)
|
||||
)
|
||||
|
||||
def is_connection_valid(_scope, from_solar_system_id, to_solar_system_id)
|
||||
def is_connection_valid(_scopes, from_solar_system_id, to_solar_system_id)
|
||||
when is_nil(from_solar_system_id) or is_nil(to_solar_system_id),
|
||||
do: false
|
||||
|
||||
def is_connection_valid([], _from_solar_system_id, _to_solar_system_id), do: false
|
||||
|
||||
# New array-based scopes support
|
||||
def is_connection_valid(scopes, from_solar_system_id, to_solar_system_id)
|
||||
when is_list(scopes) and from_solar_system_id != to_solar_system_id do
|
||||
with {: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
|
||||
# First check: neither system is prohibited
|
||||
not_prohibited =
|
||||
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))
|
||||
|
||||
if not_prohibited do
|
||||
from_is_wormhole = from_system_static_info.system_class in @wh_space
|
||||
to_is_wormhole = to_system_static_info.system_class in @wh_space
|
||||
wormholes_enabled = :wormholes in scopes
|
||||
|
||||
cond do
|
||||
# Case 1: Wormhole border behavior - at least one system is a wormhole
|
||||
# and :wormholes is enabled, allow the connection (adds border k-space systems)
|
||||
wormholes_enabled and (from_is_wormhole or to_is_wormhole) ->
|
||||
# At least one system matches (wormhole matches :wormholes, or other matches its scope)
|
||||
system_matches_any_scope?(from_system_static_info.system_class, scopes) or
|
||||
system_matches_any_scope?(to_system_static_info.system_class, scopes)
|
||||
|
||||
# Case 2: K-space to K-space with :wormholes enabled - check if it's a wormhole connection
|
||||
# If neither system is a wormhole AND there's no stargate between them, it's a wormhole connection
|
||||
wormholes_enabled and not from_is_wormhole and not to_is_wormhole ->
|
||||
# Check if there's a known stargate connection
|
||||
case find_solar_system_jump(from_solar_system_id, to_solar_system_id) do
|
||||
{:ok, known_jumps} when known_jumps == [] ->
|
||||
# No stargate exists - this is a wormhole connection through k-space
|
||||
true
|
||||
|
||||
{:ok, _known_jumps} ->
|
||||
# Stargate exists - this is NOT a wormhole, check normal scope matching
|
||||
system_matches_any_scope?(from_system_static_info.system_class, scopes) and
|
||||
system_matches_any_scope?(to_system_static_info.system_class, scopes)
|
||||
|
||||
_ ->
|
||||
# Error fetching jumps - fall back to scope matching
|
||||
system_matches_any_scope?(from_system_static_info.system_class, scopes) and
|
||||
system_matches_any_scope?(to_system_static_info.system_class, scopes)
|
||||
end
|
||||
|
||||
# Case 3: Non-wormhole movement without :wormholes scope
|
||||
# Both systems must match the configured scopes
|
||||
true ->
|
||||
system_matches_any_scope?(from_system_static_info.system_class, scopes) and
|
||||
system_matches_any_scope?(to_system_static_info.system_class, scopes)
|
||||
end
|
||||
else
|
||||
false
|
||||
end
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
# Legacy support: :all scope
|
||||
def is_connection_valid(:all, from_solar_system_id, to_solar_system_id),
|
||||
do: from_solar_system_id != to_solar_system_id
|
||||
|
||||
# Legacy support: :none scope
|
||||
def is_connection_valid(:none, _from_solar_system_id, _to_solar_system_id), do: false
|
||||
|
||||
# Legacy support: single atom scope (including :stargates which is used for connection type detection)
|
||||
def is_connection_valid(scope, from_solar_system_id, to_solar_system_id)
|
||||
when from_solar_system_id != to_solar_system_id do
|
||||
when is_atom(scope) and from_solar_system_id != to_solar_system_id do
|
||||
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
|
||||
@@ -712,7 +846,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
:stargates ->
|
||||
# For stargates, we need to check:
|
||||
# 1. Both systems are in known space (HS, LS, NS)
|
||||
# 1. Both systems are in known space (HS, LS, NS, Pochven)
|
||||
# 2. There is a known jump between them
|
||||
# 3. Neither system is prohibited
|
||||
from_system_static_info.system_class in @known_space and
|
||||
@@ -720,13 +854,21 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
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?())
|
||||
|
||||
_ ->
|
||||
# For other legacy scopes, convert to array and use new logic
|
||||
is_connection_valid(
|
||||
legacy_scope_to_scopes(scope),
|
||||
from_solar_system_id,
|
||||
to_solar_system_id
|
||||
)
|
||||
end
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
def is_connection_valid(_scope, _from_solar_system_id, _to_solar_system_id), do: false
|
||||
def is_connection_valid(_scopes, _from_solar_system_id, _to_solar_system_id), do: false
|
||||
|
||||
def get_connection_mark_eol_time(map_id, connection_id, default \\ DateTime.utc_now()) do
|
||||
WandererApp.Cache.get("map_#{map_id}:conn_#{connection_id}:mark_eol_time")
|
||||
@@ -746,6 +888,44 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check if a connection between two k-space systems is a wormhole connection.
|
||||
Returns true if:
|
||||
1. Both systems are k-space (not wormhole space)
|
||||
2. There is no known stargate between them
|
||||
|
||||
This is used to detect wormhole connections through k-space, like when
|
||||
a player jumps from low-sec to low-sec through a wormhole.
|
||||
"""
|
||||
def is_kspace_wormhole_connection?(from_solar_system_id, to_solar_system_id)
|
||||
when is_nil(from_solar_system_id) or is_nil(to_solar_system_id),
|
||||
do: false
|
||||
|
||||
def is_kspace_wormhole_connection?(from_solar_system_id, to_solar_system_id)
|
||||
when from_solar_system_id == to_solar_system_id,
|
||||
do: false
|
||||
|
||||
def is_kspace_wormhole_connection?(from_solar_system_id, to_solar_system_id) do
|
||||
with {:ok, from_info} <- get_system_static_info(from_solar_system_id),
|
||||
{:ok, to_info} <- get_system_static_info(to_solar_system_id) do
|
||||
from_is_wormhole = from_info.system_class in @wh_space
|
||||
to_is_wormhole = to_info.system_class in @wh_space
|
||||
|
||||
# Both must be k-space (not wormhole space)
|
||||
if not from_is_wormhole and not to_is_wormhole do
|
||||
# Check if there's a known stargate
|
||||
case find_solar_system_jump(from_solar_system_id, to_solar_system_id) do
|
||||
{:ok, []} -> true # No stargate = wormhole connection
|
||||
_ -> false # Stargate exists or error
|
||||
end
|
||||
else
|
||||
false
|
||||
end
|
||||
else
|
||||
_ -> false
|
||||
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) ->
|
||||
@@ -779,6 +959,13 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
WandererApp.Cache.delete("map_#{map_id}:conn_#{connection.id}:start_time")
|
||||
|
||||
# Clear linked_sig_eve_id on target system when connection is deleted
|
||||
# This ensures old signatures become orphaned and won't affect future connections
|
||||
SystemsImpl.update_system_linked_sig_eve_id(map_id, %{
|
||||
solar_system_id: location.solar_system_id,
|
||||
linked_sig_eve_id: nil
|
||||
})
|
||||
|
||||
_error ->
|
||||
:ok
|
||||
end
|
||||
@@ -901,9 +1088,6 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_time_status(_source_solar_system_id, _target_solar_system_id, _ship_size_type),
|
||||
do: @connection_time_status_default
|
||||
|
||||
defp get_new_time_status(_start_time, @connection_time_status_default),
|
||||
do: @connection_time_status_eol_24
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
Logger.error("Cannot start map #{map_id}: map not loaded")
|
||||
{:error, :map_not_loaded}
|
||||
|
||||
map ->
|
||||
_map ->
|
||||
with :ok <- AclsImpl.track_acls(acls |> Enum.map(& &1.access_list_id)) do
|
||||
@pubsub_client.subscribe(
|
||||
WandererApp.PubSub,
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule WandererApp.Map.Server.PingsImpl do
|
||||
|
||||
alias WandererApp.Map.Server.Impl
|
||||
|
||||
@ping_auto_expire_timeout :timer.minutes(15)
|
||||
# @ping_auto_expire_timeout :timer.minutes(15) # reserved for future use
|
||||
|
||||
def add_ping(
|
||||
map_id,
|
||||
@@ -72,17 +72,24 @@ defmodule WandererApp.Map.Server.PingsImpl do
|
||||
type: type
|
||||
} = _ping_info
|
||||
) do
|
||||
case WandererApp.MapPingsRepo.get_by_id(ping_id) do
|
||||
|
||||
result = WandererApp.MapPingsRepo.get_by_id(ping_id)
|
||||
|
||||
case result do
|
||||
{:ok,
|
||||
%{system: %{id: system_id, name: system_name, solar_system_id: solar_system_id}} = ping} ->
|
||||
with {:ok, character} <- WandererApp.Character.get_character(character_id),
|
||||
:ok <- WandererApp.MapPingsRepo.destroy(ping) do
|
||||
Logger.debug("Ping #{ping_id} destroyed successfully, broadcasting :ping_cancelled")
|
||||
|
||||
Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: solar_system_id,
|
||||
type: type
|
||||
})
|
||||
|
||||
Logger.debug("Broadcast :ping_cancelled sent for ping #{ping_id}")
|
||||
|
||||
# Broadcast rally point removal events to external clients (webhooks/SSE)
|
||||
if type == 1 do
|
||||
WandererApp.ExternalEvents.broadcast(map_id, :rally_point_removed, %{
|
||||
@@ -107,18 +114,45 @@ defmodule WandererApp.Map.Server.PingsImpl do
|
||||
Logger.error("Failed to destroy ping: #{inspect(error, pretty: true)}")
|
||||
end
|
||||
|
||||
# Handle case where ping exists but system was deleted (nil)
|
||||
{:ok, %{system: nil} = ping} ->
|
||||
case WandererApp.MapPingsRepo.destroy(ping) do
|
||||
:ok ->
|
||||
Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: nil,
|
||||
type: type
|
||||
})
|
||||
|
||||
error ->
|
||||
Logger.error("Failed to destroy orphaned ping: #{inspect(error, pretty: true)}")
|
||||
end
|
||||
|
||||
{:error, %Ash.Error.Query.NotFound{}} ->
|
||||
# Ping already deleted (possibly by cascade deletion from map/system/character removal,
|
||||
# auto-expiry, or concurrent cancellation). This is not an error - the desired state
|
||||
# (ping is gone) is already achieved. Just broadcast the cancellation event.
|
||||
Logger.debug(
|
||||
"Ping #{ping_id} not found during cancellation - already deleted, skipping broadcast"
|
||||
)
|
||||
# auto-expiry, or concurrent cancellation). Broadcast cancellation so frontend updates.
|
||||
Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: nil,
|
||||
type: type
|
||||
})
|
||||
|
||||
:ok
|
||||
|
||||
error ->
|
||||
Logger.error("Failed to fetch ping for cancellation: #{inspect(error, pretty: true)}")
|
||||
{:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]}} ->
|
||||
# Same as above, but Ash wraps NotFound inside Invalid in some cases
|
||||
Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: nil,
|
||||
type: type
|
||||
})
|
||||
|
||||
:ok
|
||||
|
||||
other ->
|
||||
Logger.error(
|
||||
"Failed to cancel ping #{ping_id}: unexpected result from get_by_id: #{inspect(other, pretty: true)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -167,19 +167,26 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
updated_count: length(updated_ids),
|
||||
removed_count: length(removed_ids)
|
||||
})
|
||||
|
||||
# Always return :ok - external event failures should not affect the main operation
|
||||
:ok
|
||||
end
|
||||
|
||||
defp remove_signature(map_id, sig, system, delete_conn?) do
|
||||
# optionally remove the linked connection
|
||||
if delete_conn? && sig.linked_system_id do
|
||||
# Check if this signature is the active one for the target system
|
||||
# This prevents deleting connections when old/orphan signatures are removed
|
||||
is_active = sig.linked_system_id && is_active_signature_for_target?(map_id, sig)
|
||||
|
||||
# Only delete connection if this signature is the active one
|
||||
if delete_conn? && is_active do
|
||||
ConnectionsImpl.delete_connection(map_id, %{
|
||||
solar_system_source_id: system.solar_system_id,
|
||||
solar_system_target_id: sig.linked_system_id
|
||||
})
|
||||
end
|
||||
|
||||
# clear any linked_sig_eve_id on the target system
|
||||
if sig.linked_system_id do
|
||||
# Only clear linked_sig_eve_id if this signature is the active one
|
||||
if is_active do
|
||||
SystemsImpl.update_system_linked_sig_eve_id(map_id, %{
|
||||
solar_system_id: sig.linked_system_id,
|
||||
linked_sig_eve_id: nil
|
||||
@@ -190,6 +197,16 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
|> MapSystemSignature.destroy!()
|
||||
end
|
||||
|
||||
defp is_active_signature_for_target?(map_id, sig) do
|
||||
case MapSystem.read_by_map_and_solar_system(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: sig.linked_system_id
|
||||
}) do
|
||||
{:ok, target_system} -> target_system.linked_sig_eve_id == sig.eve_id
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
def apply_update_signature(
|
||||
map_id,
|
||||
%MapSystemSignature{} = existing,
|
||||
@@ -256,6 +273,37 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
|
||||
defp maybe_update_connection_mass_status(_map_id, _old_sig, _updated_sig), do: :ok
|
||||
|
||||
@doc """
|
||||
Wrapper for updating a signature's linked_system_id with logging.
|
||||
Logs all unlink operations (when linked_system_id is set to nil) with context
|
||||
to help diagnose unexpected unlinking issues.
|
||||
"""
|
||||
def update_signature_linked_system(signature, %{linked_system_id: nil} = params) do
|
||||
# Log all unlink operations with context for debugging
|
||||
Logger.warning(
|
||||
"[Signature Unlink] eve_id=#{signature.eve_id} " <>
|
||||
"system_id=#{signature.system_id} " <>
|
||||
"old_linked_system_id=#{signature.linked_system_id} " <>
|
||||
"stacktrace=#{format_stacktrace()}"
|
||||
)
|
||||
|
||||
MapSystemSignature.update_linked_system(signature, params)
|
||||
end
|
||||
|
||||
def update_signature_linked_system(signature, params) do
|
||||
MapSystemSignature.update_linked_system(signature, params)
|
||||
end
|
||||
|
||||
defp format_stacktrace do
|
||||
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
|
||||
|
||||
stacktrace
|
||||
|> Enum.take(10)
|
||||
|> Enum.map_join(" <- ", fn {mod, fun, arity, _} ->
|
||||
"#{inspect(mod)}.#{fun}/#{arity}"
|
||||
end)
|
||||
end
|
||||
|
||||
defp track_activity(event, map_id, solar_system_id, user_id, character_id, signatures) do
|
||||
ActivityTracker.track_map_event(event, %{
|
||||
map_id: map_id,
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Map.Server.Impl
|
||||
alias WandererApp.Map.Server.SignaturesImpl
|
||||
|
||||
@ddrt Application.compile_env(:wanderer_app, :ddrt)
|
||||
@system_auto_expire_minutes 15
|
||||
@@ -129,8 +130,8 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
def remove_system_comment(
|
||||
map_id,
|
||||
comment_id,
|
||||
user_id,
|
||||
character_id
|
||||
_user_id,
|
||||
_character_id
|
||||
) do
|
||||
{:ok, %{system_id: system_id} = comment} =
|
||||
WandererApp.MapSystemCommentRepo.get_by_id(comment_id)
|
||||
@@ -146,6 +147,30 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end
|
||||
|
||||
def cleanup_systems(map_id) do
|
||||
# Defensive check: Skip cleanup if cache appears invalid
|
||||
# This prevents incorrectly deleting systems when cache is empty due to
|
||||
# race conditions during map restart or cache corruption
|
||||
case WandererApp.Map.get_map(map_id) do
|
||||
{:error, :not_found} ->
|
||||
Logger.warning(
|
||||
"[cleanup_systems] Skipping map #{map_id} - cache miss detected, " <>
|
||||
"map data not found in cache"
|
||||
)
|
||||
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :cleanup_systems, :cache_miss],
|
||||
%{system_time: System.system_time()},
|
||||
%{map_id: map_id}
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
{:ok, _map} ->
|
||||
do_cleanup_systems(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_cleanup_systems(map_id) do
|
||||
expired_systems =
|
||||
map_id
|
||||
|> WandererApp.Map.list_systems!()
|
||||
@@ -309,7 +334,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
map_id
|
||||
|> WandererApp.MapSystemRepo.remove_from_map(solar_system_id)
|
||||
|> case do
|
||||
{:ok, result} ->
|
||||
{:ok, _result} ->
|
||||
:ok = WandererApp.Map.remove_system(map_id, solar_system_id)
|
||||
@ddrt.delete([solar_system_id], "rtree_#{map_id}")
|
||||
Impl.broadcast!(map_id, :systems_removed, [solar_system_id])
|
||||
@@ -383,6 +408,16 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|> Enum.each(fn connection ->
|
||||
try do
|
||||
Logger.debug(fn -> "Removing connection from map: #{inspect(connection)}" end)
|
||||
|
||||
# Audit logging for cascade deletion (no user/character context)
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_connection_removed, %{
|
||||
character_id: nil,
|
||||
user_id: nil,
|
||||
map_id: map_id,
|
||||
solar_system_source_id: connection.solar_system_source,
|
||||
solar_system_target_id: connection.solar_system_target
|
||||
})
|
||||
|
||||
:ok = WandererApp.MapConnectionRepo.destroy(map_id, connection)
|
||||
:ok = WandererApp.Map.remove_connection(map_id, connection)
|
||||
Impl.broadcast!(map_id, :remove_connections, [connection])
|
||||
@@ -393,35 +428,77 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end)
|
||||
end
|
||||
|
||||
# When destination systems are deleted, unlink signatures instead of destroying them.
|
||||
# This preserves the user's scan data while removing the stale link.
|
||||
defp cleanup_linked_signatures(map_id, removed_solar_system_ids) do
|
||||
removed_solar_system_ids
|
||||
|> Enum.map(fn solar_system_id ->
|
||||
WandererApp.Api.MapSystemSignature.by_linked_system_id!(solar_system_id)
|
||||
end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq_by(& &1.system_id)
|
||||
|> Enum.each(fn s ->
|
||||
try do
|
||||
{:ok, %{eve_id: eve_id, system: system}} = s |> Ash.load([:system])
|
||||
:ok = Ash.destroy!(s)
|
||||
# Group signatures by their source system for efficient broadcasting
|
||||
signatures_by_system =
|
||||
removed_solar_system_ids
|
||||
|> Enum.flat_map(fn solar_system_id ->
|
||||
WandererApp.Api.MapSystemSignature.by_linked_system_id!(solar_system_id)
|
||||
end)
|
||||
|> Enum.uniq_by(& &1.id)
|
||||
|> Enum.group_by(fn sig -> sig.system_id end)
|
||||
|
||||
# Handle case where parent system was already deleted
|
||||
case system do
|
||||
nil ->
|
||||
Logger.warning(
|
||||
"[cleanup_linked_signatures] signature #{eve_id} destroyed (parent system already deleted)"
|
||||
)
|
||||
signatures_by_system
|
||||
|> Enum.each(fn {_system_id, signatures} ->
|
||||
signatures
|
||||
|> Enum.each(fn sig ->
|
||||
try do
|
||||
{:ok, %{eve_id: eve_id, system: system}} = sig |> Ash.load([:system])
|
||||
|
||||
%{solar_system_id: solar_system_id} ->
|
||||
Logger.warning(
|
||||
"[cleanup_linked_signatures] for system #{solar_system_id}: #{inspect(eve_id)}"
|
||||
)
|
||||
# Clear the linked_system_id instead of destroying the signature
|
||||
# Use the wrapper to log unlink operations
|
||||
case SignaturesImpl.update_signature_linked_system(sig, %{
|
||||
linked_system_id: nil
|
||||
}) do
|
||||
{:ok, _updated_sig} ->
|
||||
case system do
|
||||
nil ->
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] signature #{eve_id} unlinked (parent system already deleted)"
|
||||
end)
|
||||
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
%{solar_system_id: solar_system_id} ->
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] unlinked signature #{eve_id} in system #{solar_system_id}"
|
||||
end)
|
||||
|
||||
# Audit logging for cascade unlink (no user/character context)
|
||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_unlinked, %{
|
||||
character_id: nil,
|
||||
user_id: nil,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id,
|
||||
signatures: [eve_id]
|
||||
})
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error(
|
||||
"[cleanup_linked_signatures] Failed to unlink signature #{sig.eve_id}: #{inspect(error)}"
|
||||
)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Failed to cleanup linked signature: #{inspect(e)}")
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Failed to cleanup linked signature: #{inspect(e)}")
|
||||
end)
|
||||
|
||||
# Broadcast once per source system after all its signatures are processed
|
||||
case List.first(signatures) do
|
||||
%{system: %{solar_system_id: solar_system_id}} ->
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
|
||||
_ ->
|
||||
# Try to get the system info if not preloaded
|
||||
case List.first(signatures) |> Ash.load([:system]) do
|
||||
{:ok, %{system: %{solar_system_id: solar_system_id}}} ->
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -446,8 +523,62 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end)
|
||||
end
|
||||
|
||||
def maybe_add_system(map_id, location, old_location, map_opts)
|
||||
def maybe_add_system(map_id, location, old_location, map_opts, scopes \\ nil)
|
||||
|
||||
def maybe_add_system(map_id, location, old_location, map_opts, scopes)
|
||||
when not is_nil(location) do
|
||||
alias WandererApp.Map.Server.ConnectionsImpl
|
||||
|
||||
# Check if the system matches the map's configured scopes before adding
|
||||
should_add =
|
||||
case scopes do
|
||||
nil ->
|
||||
true
|
||||
|
||||
[] ->
|
||||
true
|
||||
|
||||
scopes when is_list(scopes) ->
|
||||
# First check: does the location directly match scopes?
|
||||
if ConnectionsImpl.can_add_location(scopes, location.solar_system_id) do
|
||||
true
|
||||
else
|
||||
# Second check: wormhole border behavior
|
||||
# If :wormholes scope is enabled AND old_location is a wormhole,
|
||||
# allow this system to be added as a border system (so you can see
|
||||
# where your wormhole exits to)
|
||||
wormhole_border_from_wh_space =
|
||||
:wormholes in scopes and
|
||||
not is_nil(old_location) and
|
||||
ConnectionsImpl.can_add_location([:wormholes], old_location.solar_system_id)
|
||||
|
||||
# Third check: k-space wormhole connection
|
||||
# If :wormholes scope is enabled AND there's no stargate between the systems,
|
||||
# this is a wormhole connection through k-space - add both systems
|
||||
kspace_wormhole_connection =
|
||||
:wormholes in scopes and
|
||||
not is_nil(old_location) and
|
||||
not is_nil(old_location.solar_system_id) and
|
||||
ConnectionsImpl.is_kspace_wormhole_connection?(
|
||||
old_location.solar_system_id,
|
||||
location.solar_system_id
|
||||
)
|
||||
|
||||
wormhole_border_from_wh_space or kspace_wormhole_connection
|
||||
end
|
||||
end
|
||||
|
||||
if should_add do
|
||||
do_add_system_from_location(map_id, location, old_location, map_opts)
|
||||
else
|
||||
# System filtered out by scope settings - this is expected behavior
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_add_system(_map_id, _location, _old_location, _map_opts, _scopes), do: :ok
|
||||
|
||||
defp do_add_system_from_location(map_id, location, old_location, map_opts) do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :system_addition, :start],
|
||||
%{system_time: System.system_time()},
|
||||
@@ -526,12 +657,14 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|> case do
|
||||
{:ok, solar_system_info} ->
|
||||
# Use upsert instead of create - handles race conditions gracefully
|
||||
# visible: true ensures previously-deleted systems become visible again
|
||||
WandererApp.MapSystemRepo.upsert(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: location.solar_system_id,
|
||||
name: solar_system_info.solar_system_name,
|
||||
position_x: position.x,
|
||||
position_y: position.y
|
||||
position_y: position.y,
|
||||
visible: true
|
||||
})
|
||||
|> case do
|
||||
{:ok, system} ->
|
||||
@@ -653,8 +786,6 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_add_system(_map_id, _location, _old_location, _map_opts), do: :ok
|
||||
|
||||
defp do_add_system(
|
||||
map_id,
|
||||
%{
|
||||
@@ -679,7 +810,11 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|
||||
_ ->
|
||||
%{x: x, y: y} =
|
||||
WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name, map_opts)
|
||||
WandererApp.Map.PositionCalculator.get_new_system_position(
|
||||
nil,
|
||||
rtree_name,
|
||||
map_opts
|
||||
)
|
||||
|
||||
%{"x" => x, "y" => y}
|
||||
end
|
||||
@@ -742,7 +877,10 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
})
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to get system static info for #{solar_system_id}: #{inspect(reason)}")
|
||||
Logger.error(
|
||||
"Failed to get system static info for #{solar_system_id}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:error, :system_info_not_found}
|
||||
end
|
||||
end
|
||||
@@ -775,7 +913,10 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
:ok
|
||||
|
||||
{:error, reason} = error ->
|
||||
Logger.error("Failed to add system #{solar_system_id} to map #{map_id}: #{inspect(reason)}")
|
||||
Logger.error(
|
||||
"Failed to add system #{solar_system_id} to map #{map_id}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
error
|
||||
end
|
||||
else
|
||||
@@ -863,10 +1004,8 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
updated_system
|
||||
end
|
||||
|
||||
defp maybe_update_labels(system, _labels), do: system
|
||||
|
||||
defp maybe_update_labels(
|
||||
%{name: old_labels} = system,
|
||||
%{labels: old_labels} = system,
|
||||
labels
|
||||
)
|
||||
when not is_nil(labels) and old_labels != labels do
|
||||
@@ -980,12 +1119,16 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
# ADDITIVE: Also broadcast to external event system (webhooks/WebSocket)
|
||||
# This may fail if the relay is not available (e.g., in tests), which is fine
|
||||
WandererApp.ExternalEvents.broadcast(map_id, :system_metadata_changed, %{
|
||||
system_id: updated_system.id,
|
||||
solar_system_id: updated_system.solar_system_id,
|
||||
name: updated_system.name,
|
||||
temporary_name: updated_system.temporary_name,
|
||||
labels: updated_system.labels,
|
||||
description: updated_system.description,
|
||||
status: updated_system.status
|
||||
status: updated_system.status,
|
||||
locked: updated_system.locked,
|
||||
position_x: updated_system.position_x,
|
||||
position_y: updated_system.position_y
|
||||
})
|
||||
|
||||
:ok
|
||||
|
||||
@@ -128,7 +128,7 @@ defmodule WandererApp.Maps do
|
||||
tracked: tracked
|
||||
}
|
||||
|
||||
defp get_map_characters(%{id: map_id} = map) do
|
||||
defp get_map_characters(%{id: map_id} = _map) do
|
||||
WandererApp.Cache.lookup!("map_characters-#{map_id}")
|
||||
|> case do
|
||||
nil ->
|
||||
@@ -174,9 +174,11 @@ defmodule WandererApp.Maps do
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}
|
||||
|
||||
# Cache with 5 minute TTL so ACL changes are picked up even when map server isn't running
|
||||
WandererApp.Cache.insert(
|
||||
"map_characters-#{map_id}",
|
||||
map_characters
|
||||
map_characters,
|
||||
ttl: :timer.minutes(5)
|
||||
)
|
||||
|
||||
{:ok, map_characters}
|
||||
|
||||
@@ -99,7 +99,7 @@ defmodule WandererApp.MapConnectionRepo do
|
||||
def get_by_id(map_id, id) do
|
||||
# Use read_by_map action which doesn't have the FilterConnectionsByActorMap preparation
|
||||
# that was causing "filter being false" errors in tests
|
||||
import Ash.Query
|
||||
require Ash.Query
|
||||
|
||||
WandererApp.Api.MapConnection
|
||||
|> Ash.Query.for_read(:read_by_map, %{map_id: map_id})
|
||||
|
||||
@@ -29,6 +29,34 @@ defmodule WandererApp.MapPingsRepo do
|
||||
def get_by_inserted_before(inserted_before_date),
|
||||
do: WandererApp.Api.MapPing.by_inserted_before(inserted_before_date)
|
||||
|
||||
@doc """
|
||||
Returns all pings that have orphaned relationships (nil system, character, or map)
|
||||
or where the system has been soft-deleted (visible = false).
|
||||
These pings should be cleaned up as they can no longer be properly displayed or cancelled.
|
||||
"""
|
||||
def get_orphaned_pings() do
|
||||
# Use :all_pings action which has no actor filtering (unlike primary :read)
|
||||
case WandererApp.Api.MapPing |> Ash.Query.for_read(:all_pings) |> Ash.read() do
|
||||
{:ok, pings} ->
|
||||
# Load relationships and filter for orphaned ones
|
||||
orphaned =
|
||||
pings
|
||||
|> Enum.map(fn ping ->
|
||||
{:ok, loaded} = ping |> Ash.load([:system, :character, :map], authorize?: false)
|
||||
loaded
|
||||
end)
|
||||
|> Enum.filter(fn ping ->
|
||||
is_nil(ping.system) or is_nil(ping.character) or is_nil(ping.map) or
|
||||
(not is_nil(ping.system) and ping.system.visible == false)
|
||||
end)
|
||||
|
||||
{:ok, orphaned}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
def create(ping), do: ping |> WandererApp.Api.MapPing.new()
|
||||
def create!(ping), do: ping |> WandererApp.Api.MapPing.new!()
|
||||
|
||||
@@ -39,5 +67,23 @@ defmodule WandererApp.MapPingsRepo do
|
||||
:ok
|
||||
end
|
||||
|
||||
def destroy(_ping_id), do: :ok
|
||||
@doc """
|
||||
Deletes all pings for a given map. Use with caution - for cleanup purposes.
|
||||
"""
|
||||
def delete_all_for_map(map_id) do
|
||||
case get_by_map(map_id) do
|
||||
{:ok, pings} ->
|
||||
Logger.info("[MapPingsRepo] Deleting #{length(pings)} pings for map #{map_id}")
|
||||
|
||||
Enum.each(pings, fn ping ->
|
||||
Logger.info("[MapPingsRepo] Deleting ping #{ping.id} (type: #{ping.type})")
|
||||
Ash.destroy!(ping)
|
||||
end)
|
||||
|
||||
{:ok, length(pings)}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -84,7 +84,7 @@ defmodule WandererApp.MapRepo do
|
||||
end
|
||||
end
|
||||
|
||||
error in Ash.Error.Query.NotFound ->
|
||||
_error in Ash.Error.Query.NotFound ->
|
||||
Logger.debug("Map not found with slug: #{slug}")
|
||||
{:error, :not_found}
|
||||
|
||||
|
||||
265
lib/wanderer_app/route_builder_client.ex
Normal file
265
lib/wanderer_app/route_builder_client.ex
Normal file
@@ -0,0 +1,265 @@
|
||||
defmodule WandererApp.RouteBuilderClient do
|
||||
@moduledoc """
|
||||
HTTP client for the local route builder service.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
@timeout_opts [pool_timeout: 5_000, receive_timeout: :timer.seconds(30)]
|
||||
@loot_dir Path.join(["repo", "data", "route_by_systems"])
|
||||
|
||||
def find_closest(
|
||||
%{
|
||||
origin: origin,
|
||||
flag: flag,
|
||||
connections: connections,
|
||||
avoid: avoid,
|
||||
count: count,
|
||||
type: type,
|
||||
security_type: security_type
|
||||
} = payload
|
||||
) do
|
||||
url = "#{WandererApp.Env.route_builder_base_url()}/route/findClosest"
|
||||
|
||||
routes_settings = Map.get(payload, :routes_settings, %{})
|
||||
destinations = destinations_for(type, security_type, routes_settings)
|
||||
|
||||
payload = %{
|
||||
origin: origin,
|
||||
flag: flag,
|
||||
connections: connections || [],
|
||||
avoid: avoid || [],
|
||||
destinations: destinations,
|
||||
count: count || 1
|
||||
}
|
||||
|
||||
case Req.post(url, Keyword.merge([json: payload], @timeout_opts)) do
|
||||
{:ok, %{status: status, body: body}} when status in [200, 201] ->
|
||||
{:ok, body}
|
||||
|
||||
{:ok, %{status: status, body: body}} ->
|
||||
Logger.warning("[RouteBuilderClient] Unexpected status: #{status}")
|
||||
{:error, {:unexpected_status, status, body}}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("[RouteBuilderClient] Request failed: #{inspect(reason)}")
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp destinations_for(type, security_type, routes_settings) do
|
||||
case normalize_type(type) do
|
||||
:thera ->
|
||||
thera_destinations(routes_settings, security_type)
|
||||
|
||||
:turnur ->
|
||||
turnur_destinations(routes_settings, security_type)
|
||||
|
||||
_ ->
|
||||
case load_loot_data(type) do
|
||||
{:ok, %{"system_ids_by_band" => by_band}} ->
|
||||
high = Map.get(by_band, "high", [])
|
||||
low = Map.get(by_band, "low", [])
|
||||
pick_by_band(high, low, security_type)
|
||||
|
||||
{:ok, %{"system_ids" => system_ids}} when is_list(system_ids) ->
|
||||
filter_by_security(system_ids, security_type)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("[RouteBuilderClient] Failed to load loot data: #{inspect(reason)}")
|
||||
[]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp thera_destinations(routes_settings, security_type) do
|
||||
{:ok, thera_chains} = WandererApp.Server.TheraDataFetcher.get_chain_pairs(routes_settings)
|
||||
|
||||
system_ids =
|
||||
thera_chains
|
||||
|> Enum.map(fn %{first: first, second: second} ->
|
||||
pick_thera_destination(first, second)
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.uniq()
|
||||
|
||||
filter_by_security(system_ids, security_type)
|
||||
end
|
||||
|
||||
defp turnur_destinations(routes_settings, security_type) do
|
||||
{:ok, turnur_chains} = WandererApp.Server.TurnurDataFetcher.get_chain_pairs(routes_settings)
|
||||
|
||||
system_ids =
|
||||
turnur_chains
|
||||
|> Enum.map(fn %{first: first, second: second} ->
|
||||
pick_turnur_destination(first, second)
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.uniq()
|
||||
|
||||
filter_by_security(system_ids, security_type)
|
||||
end
|
||||
|
||||
defp filter_by_security(system_ids, security_type) do
|
||||
case normalize_security_type(security_type) do
|
||||
"high" ->
|
||||
Enum.filter(system_ids, fn system_id ->
|
||||
case system_security(system_id) do
|
||||
{:ok, security} -> security >= 0.5
|
||||
_ -> false
|
||||
end
|
||||
end)
|
||||
|
||||
"low" ->
|
||||
Enum.filter(system_ids, fn system_id ->
|
||||
case system_security(system_id) do
|
||||
{:ok, security} -> security > 0.0 and security < 0.5
|
||||
_ -> false
|
||||
end
|
||||
end)
|
||||
|
||||
_ ->
|
||||
system_ids
|
||||
end
|
||||
end
|
||||
|
||||
defp system_security(system_id) do
|
||||
case WandererApp.CachedInfo.get_system_static_info(system_id) do
|
||||
{:ok, %{security: security}} -> parse_security(security)
|
||||
_ -> {:error, :missing_security}
|
||||
end
|
||||
end
|
||||
|
||||
defp pick_thera_destination(first, second) do
|
||||
first_is_thera = is_thera_system?(first)
|
||||
second_is_thera = is_thera_system?(second)
|
||||
|
||||
cond do
|
||||
first_is_thera and not second_is_thera -> second
|
||||
second_is_thera and not first_is_thera -> first
|
||||
true -> second
|
||||
end
|
||||
end
|
||||
|
||||
defp is_thera_system?(system_id) do
|
||||
case WandererApp.CachedInfo.get_system_static_info(system_id) do
|
||||
{:ok, %{system_class: 12}} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp pick_turnur_destination(first, second) do
|
||||
first_is_turnur = is_turnur_system?(first)
|
||||
second_is_turnur = is_turnur_system?(second)
|
||||
|
||||
cond do
|
||||
first_is_turnur and not second_is_turnur -> second
|
||||
second_is_turnur and not first_is_turnur -> first
|
||||
true -> second
|
||||
end
|
||||
end
|
||||
|
||||
defp is_turnur_system?(system_id) do
|
||||
case WandererApp.CachedInfo.get_system_static_info(system_id) do
|
||||
{:ok, %{solar_system_name: name}} when is_binary(name) ->
|
||||
String.downcase(name) == "turnur"
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_security(security) when is_float(security), do: {:ok, security}
|
||||
defp parse_security(security) when is_integer(security), do: {:ok, security * 1.0}
|
||||
|
||||
defp parse_security(security) when is_binary(security) do
|
||||
case Float.parse(security) do
|
||||
{value, _} -> {:ok, value}
|
||||
_ -> {:error, :invalid_security}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_security(_), do: {:error, :invalid_security}
|
||||
|
||||
defp normalize_security_type("high"), do: "high"
|
||||
defp normalize_security_type(:high), do: "high"
|
||||
defp normalize_security_type("hight"), do: "high"
|
||||
defp normalize_security_type(:hight), do: "high"
|
||||
defp normalize_security_type("low"), do: "low"
|
||||
defp normalize_security_type(:low), do: "low"
|
||||
defp normalize_security_type(_), do: "both"
|
||||
|
||||
def stations_for(type) do
|
||||
case normalize_type(type) do
|
||||
:thera ->
|
||||
%{}
|
||||
|
||||
:turnur ->
|
||||
%{}
|
||||
|
||||
_ ->
|
||||
case load_loot_data(type) do
|
||||
{:ok, %{"system_stations" => system_stations}} when is_map(system_stations) ->
|
||||
system_stations
|
||||
|
||||
{:ok, _} ->
|
||||
%{}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("[RouteBuilderClient] Failed to load loot stations: #{inspect(reason)}")
|
||||
%{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp pick_by_band(high, _low, "high"), do: high
|
||||
defp pick_by_band(high, _low, :high), do: high
|
||||
defp pick_by_band(high, _low, "hight"), do: high
|
||||
defp pick_by_band(high, _low, :hight), do: high
|
||||
defp pick_by_band(_high, low, "low"), do: low
|
||||
defp pick_by_band(_high, low, :low), do: low
|
||||
defp pick_by_band(high, low, _), do: high ++ low
|
||||
|
||||
defp load_loot_data("blueLoot"), do: load_loot_file("blueloot.json")
|
||||
defp load_loot_data(:blueLoot), do: load_loot_file("blueloot.json")
|
||||
defp load_loot_data("redLoot"), do: load_loot_file("redloot.json")
|
||||
defp load_loot_data(:redLoot), do: load_loot_file("redloot.json")
|
||||
defp load_loot_data("so_cleaning"), do: load_loot_file("ss_cleaning.json")
|
||||
defp load_loot_data(:so_cleaning), do: load_loot_file("ss_cleaning.json")
|
||||
defp load_loot_data("trade_hubs"), do: load_loot_file("trade_hubs.json")
|
||||
defp load_loot_data(:trade_hubs), do: load_loot_file("trade_hubs.json")
|
||||
defp load_loot_data(_), do: load_loot_file("blueloot.json")
|
||||
|
||||
defp normalize_type("thera"), do: :thera
|
||||
defp normalize_type(:thera), do: :thera
|
||||
defp normalize_type("turnur"), do: :turnur
|
||||
defp normalize_type(:turnur), do: :turnur
|
||||
defp normalize_type("so_cleaning"), do: :so_cleaning
|
||||
defp normalize_type(:so_cleaning), do: :so_cleaning
|
||||
defp normalize_type("trade_hubs"), do: :trade_hubs
|
||||
defp normalize_type(:trade_hubs), do: :trade_hubs
|
||||
defp normalize_type(type), do: type
|
||||
|
||||
defp load_loot_file(filename) do
|
||||
key = {__MODULE__, :loot_data, filename}
|
||||
|
||||
case :persistent_term.get(key, :missing) do
|
||||
:missing ->
|
||||
path = Path.join([:code.priv_dir(:wanderer_app), @loot_dir, filename])
|
||||
|
||||
with {:ok, body} <- File.read(path),
|
||||
{:ok, json} <- Jason.decode(body) do
|
||||
:persistent_term.put(key, json)
|
||||
{:ok, json}
|
||||
else
|
||||
error -> error
|
||||
end
|
||||
|
||||
cached ->
|
||||
{:ok, cached}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -487,15 +487,6 @@ defmodule WandererApp.SecurityAudit do
|
||||
|
||||
# Private functions
|
||||
|
||||
defp store_audit_entry(_audit_entry) do
|
||||
# Handle async processing if enabled
|
||||
# if async_enabled?() do
|
||||
# WandererApp.SecurityAudit.AsyncProcessor.log_event(audit_entry)
|
||||
# else
|
||||
# do_store_audit_entry(audit_entry)
|
||||
# end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def do_store_audit_entry(audit_entry) do
|
||||
# Ensure event_type is properly formatted
|
||||
@@ -631,11 +622,6 @@ defmodule WandererApp.SecurityAudit do
|
||||
end
|
||||
end
|
||||
|
||||
defp async_enabled? do
|
||||
Application.get_env(:wanderer_app, __MODULE__, [])
|
||||
|> Keyword.get(:async, false)
|
||||
end
|
||||
|
||||
defp emit_telemetry_event(audit_entry) do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :security_audit],
|
||||
|
||||
@@ -5,7 +5,11 @@ defmodule WandererApp.Test.Logger do
|
||||
"""
|
||||
|
||||
@callback info(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback info(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
@callback error(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback error(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
@callback warning(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback warning(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
@callback debug(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback debug(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
end
|
||||
|
||||
@@ -9,12 +9,24 @@ defmodule WandererApp.Test.LoggerStub do
|
||||
@impl true
|
||||
def info(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def info(_message, _metadata), do: :ok
|
||||
|
||||
@impl true
|
||||
def error(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def error(_message, _metadata), do: :ok
|
||||
|
||||
@impl true
|
||||
def warning(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def warning(_message, _metadata), do: :ok
|
||||
|
||||
@impl true
|
||||
def debug(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def debug(_message, _metadata), do: :ok
|
||||
end
|
||||
|
||||
@@ -124,7 +124,7 @@ defmodule WandererApp.Vault do
|
||||
end)
|
||||
end
|
||||
|
||||
defp find_fallback_module_to_decrypt(config, ciphertext) do
|
||||
defp find_fallback_module_to_decrypt(config, _ciphertext) do
|
||||
Enum.find(config[:ciphers], fn {label, _} ->
|
||||
label == :fallback
|
||||
end)
|
||||
|
||||
@@ -12,7 +12,6 @@ defmodule WandererAppWeb.ApiRouter do
|
||||
"""
|
||||
|
||||
use Phoenix.Router
|
||||
import WandererAppWeb.ApiRouterHelpers
|
||||
alias WandererAppWeb.{ApiRoutes, ApiRouter.RouteSpec}
|
||||
require Logger
|
||||
|
||||
@@ -171,7 +170,7 @@ defmodule WandererAppWeb.ApiRouter do
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp find_similar_routes(path_info, version) do
|
||||
defp find_similar_routes(path_info, _version) do
|
||||
# Find routes with similar paths in current or other versions
|
||||
all_routes = ApiRoutes.table()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user