mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-02-13 01:16:06 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5af43dca1 | ||
|
|
549fa1d2cf | ||
|
|
34a4d5dc9f | ||
|
|
15142f188b | ||
|
|
daf4a81568 | ||
|
|
8c5340e911 | ||
|
|
6b0f636964 | ||
|
|
09ebd29eb4 | ||
|
|
35bd5645bf | ||
|
|
a6948ee1da | ||
|
|
98b3f5855c | ||
|
|
11ad48b40a | ||
|
|
ecd018abfe | ||
|
|
f430f74e98 | ||
|
|
9e146d1117 | ||
|
|
2005e6f3dd | ||
|
|
ab066a342f | ||
|
|
82b4a5f35a | ||
|
|
ca3a25b836 | ||
|
|
8e46c01a8a | ||
|
|
9d9fa3c6b5 | ||
|
|
0e24501225 | ||
|
|
25a3d8951e | ||
|
|
f4ddc8dc8b | ||
|
|
ac9b46e24d | ||
|
|
40d0a0777a | ||
|
|
608792d99a | ||
|
|
dc9e0c821e | ||
|
|
79d4fd0e43 | ||
|
|
5d03c1ecc7 | ||
|
|
2eef05495e | ||
|
|
f724455a1e | ||
|
|
33bbb3425c | ||
|
|
a919bd9038 | ||
|
|
8ae34cd94a | ||
|
|
2f38da52e8 | ||
|
|
a7d6b06332 | ||
|
|
8f6da817db | ||
|
|
378f22a1ef | ||
|
|
14730097b2 | ||
|
|
e8bff3098a |
56
CHANGELOG.md
56
CHANGELOG.md
@@ -2,6 +2,62 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.95.0](https://github.com/wanderer-industries/wanderer/compare/v1.94.0...v1.95.0) (2026-02-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* subscriptions: Added top map donators support
|
||||
|
||||
* Added lost files
|
||||
|
||||
* Added paywall for RoutesBy widget
|
||||
|
||||
* removed unnecessary env variable for routes
|
||||
|
||||
* Add systems with Security Status cleaning. Add trade hubs. Add ability to store data for this widget
|
||||
|
||||
* Add Routes By widget. Allow to find nearest blue loot and red loot stations. Added ability to set waypoint to station.
|
||||
|
||||
* auto add system on sig addition
|
||||
|
||||
* map: Reviewed changes
|
||||
|
||||
* map: Logic for multiple owner updates
|
||||
|
||||
* map: wip New Dialog for Structure Owners
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* signatures: Fixed back linked sigs data sync and leading to system override issues
|
||||
|
||||
* signatures: Moved C1/C2/C3 and C4/C5 to the bottom of the available list
|
||||
|
||||
* use cache for sse
|
||||
|
||||
* adding system when linked signature is provided
|
||||
|
||||
* saving updates to unknown sigs
|
||||
|
||||
* wh position and sig type change
|
||||
|
||||
* api updates and linked sig addition
|
||||
|
||||
* api fixes and format
|
||||
|
||||
* Wrong file added to commits
|
||||
|
||||
## [v1.94.0](https://github.com/wanderer-industries/wanderer/compare/v1.93.0...v1.94.0) (2026-02-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* administration: Added registered characters admin view with cort/ally info, sort and filter options
|
||||
|
||||
## [v1.93.0](https://github.com/wanderer-industries/wanderer/compare/v1.92.0...v1.93.0) (2026-02-08)
|
||||
|
||||
|
||||
|
||||
@@ -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,106 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
onWaypointSet,
|
||||
systemId,
|
||||
systemIdFrom,
|
||||
hubs,
|
||||
routes,
|
||||
}) => {
|
||||
const getWaypointMenu = useWaypointMenu(onWaypointSet);
|
||||
const getJumpPlannerMenu = useJumpPlannerMenu(systems, systemIdFrom);
|
||||
const { toggleHubCommand, hubs } = useRouteProvider();
|
||||
const getOwnOnlineCharacters = useGetOwnOnlineCharacters();
|
||||
|
||||
const getStationWaypointItems = useCallback(
|
||||
(destinationId: string, chars: CharacterTypeRaw[]): MenuItem[] => [
|
||||
{
|
||||
label: 'Set Destination',
|
||||
icon: PrimeIcons.SEND,
|
||||
command: () => {
|
||||
onWaypointSet({
|
||||
fromBeginning: true,
|
||||
clearWay: true,
|
||||
destination: destinationId,
|
||||
charIds: chars.map(char => char.eve_id),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Add Waypoint',
|
||||
icon: PrimeIcons.DIRECTIONS_ALT,
|
||||
command: () => {
|
||||
onWaypointSet({
|
||||
fromBeginning: false,
|
||||
clearWay: false,
|
||||
destination: destinationId,
|
||||
charIds: chars.map(char => char.eve_id),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Add Waypoint Front',
|
||||
icon: PrimeIcons.DIRECTIONS,
|
||||
command: () => {
|
||||
onWaypointSet({
|
||||
fromBeginning: true,
|
||||
clearWay: false,
|
||||
destination: destinationId,
|
||||
charIds: chars.map(char => char.eve_id),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
[onWaypointSet],
|
||||
);
|
||||
|
||||
const getStationsMenu = useCallback(
|
||||
(stations: RouteStationSummary[]) => {
|
||||
const chars = getOwnOnlineCharacters().filter(x => x.online);
|
||||
const sortedStations = sortStationsByDistance(stations);
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Stations',
|
||||
icon: PrimeIcons.MAP_MARKER,
|
||||
items: sortedStations.map(station => {
|
||||
const destinationId = station.station_id.toString();
|
||||
const specialClass = station.special ? '[&_.p-menuitem-text]:text-orange-400' : '';
|
||||
|
||||
if (chars.length === 0) {
|
||||
return {
|
||||
label: station.station_name,
|
||||
className: specialClass || undefined,
|
||||
items: [{ label: 'No online characters', disabled: true }],
|
||||
};
|
||||
}
|
||||
|
||||
if (chars.length === 1) {
|
||||
return {
|
||||
label: station.station_name,
|
||||
className: specialClass || undefined,
|
||||
items: getStationWaypointItems(destinationId, chars.slice(0, 1)),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: station.station_name,
|
||||
className: `${specialClass} w-[500px]`.trim(),
|
||||
items: [
|
||||
{
|
||||
label: 'All',
|
||||
icon: PrimeIcons.USERS,
|
||||
items: getStationWaypointItems(destinationId, chars),
|
||||
},
|
||||
...chars.map(char => ({
|
||||
label: char.name,
|
||||
icon: PrimeIcons.USER,
|
||||
items: getStationWaypointItems(destinationId, [char]),
|
||||
})),
|
||||
],
|
||||
};
|
||||
}),
|
||||
},
|
||||
];
|
||||
},
|
||||
[getOwnOnlineCharacters, getStationWaypointItems],
|
||||
);
|
||||
|
||||
const items: MenuItem[] = useMemo(() => {
|
||||
const system = systemId ? systemStatics.get(parseInt(systemId)) : undefined;
|
||||
@@ -50,6 +147,10 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
if (!systemId || !system) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const route = routes.find(x => x.destination?.toString() === systemId);
|
||||
const stationItems = route?.stations?.length ? getStationsMenu(route.stations) : [];
|
||||
|
||||
return [
|
||||
{
|
||||
className: classes.FastActions,
|
||||
@@ -69,15 +170,20 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
{ separator: true },
|
||||
...getJumpPlannerMenu(system, routes),
|
||||
...getWaypointMenu(systemId, system.system_class),
|
||||
{
|
||||
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
|
||||
icon: !hubs.includes(systemId) ? (
|
||||
<MapAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
command: onHubToggle,
|
||||
},
|
||||
...stationItems,
|
||||
...(toggleHubCommand
|
||||
? [
|
||||
{
|
||||
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
|
||||
icon: !hubs.includes(systemId) ? (
|
||||
<MapAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
command: onHubToggle,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!systemOnMap
|
||||
? [
|
||||
{
|
||||
@@ -94,15 +200,18 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
systems,
|
||||
getJumpPlannerMenu,
|
||||
getWaypointMenu,
|
||||
getStationsMenu,
|
||||
hubs,
|
||||
onHubToggle,
|
||||
onAddSystem,
|
||||
onOpenSettings,
|
||||
toggleHubCommand,
|
||||
routes,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu model={items} ref={contextMenuRef} breakpoint="767px" />
|
||||
<ContextMenu className={classes.ContextMenu} model={items} ref={contextMenuRef} breakpoint="767px" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { RouteStationSummary } from '@/hooks/Mapper/types/routes.ts';
|
||||
|
||||
const ROMAN_VALUES: Record<string, number> = {
|
||||
I: 1,
|
||||
V: 5,
|
||||
X: 10,
|
||||
L: 50,
|
||||
C: 100,
|
||||
D: 500,
|
||||
M: 1000,
|
||||
};
|
||||
|
||||
const MAX_DISTANCE = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
const romanToInt = (value: string): number | null => {
|
||||
const chars = value.toUpperCase().split('');
|
||||
|
||||
if (chars.length === 0 || chars.some(char => ROMAN_VALUES[char] === undefined)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
let prev = 0;
|
||||
|
||||
for (let i = chars.length - 1; i >= 0; i--) {
|
||||
const current = ROMAN_VALUES[chars[i]];
|
||||
if (current < prev) {
|
||||
total -= current;
|
||||
} else {
|
||||
total += current;
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
};
|
||||
|
||||
const parseOrbitIndex = (value: string | undefined): number | null => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
const asInt = Number.parseInt(trimmed, 10);
|
||||
|
||||
if (!Number.isNaN(asInt) && `${asInt}` === trimmed) {
|
||||
return asInt;
|
||||
}
|
||||
|
||||
return romanToInt(trimmed);
|
||||
};
|
||||
|
||||
const extractPlanetOrbit = (name: string): number | null => {
|
||||
const firstPart = name.split(' - ')[0] ?? '';
|
||||
const match = firstPart.match(/([IVXLCDM]+|\d+)(?:\s*\([^)]*\))?$/i);
|
||||
return parseOrbitIndex(match?.[1]);
|
||||
};
|
||||
|
||||
const extractMoonOrbit = (name: string): number | null => {
|
||||
const match = name.match(/\bMoon\s+([IVXLCDM]+|\d+)\b/i);
|
||||
return parseOrbitIndex(match?.[1]);
|
||||
};
|
||||
|
||||
const stationSortKey = (station: RouteStationSummary): [number, number, string, number] => {
|
||||
return [
|
||||
extractPlanetOrbit(station.station_name) ?? MAX_DISTANCE,
|
||||
// If there is no moon in the station name, treat it as closer than moon orbits.
|
||||
extractMoonOrbit(station.station_name) ?? 0,
|
||||
station.station_name.toLowerCase(),
|
||||
station.station_id,
|
||||
];
|
||||
};
|
||||
|
||||
export const sortStationsByDistance = (stations: RouteStationSummary[]): RouteStationSummary[] => {
|
||||
return [...stations].sort((a, b) => {
|
||||
const aKey = stationSortKey(a);
|
||||
const bKey = stationSortKey(b);
|
||||
|
||||
for (let i = 0; i < aKey.length; i++) {
|
||||
if (aKey[i] < bKey[i]) {
|
||||
return -1;
|
||||
}
|
||||
if (aKey[i] > bKey[i]) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -41,7 +41,7 @@ export const RoutesWidgetContent = () => {
|
||||
const {
|
||||
data: { selectedSystems, systems, isSubscriptionActive },
|
||||
} = useMapRootState();
|
||||
const { hubs = [], routesList, isRestricted, loading } = useRouteProvider();
|
||||
const { hubs = [], routesList, isRestricted, loading, nohubsPlaceholder } = useRouteProvider();
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
|
||||
@@ -105,7 +105,11 @@ export const RoutesWidgetContent = () => {
|
||||
}
|
||||
|
||||
if (hubs.length === 0) {
|
||||
return <div className="w-full h-full flex justify-center items-center select-none">Routes not set</div>;
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center items-center select-none">
|
||||
{nohubsPlaceholder ?? 'Routes not set'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -129,7 +133,6 @@ export const RoutesWidgetContent = () => {
|
||||
offset: 10,
|
||||
}}
|
||||
/>
|
||||
|
||||
<SystemView
|
||||
systemId={route.destination.toString()}
|
||||
className={clsx('select-none text-center cursor-context-menu')}
|
||||
@@ -138,7 +141,7 @@ export const RoutesWidgetContent = () => {
|
||||
showCustomName
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right pl-1">{route.has_connection ? route.systems?.length ?? 2 : ''}</div>
|
||||
<div className="text-right pl-1">{route.has_connection ? (route.systems?.length ?? 2) : ''}</div>
|
||||
<div className="pl-2 pb-0.5">
|
||||
<RoutesList data={route} onContextMenu={handleContextMenu} />
|
||||
</div>
|
||||
@@ -147,9 +150,7 @@ export const RoutesWidgetContent = () => {
|
||||
})}
|
||||
</div>
|
||||
</LoadingWrapper>
|
||||
|
||||
<ContextMenuSystemInfo
|
||||
hubs={hubs}
|
||||
routes={preparedRoutes}
|
||||
systems={systems}
|
||||
systemStatics={systemStatics}
|
||||
@@ -162,9 +163,10 @@ export const RoutesWidgetContent = () => {
|
||||
|
||||
type RoutesWidgetCompProps = {
|
||||
title: ReactNode | string;
|
||||
renderContent?: (content: ReactNode, compact: boolean) => ReactNode;
|
||||
};
|
||||
|
||||
export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps) => {
|
||||
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
|
||||
const { data, update, addHubCommand } = useRouteProvider();
|
||||
|
||||
@@ -183,7 +185,7 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
|
||||
|
||||
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
|
||||
async item => addHubCommand(item.value.toString()),
|
||||
async item => addHubCommand?.(item.value.toString()),
|
||||
[addHubCommand],
|
||||
);
|
||||
|
||||
@@ -191,15 +193,17 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
<Widget
|
||||
label={
|
||||
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
|
||||
<span className="select-none">{title}</span>
|
||||
<div className="select-none flex items-center gap-2">{title}</div>
|
||||
<LayoutEventBlocker className="flex items-center gap-2">
|
||||
<WdImgButton
|
||||
className={PrimeIcons.PLUS_CIRCLE}
|
||||
onClick={onAddSystem}
|
||||
tooltip={{
|
||||
content: 'Click here to add new system to routes',
|
||||
}}
|
||||
/>
|
||||
{addHubCommand && (
|
||||
<WdImgButton
|
||||
className={PrimeIcons.PLUS_CIRCLE}
|
||||
onClick={onAddSystem}
|
||||
tooltip={{
|
||||
content: 'Click here to add new system to routes',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<WdTooltipWrapper content="Show shortest route" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
@@ -223,24 +227,38 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<RoutesWidgetContent />
|
||||
{renderContent ? (
|
||||
renderContent(
|
||||
<div className="h-full overflow-auto bg-opacity-5 custom-scrollbar">
|
||||
<RoutesWidgetContent />
|
||||
</div>,
|
||||
compact,
|
||||
)
|
||||
) : (
|
||||
<div className="h-full overflow-auto bg-opacity-5 custom-scrollbar">
|
||||
<RoutesWidgetContent />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RoutesSettingsDialog visible={routeSettingsVisible} setVisible={setRouteSettingsVisible} />
|
||||
|
||||
<AddSystemDialog
|
||||
title="Add system to routes"
|
||||
visible={openAddSystem}
|
||||
setVisible={() => setOpenAddSystem(false)}
|
||||
onSubmit={handleSubmitAddSystem}
|
||||
/>
|
||||
{addHubCommand && (
|
||||
<AddSystemDialog
|
||||
title="Add system to routes"
|
||||
visible={openAddSystem}
|
||||
setVisible={() => setOpenAddSystem(false)}
|
||||
onSubmit={handleSubmitAddSystem}
|
||||
/>
|
||||
)}
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export const RoutesWidget = forwardRef<RoutesImperativeHandle, RoutesWidgetProps & RoutesWidgetCompProps>(
|
||||
({ title, ...props }, ref) => {
|
||||
({ title, renderContent, ...props }, ref) => {
|
||||
return (
|
||||
<RoutesProvider {...props} ref={ref}>
|
||||
<RoutesWidgetComp title={title} />
|
||||
<RoutesWidgetComp title={title} renderContent={renderContent} />
|
||||
</RoutesProvider>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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,9 +12,10 @@ export type RoutesWidgetProps = {
|
||||
routesList: RoutesList | undefined;
|
||||
loading: boolean;
|
||||
|
||||
addHubCommand: AddHubCommand;
|
||||
toggleHubCommand: ToggleHubCommand;
|
||||
addHubCommand?: AddHubCommand;
|
||||
toggleHubCommand?: ToggleHubCommand;
|
||||
isRestricted?: boolean;
|
||||
nohubsPlaceholder?: string;
|
||||
};
|
||||
|
||||
export type RoutesProviderInnerProps = RoutesWidgetProps;
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { FINAL_DURATION_MS } from '../constants';
|
||||
|
||||
// Strip frontend-only fields that should never be sent to the backend.
|
||||
// "linked_system" is an object the frontend uses; the backend expects "linked_system_id" (integer)
|
||||
// which is set via a separate linkSignatureToSystem call.
|
||||
function stripFrontendFields(s: ExtendedSystemSignature) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { linked_system, pendingDeletion, pendingAddition, pendingUntil, finalTimeoutId, character_name, ...rest } =
|
||||
s as any;
|
||||
return rest;
|
||||
}
|
||||
|
||||
export function prepareUpdatePayload(
|
||||
systemId: string,
|
||||
added: ExtendedSystemSignature[],
|
||||
@@ -9,9 +19,9 @@ export function prepareUpdatePayload(
|
||||
) {
|
||||
return {
|
||||
system_id: systemId,
|
||||
added: added.map(s => ({ ...s })),
|
||||
updated: updated.map(s => ({ ...s })),
|
||||
removed: removed.map(s => ({ ...s })),
|
||||
added: added.map(stripFrontendFields),
|
||||
updated: updated.map(stripFrontendFields),
|
||||
removed: removed.map(stripFrontendFields),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, ClipboardEvent, useRef } from 'react';
|
||||
import React, { useCallback, ClipboardEvent, useRef, useState } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import {
|
||||
@@ -13,7 +13,9 @@ import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
|
||||
import { SystemStructuresContent } from './SystemStructuresContent/SystemStructuresContent';
|
||||
import { useSystemStructures } from './hooks/useSystemStructures';
|
||||
import { processSnippetText } from './helpers';
|
||||
import { processSnippetText, StructureItem } from './helpers';
|
||||
import { SystemStructuresOwnersDialog } from './SystemStructuresOwnersDialog/SystemStructuresOwnersDialog';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const SystemStructures: React.FC = () => {
|
||||
const {
|
||||
@@ -24,6 +26,7 @@ export const SystemStructures: React.FC = () => {
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
|
||||
const { structures, handleUpdateStructures } = useSystemStructures({ systemId, outCommand });
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
|
||||
const labelRef = useRef<HTMLDivElement>(null);
|
||||
const isCompact = useMaxWidth(labelRef, 260);
|
||||
@@ -48,6 +51,18 @@ export const SystemStructures: React.FC = () => {
|
||||
[processClipboard],
|
||||
);
|
||||
|
||||
const handleSave = (updatedStructures: StructureItem[]) => {
|
||||
handleUpdateStructures(updatedStructures)
|
||||
}
|
||||
|
||||
const handleOpenDialog = useCallback(() => {
|
||||
setShowEditDialog(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseDialog = useCallback(() => {
|
||||
setShowEditDialog(false)
|
||||
}, [])
|
||||
|
||||
const handlePasteTimer = useCallback(async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
@@ -71,8 +86,19 @@ export const SystemStructures: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<LayoutEventBlocker className="flex gap-2.5">
|
||||
{structures.length > 1 && (
|
||||
<WdImgButton
|
||||
className={clsx(PrimeIcons.USER_EDIT, 'text-sky-400 hover:text-sky-200 transition duration-300')}
|
||||
onClick={handleOpenDialog}
|
||||
tooltip={{
|
||||
position: TooltipPosition.left,
|
||||
// @ts-ignore
|
||||
content: 'Update all structure owners',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<WdImgButton
|
||||
className={`${PrimeIcons.CLOCK} text-sky-400 hover:text-sky-200 transition duration-300`}
|
||||
className={clsx(PrimeIcons.CLOCK, 'text-sky-400 hover:text-sky-200 transition duration-300')}
|
||||
onClick={handlePasteTimer}
|
||||
tooltip={{
|
||||
position: TooltipPosition.left,
|
||||
@@ -117,6 +143,15 @@ export const SystemStructures: React.FC = () => {
|
||||
<SystemStructuresContent structures={structures} onUpdateStructures={handleUpdateStructures} />
|
||||
)}
|
||||
</Widget>
|
||||
|
||||
{showEditDialog && (
|
||||
<SystemStructuresOwnersDialog
|
||||
visible={showEditDialog}
|
||||
structures={structures}
|
||||
onClose={handleCloseDialog}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,14 @@ import { AutoComplete } from 'primereact/autocomplete';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { formatToISO, statusesRequiringTimer, StructureItem, StructureStatus } from '../helpers';
|
||||
import {
|
||||
calendarDateToUtcIso,
|
||||
formatToISO,
|
||||
statusesRequiringTimer,
|
||||
StructureItem,
|
||||
StructureStatus,
|
||||
utcToCalendarDate,
|
||||
} from '../helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
@@ -72,7 +79,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
|
||||
// If this is the endTime (Date from Calendar), we store as ISO or string:
|
||||
if (field === 'endTime' && val instanceof Date) {
|
||||
return { ...prev, endTime: val.toISOString() };
|
||||
return { ...prev, endTime: calendarDateToUtcIso(val) };
|
||||
}
|
||||
|
||||
return { ...prev, [field]: val };
|
||||
@@ -188,7 +195,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
Timer <br /> (Eve Time):
|
||||
</span>
|
||||
<Calendar
|
||||
value={editData.endTime ? new Date(editData.endTime) : undefined}
|
||||
value={editData.endTime ? utcToCalendarDate(editData.endTime) : undefined}
|
||||
onChange={e => handleChange('endTime', e.value ?? '')}
|
||||
showTime
|
||||
hourFormat="24"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
.systemStructuresOwnersDialog {
|
||||
|
||||
.p-dialog-content {
|
||||
background-color: var(--surface-800) !important;
|
||||
}
|
||||
|
||||
.p-dialog-header {
|
||||
background-color: var(--surface-700);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.p-dialog-header-icon,
|
||||
.p-dialog-header-title {
|
||||
color: var(--gray-200);
|
||||
}
|
||||
|
||||
.p-inputtext {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #ddd !important;
|
||||
font-size: 12px !important;
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
}
|
||||
|
||||
.p-dialog-footer {
|
||||
.p-button {
|
||||
font-size: 12px !important;
|
||||
padding: 0.3rem 0.75rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import clsx from 'clsx';
|
||||
import { AutoComplete } from 'primereact/autocomplete';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useToast } from '@/hooks/Mapper/ToastProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { StructureItem } from '../helpers';
|
||||
|
||||
interface StructuresOwnersEditDialogProps {
|
||||
visible: boolean;
|
||||
structures: StructureItem[];
|
||||
onClose: () => void;
|
||||
onSave: (updatedStuctures: StructureItem[]) => void;
|
||||
}
|
||||
|
||||
export const SystemStructuresOwnersDialog: React.FC<StructuresOwnersEditDialogProps> = ({
|
||||
visible,
|
||||
structures,
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const [ownerInput, setOwnerInput] = useState('');
|
||||
const [ownerSuggestions, setOwnerSuggestions] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
const { outCommand } = useMapRootState();
|
||||
const { show } = useToast();
|
||||
|
||||
const [prevQuery, setPrevQuery] = useState('');
|
||||
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
|
||||
const [editData, setEditData] = useState<StructureItem[]>(structures);
|
||||
|
||||
// Searching corporation owners via auto-complete
|
||||
const searchOwners = useCallback(
|
||||
async (e: { query: string }) => {
|
||||
const newQuery = e.query.trim();
|
||||
if (!newQuery) {
|
||||
setOwnerSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If user typed more text but we have partial match in prevResults
|
||||
if (newQuery.startsWith(prevQuery) && prevResults.length > 0) {
|
||||
const filtered = prevResults.filter(item => item.label.toLowerCase().includes(newQuery.toLowerCase()));
|
||||
setOwnerSuggestions(filtered);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO fix it
|
||||
const { results = [] } = await outCommand({
|
||||
type: OutCommand.getCorporationNames,
|
||||
data: { search: newQuery },
|
||||
});
|
||||
setOwnerSuggestions(results);
|
||||
setPrevQuery(newQuery);
|
||||
setPrevResults(results);
|
||||
} catch (err) {
|
||||
show({
|
||||
severity: 'error',
|
||||
summary: 'Failed to fetch owners',
|
||||
detail: `${err}`,
|
||||
life: 10000,
|
||||
});
|
||||
}
|
||||
},
|
||||
[prevQuery, prevResults, outCommand],
|
||||
);
|
||||
|
||||
// when user picks a corp from auto-complete
|
||||
const handleSelectOwner = (selected: { label: string; value: string }) => {
|
||||
setOwnerInput(selected.label);
|
||||
|
||||
setEditData(
|
||||
structures.map(item => {
|
||||
return { ...item, ownerName: selected.label, ownerId: selected.value };
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleSaveClick = async () => {
|
||||
if (!editData) return;
|
||||
|
||||
// Get all unique owner IDs that need ticker lookup
|
||||
const allOwnerIds = editData.filter(x => x.ownerId != null).map(x => x.ownerId as string);
|
||||
|
||||
const uniqueOwnerIds = [...new Set(allOwnerIds)];
|
||||
|
||||
// Fetch all tickers in parallel
|
||||
const tickerResults = await Promise.all(
|
||||
uniqueOwnerIds.map(async ownerId => {
|
||||
try {
|
||||
const { ticker } = await outCommand({
|
||||
type: OutCommand.getCorporationTicker,
|
||||
data: { corp_id: ownerId },
|
||||
});
|
||||
return { ownerId, ticker: ticker ?? '' };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch ticker for ownerId:', ownerId, err);
|
||||
return { ownerId, ticker: '' };
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Create a map of ownerId -> ticker for quick lookup
|
||||
const tickerMap = new Map(tickerResults.map(r => [r.ownerId, r.ticker]));
|
||||
|
||||
// Create new array with updated values (no mutation)
|
||||
const updatedStructures = editData.map(structure => {
|
||||
if (!structure.ownerId) {
|
||||
return structure;
|
||||
}
|
||||
|
||||
return {
|
||||
...structure,
|
||||
ownerTicker: tickerMap.get(structure.ownerId) ?? '',
|
||||
};
|
||||
});
|
||||
|
||||
onSave(updatedStructures);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onHide={onClose}
|
||||
header={'Update All Structure Owners'}
|
||||
className={clsx('myStructuresOwnersDialog', 'text-stone-200 w-full max-w-md')}
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-[14px]">
|
||||
<div className="flex gap-2">
|
||||
Updating the corporation name below will update all structures currently saved within the system.
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="grid grid-cols-[100px_1fr] gap-2 items-start mt-2">
|
||||
<span className="mt-1">Structures to update:</span>
|
||||
<ul>
|
||||
{structures &&
|
||||
structures.map((item, i) => (
|
||||
<li key={i}>
|
||||
{item.structureType || 'Unknown Type'} - {item.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Owner:</span>
|
||||
<AutoComplete
|
||||
id="owner"
|
||||
value={ownerInput}
|
||||
suggestions={ownerSuggestions}
|
||||
completeMethod={searchOwners}
|
||||
minLength={3}
|
||||
delay={400}
|
||||
field="label"
|
||||
placeholder="Corporation name..."
|
||||
onChange={e => setOwnerInput(e.value)}
|
||||
onSelect={e => handleSelectOwner(e.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end items-center gap-2 mt-4">
|
||||
<WdButton label="Save" className="p-button-sm" onClick={handleSaveClick} />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -43,6 +43,29 @@ export function mapServerStructure(serverData: any): StructureItem {
|
||||
};
|
||||
}
|
||||
|
||||
export function utcToCalendarDate(utcIso: string): Date {
|
||||
// Parse ISO components manually to avoid browser quirks with
|
||||
// 6-digit microsecond precision from Elixir's :utc_datetime_usec.
|
||||
const m = utcIso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/);
|
||||
if (m) {
|
||||
const [, yr, mo, dy, hr, mi, sc] = m;
|
||||
return new Date(+yr, +mo - 1, +dy, +hr, +mi, +sc);
|
||||
}
|
||||
// Fallback for non-ISO strings
|
||||
const d = new Date(utcIso);
|
||||
return new Date(d.getTime() + d.getTimezoneOffset() * 60_000);
|
||||
}
|
||||
|
||||
export function calendarDateToUtcIso(localDate: Date): string {
|
||||
// Read local-time components (which represent EVE/UTC time) and
|
||||
// build the ISO string directly — no timezone arithmetic needed.
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return (
|
||||
`${localDate.getFullYear()}-${pad(localDate.getMonth() + 1)}-${pad(localDate.getDate())}` +
|
||||
`T${pad(localDate.getHours())}:${pad(localDate.getMinutes())}:${pad(localDate.getSeconds())}.000Z`
|
||||
);
|
||||
}
|
||||
|
||||
export function formatToISO(datetimeLocal: string): string {
|
||||
if (!datetimeLocal) return '';
|
||||
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { RoutesWidget } from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
|
||||
import { useLoadRoutesBy } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/hooks';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { SelectItemOptionsType } from 'primereact/selectitem';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||
import clsx from 'clsx';
|
||||
import { RoutesByCategoryType, RoutesByScopeType, RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { DEFAULT_ROUTES_SETTINGS } from '@/hooks/Mapper/mapRootProvider/constants.ts';
|
||||
import { TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
export type RoutesByType = RoutesByCategoryType;
|
||||
|
||||
type WRoutesByProps = {
|
||||
type?: RoutesByType;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const ROUTES_BY_OPTIONS: SelectItemOptionsType = [
|
||||
{
|
||||
label: 'Blue Loot',
|
||||
value: 'blueLoot',
|
||||
icon: 'images/30747_64.png',
|
||||
},
|
||||
{
|
||||
label: 'Red Loot',
|
||||
value: 'redLoot',
|
||||
icon: 'images/89219_64.png',
|
||||
},
|
||||
{
|
||||
label: 'Thera',
|
||||
value: 'thera',
|
||||
icon: 'images/map.png',
|
||||
},
|
||||
{
|
||||
label: 'Turnur',
|
||||
value: 'turnur',
|
||||
icon: 'images/map.png',
|
||||
},
|
||||
{
|
||||
label: 'Security Office',
|
||||
value: 'so_cleaning',
|
||||
icon: 'images/concord-so.png',
|
||||
},
|
||||
{
|
||||
label: 'Trade Hubs',
|
||||
value: 'trade_hubs',
|
||||
icon: 'images/market.png',
|
||||
},
|
||||
];
|
||||
const ROUTES_BY_SECURITY_OPTIONS = [
|
||||
{ label: 'All', value: 'ALL' },
|
||||
{ label: 'High', value: 'HIGH' },
|
||||
];
|
||||
|
||||
export const WRoutesBy = ({ type = 'blueLoot', title = 'Routes By' }: WRoutesByProps) => {
|
||||
const {
|
||||
outCommand,
|
||||
storedSettings: { settingsRoutesBy, settingsRoutesByUpdate },
|
||||
data,
|
||||
} = useMapRootState();
|
||||
|
||||
const criteriaType = settingsRoutesBy.type ?? type;
|
||||
const securityType = settingsRoutesBy.scope ?? 'ALL';
|
||||
const routesSettings = settingsRoutesBy.routes ?? DEFAULT_ROUTES_SETTINGS;
|
||||
const routesListBy = data.routesListBy;
|
||||
const availableRoutesBy = data.availableRoutesBy;
|
||||
|
||||
const routesByOptions = useMemo(() => {
|
||||
if (!availableRoutesBy || availableRoutesBy.length === 0) {
|
||||
return ROUTES_BY_OPTIONS;
|
||||
}
|
||||
|
||||
return ROUTES_BY_OPTIONS.filter(option => availableRoutesBy.includes(option.value as RoutesByType));
|
||||
}, [availableRoutesBy]);
|
||||
|
||||
const resolvedCriteriaType = useMemo(() => {
|
||||
const optionValues = routesByOptions.map(option => option.value as RoutesByType);
|
||||
|
||||
if (optionValues.length === 0) {
|
||||
return criteriaType;
|
||||
}
|
||||
|
||||
return optionValues.includes(criteriaType) ? criteriaType : optionValues[0];
|
||||
}, [routesByOptions, criteriaType]);
|
||||
|
||||
const loadRoutesCommand: LoadRoutesCommand = useCallback(
|
||||
async (systemId, currentRoutesSettings) => {
|
||||
await outCommand({
|
||||
type: OutCommand.getRoutesBy,
|
||||
data: {
|
||||
system_id: systemId,
|
||||
type: resolvedCriteriaType,
|
||||
securityType: securityType === 'HIGH' ? 'high' : 'both',
|
||||
routes_settings: currentRoutesSettings,
|
||||
},
|
||||
});
|
||||
},
|
||||
[outCommand, resolvedCriteriaType, securityType],
|
||||
);
|
||||
|
||||
const hubs = useMemo(() => routesListBy?.routes?.map(route => route.destination.toString()) ?? [], [routesListBy]);
|
||||
|
||||
const { loading: internalLoading } = useLoadRoutesBy({
|
||||
data: routesSettings,
|
||||
loadRoutesCommand,
|
||||
routesList: routesListBy,
|
||||
deps: [resolvedCriteriaType, securityType],
|
||||
});
|
||||
|
||||
const updateRoutesSettings = useCallback(
|
||||
(next: RoutesType) => settingsRoutesByUpdate(prev => ({ ...prev, routes: next })),
|
||||
[settingsRoutesByUpdate],
|
||||
);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const compactSmall = useMaxWidth(ref, 180);
|
||||
const compactMiddle = useMaxWidth(ref, 245);
|
||||
|
||||
const titleNode = useMemo(
|
||||
() => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="select-none">{title}</span>
|
||||
<WdImgButton
|
||||
className={PrimeIcons.QUESTION_CIRCLE}
|
||||
tooltip={{
|
||||
position: TooltipPosition.top,
|
||||
content: 'Alpha map users can access only 1 route',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
[title],
|
||||
);
|
||||
|
||||
return (
|
||||
<RoutesWidget
|
||||
title={titleNode}
|
||||
nohubsPlaceholder="Not found any destinations"
|
||||
renderContent={(content /*, compact*/) => (
|
||||
<div className="h-full grid grid-rows-[1fr_auto]" ref={ref}>
|
||||
{content}
|
||||
<div className="flex items-center gap-2 justify-end mb-2 px-2 pt-2">
|
||||
{!compactSmall && (
|
||||
<Dropdown
|
||||
value={securityType}
|
||||
options={ROUTES_BY_SECURITY_OPTIONS}
|
||||
onChange={e => settingsRoutesByUpdate(prev => ({ ...prev, scope: e.value as RoutesByScopeType }))}
|
||||
className="w-[90px] [&_span]:!text-[12px]"
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
value={resolvedCriteriaType}
|
||||
itemTemplate={e => (
|
||||
<div className="flex items-center gap-2">
|
||||
{e.icon && <img src={e.icon} height="18" width="18" />}
|
||||
<span className="text-[12px]">{e.label}</span>
|
||||
</div>
|
||||
)}
|
||||
valueTemplate={e => {
|
||||
if (!e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (compactMiddle) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-[50px]">
|
||||
{e.icon ? <img src={e.icon} height="18" width="18" /> : <span>{e.label}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{e.icon && <img src={e.icon} height="18" width="18" />}
|
||||
<span className="text-[12px]">{e.label}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
options={routesByOptions}
|
||||
onChange={e => settingsRoutesByUpdate(prev => ({ ...prev, type: e.value as RoutesByCategoryType }))}
|
||||
className={clsx({
|
||||
['w-[130px]']: !compactMiddle,
|
||||
['w-[65px]']: compactMiddle,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
data={routesSettings}
|
||||
update={updateRoutesSettings}
|
||||
hubs={hubs}
|
||||
routesList={routesListBy}
|
||||
loading={internalLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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,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,16 +88,6 @@ 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',
|
||||
@@ -143,6 +133,16 @@ export const K162_TYPES: K162Type[] = [
|
||||
value: 'pochven',
|
||||
whClassName: 'F216',
|
||||
},
|
||||
{
|
||||
label: 'C1/C2/C3',
|
||||
value: 'c1_c2_c3',
|
||||
whClassName: 'E004_D382_L477',
|
||||
},
|
||||
{
|
||||
label: 'C4/C5',
|
||||
value: 'c4_c5',
|
||||
whClassName: 'M001_L614',
|
||||
},
|
||||
];
|
||||
|
||||
export const K162_TYPES_MAP: { [key: string]: K162Type } = K162_TYPES.reduce(
|
||||
|
||||
@@ -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,8 @@ const INITIAL_DATA: MapRootData = {
|
||||
userHubs: [],
|
||||
routes: undefined,
|
||||
userRoutes: undefined,
|
||||
routesListBy: undefined,
|
||||
availableRoutesBy: [],
|
||||
kills: [],
|
||||
connections: [],
|
||||
detailedKills: {},
|
||||
@@ -132,6 +135,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 +184,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:
|
||||
|
||||
@@ -24,6 +24,7 @@ export const useMapInit = () => {
|
||||
user_permissions,
|
||||
options,
|
||||
is_subscription_active,
|
||||
available_routes_by,
|
||||
main_character_eve_id,
|
||||
following_character_eve_id,
|
||||
user_hubs,
|
||||
@@ -85,6 +86,10 @@ export const useMapInit = () => {
|
||||
updateData.isSubscriptionActive = is_subscription_active;
|
||||
}
|
||||
|
||||
if (available_routes_by) {
|
||||
updateData.availableRoutesBy = available_routes_by;
|
||||
}
|
||||
|
||||
if (system_static_infos) {
|
||||
system_static_infos.forEach(static_info => {
|
||||
addSystemStatic(static_info);
|
||||
|
||||
@@ -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 });
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
useMapInit,
|
||||
useMapUpdated,
|
||||
useRoutes,
|
||||
useRoutesListBy,
|
||||
useUserRoutes,
|
||||
} from './api';
|
||||
|
||||
@@ -61,6 +62,7 @@ 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();
|
||||
@@ -115,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);
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { DEFAULT_ROUTES_BY_SETTINGS, DEFAULT_ROUTES_SETTINGS } from '@/hooks/Mapper/mapRootProvider/constants.ts';
|
||||
|
||||
export const to_3: MigrationStructure = {
|
||||
to: 3,
|
||||
up: (prev: any) => {
|
||||
const rawRoutesBy = prev?.routesBy;
|
||||
const hasStructuredRoutesBy =
|
||||
rawRoutesBy && typeof rawRoutesBy === 'object' && 'routes' in rawRoutesBy;
|
||||
|
||||
const routes = hasStructuredRoutesBy
|
||||
? { ...DEFAULT_ROUTES_SETTINGS, ...rawRoutesBy.routes }
|
||||
: { ...DEFAULT_ROUTES_SETTINGS, ...(rawRoutesBy ?? prev?.routes ?? {}) };
|
||||
|
||||
const scopeRaw = hasStructuredRoutesBy ? rawRoutesBy?.scope : undefined;
|
||||
const scope = scopeRaw === 'HIGH' ? 'HIGH' : 'ALL';
|
||||
|
||||
const type = hasStructuredRoutesBy && rawRoutesBy?.type ? rawRoutesBy.type : DEFAULT_ROUTES_BY_SETTINGS.type;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
routesBy: {
|
||||
...DEFAULT_ROUTES_BY_SETTINGS,
|
||||
...(hasStructuredRoutesBy ? rawRoutesBy : {}),
|
||||
scope,
|
||||
type,
|
||||
routes,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Ma
|
||||
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
|
||||
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { RoutesByCategoryType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types/system.ts';
|
||||
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
|
||||
|
||||
@@ -25,6 +26,7 @@ export enum Commands {
|
||||
detailedKillsUpdated = 'detailed_kills_updated',
|
||||
routes = 'routes',
|
||||
userRoutes = 'user_routes',
|
||||
routesListBy = 'routes_list_by',
|
||||
centerSystem = 'center_system',
|
||||
selectSystem = 'select_system',
|
||||
selectSystems = 'select_systems',
|
||||
@@ -62,6 +64,7 @@ export type Command =
|
||||
| Commands.detailedKillsUpdated
|
||||
| Commands.routes
|
||||
| Commands.userRoutes
|
||||
| Commands.routesListBy
|
||||
| Commands.selectSystem
|
||||
| Commands.selectSystems
|
||||
| Commands.centerSystem
|
||||
@@ -101,6 +104,7 @@ export type CommandInit = {
|
||||
options: MapOptions;
|
||||
reset?: boolean;
|
||||
is_subscription_active?: boolean;
|
||||
available_routes_by?: RoutesByCategoryType[];
|
||||
main_character_eve_id?: string | null;
|
||||
following_character_eve_id?: string | null;
|
||||
map_slug?: string;
|
||||
@@ -121,6 +125,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;
|
||||
@@ -199,6 +204,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;
|
||||
@@ -232,6 +238,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',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
|
||||
import { MapOptions, PingData, UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
import { RoutesByCategoryType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
|
||||
export type MapUnionTypes = {
|
||||
wormholesData: Record<string, WormholeDataRaw>;
|
||||
@@ -20,6 +21,8 @@ export type MapUnionTypes = {
|
||||
systemSignatures: Record<string, SystemSignature[]>;
|
||||
routes?: RoutesList;
|
||||
userRoutes?: RoutesList;
|
||||
routesListBy?: RoutesList;
|
||||
availableRoutesBy?: RoutesByCategoryType[];
|
||||
kills: Record<number, number>;
|
||||
connections: SolarSystemConnection[];
|
||||
userPermissions: Partial<UserPermissions>;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ defmodule WandererApp.Api.Character do
|
||||
define(:active_by_user,
|
||||
action: :active_by_user
|
||||
)
|
||||
|
||||
define(:admin_all, action: :admin_all)
|
||||
end
|
||||
|
||||
actions do
|
||||
@@ -69,6 +71,10 @@ defmodule WandererApp.Api.Character do
|
||||
filter(expr(user_id == ^arg(:user_id) and deleted == false))
|
||||
end
|
||||
|
||||
read :admin_all do
|
||||
prepare build(load: [:user])
|
||||
end
|
||||
|
||||
read :last_active do
|
||||
argument(:from, :utc_datetime, allow_nil?: false)
|
||||
|
||||
|
||||
@@ -218,6 +218,11 @@ defmodule WandererApp.Api.Map do
|
||||
update :toggle_webhooks do
|
||||
accept [:webhooks_enabled]
|
||||
require_atomic? false
|
||||
|
||||
change after_action(fn _changeset, record, _context ->
|
||||
WandererApp.Map.update_webhooks_enabled(record.id, record.webhooks_enabled)
|
||||
{:ok, record}
|
||||
end)
|
||||
end
|
||||
|
||||
update :toggle_sse do
|
||||
@@ -226,6 +231,11 @@ defmodule WandererApp.Api.Map do
|
||||
|
||||
# Validate subscription when enabling SSE
|
||||
validate &validate_sse_subscription/2
|
||||
|
||||
change after_action(fn _changeset, record, _context ->
|
||||
WandererApp.Map.update_sse_enabled(record.id, record.sse_enabled)
|
||||
{:ok, record}
|
||||
end)
|
||||
end
|
||||
|
||||
create :duplicate do
|
||||
|
||||
@@ -123,7 +123,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:group,
|
||||
:type,
|
||||
:custom_info,
|
||||
:deleted
|
||||
:deleted,
|
||||
:linked_system_id
|
||||
]
|
||||
end
|
||||
|
||||
@@ -140,7 +141,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:type,
|
||||
:custom_info,
|
||||
:deleted,
|
||||
:update_forced_at
|
||||
:update_forced_at,
|
||||
:linked_system_id
|
||||
]
|
||||
|
||||
primary? true
|
||||
|
||||
@@ -5,6 +5,8 @@ defmodule WandererApp.Api.MapTransaction do
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("map_transactions_v1")
|
||||
@@ -19,6 +21,7 @@ defmodule WandererApp.Api.MapTransaction do
|
||||
define(:by_map, action: :by_map)
|
||||
define(:by_user, action: :by_user)
|
||||
define(:create, action: :create)
|
||||
define(:top_donators, action: :top_donators)
|
||||
end
|
||||
|
||||
actions do
|
||||
@@ -45,6 +48,35 @@ defmodule WandererApp.Api.MapTransaction do
|
||||
argument(:user_id, :uuid, allow_nil?: false)
|
||||
filter(expr(user_id == ^arg(:user_id)))
|
||||
end
|
||||
|
||||
action :top_donators, {:array, :struct} do
|
||||
argument(:map_id, :string, allow_nil?: false)
|
||||
argument(:after, :utc_datetime, allow_nil?: true)
|
||||
|
||||
run fn input, _context ->
|
||||
base =
|
||||
from(t in __MODULE__,
|
||||
where:
|
||||
t.map_id == ^input.arguments.map_id and
|
||||
t.type == :in and
|
||||
not is_nil(t.user_id),
|
||||
group_by: [t.user_id],
|
||||
select: %{user_id: t.user_id, total_amount: sum(t.amount)},
|
||||
order_by: [desc: sum(t.amount)],
|
||||
limit: 10
|
||||
)
|
||||
|
||||
query =
|
||||
case input.arguments[:after] do
|
||||
nil -> base
|
||||
after_date -> base |> where([t], t.inserted_at >= ^after_date)
|
||||
end
|
||||
|
||||
query
|
||||
|> WandererApp.Repo.all()
|
||||
|> then(&{:ok, &1})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
||||
@@ -45,7 +45,17 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
||||
:active?
|
||||
]
|
||||
|
||||
defaults [:read, :destroy]
|
||||
defaults [:read]
|
||||
|
||||
# Custom destroy to invalidate cache
|
||||
destroy :destroy do
|
||||
require_atomic? false
|
||||
|
||||
change after_action(fn _changeset, record, _context ->
|
||||
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
|
||||
{:ok, record}
|
||||
end)
|
||||
end
|
||||
|
||||
update :update do
|
||||
accept [
|
||||
@@ -60,6 +70,12 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
||||
]
|
||||
|
||||
require_atomic? false
|
||||
|
||||
# Invalidate cache when subscription is updated
|
||||
change after_action(fn _changeset, record, _context ->
|
||||
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
|
||||
{:ok, record}
|
||||
end)
|
||||
end
|
||||
|
||||
read :by_map do
|
||||
@@ -124,6 +140,12 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
||||
secret = generate_webhook_secret()
|
||||
Ash.Changeset.force_change_attribute(changeset, :secret, secret)
|
||||
end
|
||||
|
||||
# Invalidate cache when subscription is created
|
||||
change after_action(fn _changeset, record, _context ->
|
||||
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
|
||||
{:ok, record}
|
||||
end)
|
||||
end
|
||||
|
||||
update :rotate_secret do
|
||||
@@ -134,6 +156,11 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
||||
new_secret = generate_webhook_secret()
|
||||
Ash.Changeset.change_attribute(changeset, :secret, new_secret)
|
||||
end
|
||||
|
||||
change after_action(fn _changeset, record, _context ->
|
||||
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
|
||||
{:ok, record}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -86,6 +86,11 @@ defmodule WandererApp.Application do
|
||||
Supervisor.child_spec({Cachex, name: :wanderer_app_cache},
|
||||
id: :wanderer_app_cache_worker
|
||||
),
|
||||
# Cache for webhook subscriptions - 5 minute TTL to reduce DB load
|
||||
Supervisor.child_spec(
|
||||
{Cachex, name: :webhook_subscriptions_cache, default_ttl: :timer.minutes(5)},
|
||||
id: :webhook_subscriptions_cache_worker
|
||||
),
|
||||
{Registry, keys: :unique, name: WandererApp.Character.TrackerRegistry},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
@@ -112,6 +117,7 @@ defmodule WandererApp.Application do
|
||||
WandererApp.Scheduler,
|
||||
WandererApp.Server.ServerStatusTracker,
|
||||
WandererApp.Server.TheraDataFetcher,
|
||||
WandererApp.Server.TurnurDataFetcher,
|
||||
{WandererApp.Character.TrackerPoolSupervisor, []},
|
||||
{WandererApp.Map.MapPoolSupervisor, []},
|
||||
WandererApp.Character.TrackerManager,
|
||||
|
||||
@@ -2,9 +2,11 @@ defmodule WandererApp.ExternalEvents.SseAccessControl do
|
||||
@moduledoc """
|
||||
Handles SSE access control checks including subscription validation.
|
||||
|
||||
Note: Community Edition mode is automatically handled by the
|
||||
WandererApp.Map.is_subscription_active?/1 function, which returns
|
||||
{:ok, true} when subscriptions are disabled globally.
|
||||
IMPORTANT: This module is optimized for high-frequency calls during event delivery.
|
||||
All checks use cached data to avoid database queries on every event.
|
||||
|
||||
Note: Community Edition mode is automatically handled - when subscriptions are
|
||||
disabled globally, we skip the subscription check entirely.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
@@ -15,16 +17,14 @@ defmodule WandererApp.ExternalEvents.SseAccessControl do
|
||||
- {:error, reason} if SSE is not allowed
|
||||
|
||||
Checks in order:
|
||||
1. Global SSE enabled (config)
|
||||
2. Map exists
|
||||
3. Map SSE enabled (per-map setting)
|
||||
4. Subscription active (CE mode handled internally)
|
||||
1. Global SSE enabled (config check - no DB)
|
||||
2. Map SSE enabled (cache check - no DB)
|
||||
3. Subscription active (cache check or skipped in CE mode - no DB)
|
||||
"""
|
||||
def sse_allowed?(map_id) do
|
||||
with :ok <- check_sse_globally_enabled(),
|
||||
{:ok, map} <- fetch_map(map_id),
|
||||
:ok <- check_map_sse_enabled(map),
|
||||
:ok <- check_subscription_or_ce(map_id) do
|
||||
:ok <- check_map_sse_enabled_cached(map_id),
|
||||
:ok <- check_subscription_or_ce_cached(map_id) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -37,31 +37,47 @@ defmodule WandererApp.ExternalEvents.SseAccessControl do
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches the map by ID.
|
||||
# Returns {:ok, map} or {:error, :map_not_found}
|
||||
defp fetch_map(map_id) do
|
||||
case WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, _map} = result -> result
|
||||
_ -> {:error, :map_not_found}
|
||||
# Uses the map cache with fallback to DB query
|
||||
defp check_map_sse_enabled_cached(map_id) do
|
||||
case WandererApp.Map.sse_enabled_with_status(map_id) do
|
||||
{:ok, true} -> :ok
|
||||
{:ok, false} -> {:error, :sse_disabled_for_map}
|
||||
{:error, :not_found} -> {:error, :map_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_map_sse_enabled(map) do
|
||||
if map.sse_enabled do
|
||||
# Checks subscription status using cached data.
|
||||
# In CE mode (subscriptions disabled globally), this is a fast config check.
|
||||
# In Enterprise mode, uses cached map state's subscription settings.
|
||||
defp check_subscription_or_ce_cached(map_id) do
|
||||
# Fast path: CE mode - subscriptions disabled globally
|
||||
if not WandererApp.Env.map_subscriptions_enabled?() do
|
||||
:ok
|
||||
else
|
||||
{:error, :sse_disabled_for_map}
|
||||
# Enterprise mode: check cached subscription status from map state
|
||||
check_subscription_from_cache(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
# Checks if map has active subscription or if running Community Edition.
|
||||
#
|
||||
# Returns :ok if:
|
||||
# - Community Edition (handled internally by is_subscription_active?/1), OR
|
||||
# - Map has active subscription
|
||||
#
|
||||
# Returns {:error, :subscription_required} if subscription check fails.
|
||||
defp check_subscription_or_ce(map_id) do
|
||||
# Checks subscription status from the map cache.
|
||||
# Falls back to DB query only if cache miss.
|
||||
defp check_subscription_from_cache(map_id) do
|
||||
case WandererApp.Map.subscription_active_cached?(map_id) do
|
||||
{:ok, true} ->
|
||||
:ok
|
||||
|
||||
{:ok, false} ->
|
||||
{:error, :subscription_required}
|
||||
|
||||
{:error, :not_cached} ->
|
||||
# Cache miss - fall back to DB check
|
||||
# This should be rare as maps are initialized when accessed
|
||||
fallback_subscription_check(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
# Fallback to DB query - only used when cache miss
|
||||
defp fallback_subscription_check(map_id) do
|
||||
case WandererApp.Map.is_subscription_active?(map_id) do
|
||||
{:ok, true} -> :ok
|
||||
{:ok, false} -> {:error, :subscription_required}
|
||||
|
||||
@@ -166,6 +166,37 @@ defmodule WandererApp.ExternalEvents.WebhookDispatcher do
|
||||
end
|
||||
|
||||
defp get_active_subscriptions(map_id) do
|
||||
# Use cache to avoid DB query on every event
|
||||
cache_key = "map:#{map_id}"
|
||||
|
||||
case Cachex.get(:webhook_subscriptions_cache, cache_key) do
|
||||
{:ok, nil} ->
|
||||
# Cache miss - fetch from DB and cache
|
||||
fetch_and_cache_subscriptions(map_id, cache_key)
|
||||
|
||||
{:ok, subscriptions} ->
|
||||
# Cache hit
|
||||
{:ok, subscriptions}
|
||||
|
||||
{:error, _reason} ->
|
||||
# Cache error - fall back to DB
|
||||
fetch_subscriptions_from_db(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_and_cache_subscriptions(map_id, cache_key) do
|
||||
case fetch_subscriptions_from_db(map_id) do
|
||||
{:ok, subscriptions} = result ->
|
||||
# Cache for 5 minutes (TTL set on cache, but explicit here for clarity)
|
||||
Cachex.put(:webhook_subscriptions_cache, cache_key, subscriptions)
|
||||
result
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_subscriptions_from_db(map_id) do
|
||||
try do
|
||||
subscriptions = MapWebhookSubscription.active_by_map!(map_id)
|
||||
{:ok, subscriptions}
|
||||
@@ -409,17 +440,25 @@ defmodule WandererApp.ExternalEvents.WebhookDispatcher do
|
||||
end
|
||||
|
||||
defp webhooks_allowed?(map_id, webhooks_globally_enabled) do
|
||||
with true <- webhooks_globally_enabled,
|
||||
{:ok, map} <- WandererApp.Api.Map.by_id(map_id),
|
||||
true <- map.webhooks_enabled do
|
||||
:ok
|
||||
else
|
||||
false -> {:error, :webhooks_globally_disabled}
|
||||
nil -> {:error, :webhooks_globally_disabled}
|
||||
{:error, :not_found} -> {:error, :map_not_found}
|
||||
%{webhooks_enabled: false} -> {:error, :webhooks_disabled_for_map}
|
||||
{:error, reason} -> {:error, reason}
|
||||
error -> {:error, {:unexpected_error, error}}
|
||||
cond do
|
||||
not webhooks_globally_enabled ->
|
||||
{:error, :webhooks_globally_disabled}
|
||||
|
||||
not WandererApp.Map.webhooks_enabled?(map_id) ->
|
||||
{:error, :webhooks_disabled_for_map}
|
||||
|
||||
true ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invalidates the webhook subscriptions cache for a map.
|
||||
Called when subscriptions are created, updated, or deleted.
|
||||
"""
|
||||
def invalidate_cache(map_id) do
|
||||
cache_key = "map:#{map_id}"
|
||||
Cachex.del(:webhook_subscriptions_cache, cache_key)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,6 +8,8 @@ defmodule WandererApp.Map do
|
||||
require Logger
|
||||
|
||||
@map_state_cache :map_state_cache
|
||||
# Default plan indicates no active subscription (free tier)
|
||||
@default_subscription_plan :alpha
|
||||
|
||||
defstruct map_id: nil,
|
||||
name: nil,
|
||||
@@ -21,7 +23,10 @@ defmodule WandererApp.Map do
|
||||
acls: [],
|
||||
options: Map.new(),
|
||||
characters_limit: nil,
|
||||
hubs_limit: nil
|
||||
hubs_limit: nil,
|
||||
sse_enabled: false,
|
||||
webhooks_enabled: false,
|
||||
subscription_plan: @default_subscription_plan
|
||||
|
||||
def new(
|
||||
%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs} =
|
||||
@@ -29,6 +34,9 @@ defmodule WandererApp.Map do
|
||||
) do
|
||||
# Extract the new scopes array field if present (nil if not set)
|
||||
scopes = Map.get(input, :scopes)
|
||||
# Extract SSE/webhooks settings (default to false if not present)
|
||||
sse_enabled = Map.get(input, :sse_enabled, false)
|
||||
webhooks_enabled = Map.get(input, :webhooks_enabled, false)
|
||||
|
||||
map =
|
||||
struct!(__MODULE__,
|
||||
@@ -38,7 +46,9 @@ defmodule WandererApp.Map do
|
||||
owner_id: owner_id,
|
||||
name: name,
|
||||
acls: acls,
|
||||
hubs: hubs
|
||||
hubs: hubs,
|
||||
sse_enabled: sse_enabled,
|
||||
webhooks_enabled: webhooks_enabled
|
||||
)
|
||||
|
||||
update_map(map_id, map)
|
||||
@@ -136,7 +146,7 @@ defmodule WandererApp.Map do
|
||||
|
||||
def is_subscription_active?(map_id, _map_subscriptions_enabled) do
|
||||
{:ok, %{plan: plan}} = WandererApp.Map.SubscriptionManager.get_active_map_subscription(map_id)
|
||||
{:ok, plan != :alpha}
|
||||
{:ok, plan != @default_subscription_plan}
|
||||
end
|
||||
|
||||
def get_options(map_id),
|
||||
@@ -323,12 +333,17 @@ defmodule WandererApp.Map do
|
||||
end
|
||||
end
|
||||
|
||||
def update_subscription_settings!(%{map_id: map_id} = _map, %{
|
||||
characters_limit: characters_limit,
|
||||
hubs_limit: hubs_limit
|
||||
}) do
|
||||
def update_subscription_settings!(%{map_id: map_id} = _map, subscription_settings) do
|
||||
characters_limit = Map.get(subscription_settings, :characters_limit)
|
||||
hubs_limit = Map.get(subscription_settings, :hubs_limit)
|
||||
plan = Map.get(subscription_settings, :plan, @default_subscription_plan)
|
||||
|
||||
map_id
|
||||
|> update_map(%{characters_limit: characters_limit, hubs_limit: hubs_limit})
|
||||
|> update_map(%{
|
||||
characters_limit: characters_limit,
|
||||
hubs_limit: hubs_limit,
|
||||
subscription_plan: plan
|
||||
})
|
||||
|
||||
map_id
|
||||
|> get_map!()
|
||||
@@ -342,6 +357,99 @@ defmodule WandererApp.Map do
|
||||
|> get_map!()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates SSE enabled setting in the map cache.
|
||||
Called when the map's sse_enabled setting changes.
|
||||
"""
|
||||
def update_sse_enabled(map_id, sse_enabled)
|
||||
when is_binary(map_id) and is_boolean(sse_enabled) do
|
||||
update_map(map_id, %{sse_enabled: sse_enabled})
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates webhooks enabled setting in the map cache.
|
||||
Called when the map's webhooks_enabled setting changes.
|
||||
"""
|
||||
def update_webhooks_enabled(map_id, webhooks_enabled)
|
||||
when is_binary(map_id) and is_boolean(webhooks_enabled) do
|
||||
update_map(map_id, %{webhooks_enabled: webhooks_enabled})
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if SSE is enabled for a map using the cache.
|
||||
Falls back to DB query if map is not in cache.
|
||||
Returns a boolean (defaults to false if map not found).
|
||||
"""
|
||||
def sse_enabled?(map_id) do
|
||||
case get_map(map_id) do
|
||||
{:ok, map} ->
|
||||
Map.get(map, :sse_enabled, false)
|
||||
|
||||
{:error, :not_found} ->
|
||||
# Cache miss - fall back to DB
|
||||
case WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, db_map} -> db_map.sse_enabled
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if SSE is enabled for a map with explicit not_found handling.
|
||||
Returns {:ok, boolean} or {:error, :not_found}.
|
||||
"""
|
||||
def sse_enabled_with_status(map_id) do
|
||||
case get_map(map_id) do
|
||||
{:ok, map} ->
|
||||
{:ok, Map.get(map, :sse_enabled, false)}
|
||||
|
||||
{:error, :not_found} ->
|
||||
# Cache miss - fall back to DB
|
||||
case WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, db_map} -> {:ok, db_map.sse_enabled}
|
||||
_ -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if webhooks are enabled for a map using the cache.
|
||||
Falls back to DB query if map is not in cache.
|
||||
"""
|
||||
def webhooks_enabled?(map_id) do
|
||||
case get_map(map_id) do
|
||||
{:ok, map} ->
|
||||
Map.get(map, :webhooks_enabled, false)
|
||||
|
||||
{:error, :not_found} ->
|
||||
# Cache miss - fall back to DB
|
||||
case WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, db_map} -> db_map.webhooks_enabled
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if subscription is active for a map using the cache.
|
||||
Returns {:ok, true} if active, {:ok, false} if not, or {:error, :not_cached} if not in cache.
|
||||
|
||||
Note: In CE mode (subscriptions disabled), use is_subscription_active?/1 which
|
||||
handles this case without cache lookup.
|
||||
"""
|
||||
def subscription_active_cached?(map_id) do
|
||||
case get_map(map_id) do
|
||||
{:ok, map} ->
|
||||
plan = Map.get(map, :subscription_plan, @default_subscription_plan)
|
||||
{:ok, plan != @default_subscription_plan}
|
||||
|
||||
_ ->
|
||||
{:error, :not_cached}
|
||||
end
|
||||
end
|
||||
|
||||
def add_systems!(map, []), do: map
|
||||
|
||||
def add_systems!(%{map_id: map_id} = map, [system | rest]) do
|
||||
|
||||
@@ -152,7 +152,8 @@ defmodule WandererApp.Map.Manager do
|
||||
"[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 ->
|
||||
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"
|
||||
@@ -178,7 +179,10 @@ defmodule WandererApp.Map.Manager do
|
||||
Ash.destroy!(ping)
|
||||
end)
|
||||
|
||||
Logger.info("[cleanup_orphaned_pings] Cleaned up #{length(orphaned_pings)} orphaned pings")
|
||||
Logger.info(
|
||||
"[cleanup_orphaned_pings] Cleaned up #{length(orphaned_pings)} orphaned pings"
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
|
||||
@@ -126,4 +126,12 @@ defmodule WandererApp.Map.Operations do
|
||||
@doc "Delete a signature in a map"
|
||||
@spec delete_signature(String.t(), String.t()) :: :ok | {:error, String.t()}
|
||||
defdelegate delete_signature(map_id, sig_id), to: Signatures
|
||||
|
||||
@doc "Link a signature to a target system"
|
||||
@spec link_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
defdelegate link_signature(conn, sig_id, params), to: Signatures
|
||||
|
||||
@doc "Unlink a signature from its target system"
|
||||
@spec unlink_signature(Plug.Conn.t(), String.t()) :: {:ok, map()} | {:error, atom()}
|
||||
defdelegate unlink_signature(conn, sig_id), to: Signatures
|
||||
end
|
||||
|
||||
@@ -63,13 +63,31 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
if is_nil(src_info) or is_nil(tgt_info) do
|
||||
{:error, :invalid_system_info}
|
||||
else
|
||||
# Get wormhole_type for ship size inference
|
||||
wormhole_type = attrs["wormhole_type"]
|
||||
|
||||
# Build extra_info map with optional connection attributes
|
||||
extra_info =
|
||||
%{}
|
||||
|> maybe_add_extra("time_status", attrs["time_status"])
|
||||
|> maybe_add_extra("mass_status", attrs["mass_status"])
|
||||
|> maybe_add_extra("locked", attrs["locked"])
|
||||
|> maybe_add_extra("wormhole_type", wormhole_type)
|
||||
|
||||
info = %{
|
||||
solar_system_source_id: src_info.solar_system_id,
|
||||
solar_system_target_id: tgt_info.solar_system_id,
|
||||
character_id: char_id,
|
||||
type: parse_type(attrs["type"]),
|
||||
ship_size_type:
|
||||
resolve_ship_size(attrs["type"], attrs["ship_size_type"], src_info, tgt_info)
|
||||
resolve_ship_size(
|
||||
attrs["type"],
|
||||
attrs["ship_size_type"],
|
||||
wormhole_type,
|
||||
src_info,
|
||||
tgt_info
|
||||
),
|
||||
extra_info: if(extra_info == %{}, do: nil, else: extra_info)
|
||||
}
|
||||
|
||||
case Server.add_connection(map_id, info) do
|
||||
@@ -95,10 +113,11 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
|
||||
# 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
|
||||
# If wormhole_type is provided (e.g., "H296"), infer ship size from it.
|
||||
defp resolve_ship_size(type_val, ship_size_val, wormhole_type, src_info, tgt_info) do
|
||||
case parse_type(type_val) do
|
||||
@connection_type_wormhole ->
|
||||
wormhole_ship_size(ship_size_val, src_info, tgt_info)
|
||||
wormhole_ship_size(ship_size_val, wormhole_type, src_info, tgt_info)
|
||||
|
||||
_other ->
|
||||
# Stargates and others just use the parsed or default size
|
||||
@@ -108,15 +127,45 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
|
||||
# -- Wormhole‑specific sizing rules ----------------------------------------
|
||||
|
||||
defp wormhole_ship_size(ship_size_val, src, tgt) do
|
||||
defp wormhole_ship_size(ship_size_val, wormhole_type, src, tgt) do
|
||||
# First, try to infer from wormhole_type (e.g., "H296", "C5", etc.)
|
||||
inferred_size = infer_ship_size_from_wormhole_type(wormhole_type)
|
||||
# Parse ship_size_val early to handle string values correctly
|
||||
parsed_ship_size = parse_ship_size(ship_size_val, nil)
|
||||
|
||||
cond do
|
||||
c1_system?(src, tgt) -> @medium_ship_size
|
||||
c13_system?(src, tgt) -> @small_ship_size
|
||||
c4_to_ns?(src, tgt) -> @small_ship_size
|
||||
true -> parse_ship_size(ship_size_val, @large_ship_size)
|
||||
# If user explicitly provided a ship_size_val, use it
|
||||
not is_nil(parsed_ship_size) ->
|
||||
parsed_ship_size
|
||||
|
||||
# If we could infer from wormhole_type, use that
|
||||
not is_nil(inferred_size) ->
|
||||
inferred_size
|
||||
|
||||
# Otherwise fall back to system class rules
|
||||
c1_system?(src, tgt) ->
|
||||
@medium_ship_size
|
||||
|
||||
c13_system?(src, tgt) ->
|
||||
@small_ship_size
|
||||
|
||||
c4_to_ns?(src, tgt) ->
|
||||
@small_ship_size
|
||||
|
||||
true ->
|
||||
@large_ship_size
|
||||
end
|
||||
end
|
||||
|
||||
# Infer ship size from wormhole type name using EVE static data
|
||||
defp infer_ship_size_from_wormhole_type(nil), do: nil
|
||||
defp infer_ship_size_from_wormhole_type(""), do: nil
|
||||
defp infer_ship_size_from_wormhole_type("K162"), do: nil
|
||||
|
||||
defp infer_ship_size_from_wormhole_type(wormhole_type) do
|
||||
WandererApp.Utils.EVEUtil.get_wh_size(wormhole_type)
|
||||
end
|
||||
|
||||
defp c1_system?(%{system_class: @c1_system_class}, _), do: true
|
||||
defp c1_system?(_, %{system_class: @c1_system_class}), do: true
|
||||
defp c1_system?(_, _), do: false
|
||||
@@ -162,6 +211,9 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
|
||||
defp parse_type(_), do: @connection_type_wormhole
|
||||
|
||||
defp maybe_add_extra(map, _key, nil), do: map
|
||||
defp maybe_add_extra(map, key, value), do: Map.put(map, key, value)
|
||||
|
||||
defp parse_int(nil, field), do: {:error, {:missing_field, field}}
|
||||
defp parse_int(val, _) when is_integer(val), do: {:ok, val}
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
|
||||
require Logger
|
||||
alias WandererApp.Map.Operations
|
||||
alias WandererApp.Map.Operations.Connections
|
||||
alias WandererApp.Api.{Character, MapSystem, MapSystemSignature}
|
||||
alias WandererApp.Map.Server
|
||||
alias WandererApp.Utils.EVEUtil
|
||||
|
||||
@spec validate_character_eve_id(map() | nil, String.t()) ::
|
||||
{:ok, String.t()} | {:error, :invalid_character} | {:error, :unexpected_error}
|
||||
@@ -78,8 +80,7 @@ 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.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: solar_system_id}) do
|
||||
{:ok, system} <- ensure_system_on_map(map_id, solar_system_id, user_id, char_id) do
|
||||
attrs =
|
||||
params
|
||||
|> Map.put("system_id", system.id)
|
||||
@@ -95,6 +96,21 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
delete_connection_with_sigs: false
|
||||
}) do
|
||||
:ok ->
|
||||
# Handle linked_system_id if provided - auto-add system and create/update connection
|
||||
linked_system_id = Map.get(params, "linked_system_id")
|
||||
wormhole_type = Map.get(params, "type")
|
||||
|
||||
if is_integer(linked_system_id) and linked_system_id != solar_system_id do
|
||||
handle_linked_system(
|
||||
map_id,
|
||||
solar_system_id,
|
||||
linked_system_id,
|
||||
wormhole_type,
|
||||
user_id,
|
||||
char_id
|
||||
)
|
||||
end
|
||||
|
||||
# Try to fetch the created signature to return with proper fields
|
||||
with {:ok, sigs} <-
|
||||
MapSystemSignature.by_system_id_and_eve_ids(system.id, [attrs["eve_id"]]),
|
||||
@@ -130,6 +146,13 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
Logger.error("[create_signature] Unexpected error during character validation")
|
||||
{:error, :unexpected_error}
|
||||
|
||||
{:error, :invalid_solar_system} ->
|
||||
Logger.error(
|
||||
"[create_signature] Invalid solar_system_id: #{solar_system_id} (not a valid EVE system)"
|
||||
)
|
||||
|
||||
{:error, :invalid_solar_system}
|
||||
|
||||
_ ->
|
||||
Logger.error(
|
||||
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
|
||||
@@ -148,6 +171,203 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
|
||||
def create_signature(_conn, _params), do: {:error, :missing_params}
|
||||
|
||||
# Check cache (not DB) to ensure system is actually visible on the map.
|
||||
@spec ensure_system_on_map(String.t(), integer(), String.t(), String.t()) ::
|
||||
{:ok, map()} | {:error, atom()}
|
||||
defp ensure_system_on_map(map_id, solar_system_id, user_id, char_id) do
|
||||
case WandererApp.Map.find_system_by_location(map_id, %{solar_system_id: solar_system_id}) do
|
||||
nil -> add_system_to_map(map_id, solar_system_id, user_id, char_id)
|
||||
system -> {:ok, system}
|
||||
end
|
||||
end
|
||||
|
||||
@spec add_system_to_map(String.t(), integer(), String.t(), String.t()) ::
|
||||
{:ok, map()} | {:error, atom()}
|
||||
defp add_system_to_map(map_id, solar_system_id, user_id, char_id) do
|
||||
with {:ok, static_info} when not is_nil(static_info) <-
|
||||
WandererApp.CachedInfo.get_system_static_info(solar_system_id),
|
||||
:ok <-
|
||||
Server.add_system(
|
||||
map_id,
|
||||
%{solar_system_id: solar_system_id, coordinates: nil},
|
||||
user_id,
|
||||
char_id
|
||||
),
|
||||
system when not is_nil(system) <- fetch_system_after_add(map_id, solar_system_id) do
|
||||
Logger.info("[create_signature] Auto-added system #{solar_system_id} to map #{map_id}")
|
||||
{:ok, system}
|
||||
else
|
||||
{:ok, nil} ->
|
||||
{:error, :invalid_solar_system}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, :invalid_solar_system}
|
||||
|
||||
nil ->
|
||||
Logger.error("[add_system_to_map] Failed to fetch system after add")
|
||||
{:error, :system_add_failed}
|
||||
|
||||
error ->
|
||||
Logger.error("[add_system_to_map] Failed to add system: #{inspect(error)}")
|
||||
{:error, :system_add_failed}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_system_after_add(map_id, solar_system_id) do
|
||||
case WandererApp.Map.find_system_by_location(map_id, %{solar_system_id: solar_system_id}) do
|
||||
nil ->
|
||||
case MapSystem.read_by_map_and_solar_system(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id
|
||||
}) do
|
||||
{:ok, system} -> system
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
system ->
|
||||
system
|
||||
end
|
||||
end
|
||||
|
||||
# Handles the linked_system_id logic: auto-adds the linked system and creates/updates connection
|
||||
@spec handle_linked_system(
|
||||
String.t(),
|
||||
integer(),
|
||||
integer(),
|
||||
String.t() | nil,
|
||||
String.t(),
|
||||
String.t()
|
||||
) :: :ok | {:error, atom()}
|
||||
defp handle_linked_system(
|
||||
map_id,
|
||||
source_system_id,
|
||||
linked_system_id,
|
||||
wormhole_type,
|
||||
user_id,
|
||||
char_id
|
||||
) do
|
||||
# Ensure the linked system is on the map
|
||||
case ensure_system_on_map(map_id, linked_system_id, user_id, char_id) do
|
||||
{:ok, _linked_system} ->
|
||||
# Check if connection exists between the systems
|
||||
case Connections.get_connection_by_systems(map_id, source_system_id, linked_system_id) do
|
||||
{:ok, nil} ->
|
||||
# No connection exists, create one
|
||||
create_connection_with_wormhole_type(
|
||||
map_id,
|
||||
source_system_id,
|
||||
linked_system_id,
|
||||
wormhole_type,
|
||||
char_id
|
||||
)
|
||||
|
||||
{:ok, _existing_conn} ->
|
||||
# Connection exists, update wormhole type if provided
|
||||
update_connection_wormhole_type(
|
||||
map_id,
|
||||
source_system_id,
|
||||
linked_system_id,
|
||||
wormhole_type
|
||||
)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning(
|
||||
"[handle_linked_system] Failed to check connection: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:error, :connection_check_failed}
|
||||
end
|
||||
|
||||
{:error, :invalid_solar_system} ->
|
||||
Logger.warning(
|
||||
"[handle_linked_system] Invalid linked_system_id: #{linked_system_id} (not a valid EVE system)"
|
||||
)
|
||||
|
||||
{:error, :invalid_linked_system}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("[handle_linked_system] Failed to add linked system: #{inspect(reason)}")
|
||||
{:error, :linked_system_add_failed}
|
||||
end
|
||||
end
|
||||
|
||||
# Creates a connection between two systems with the specified wormhole type
|
||||
@spec create_connection_with_wormhole_type(
|
||||
String.t(),
|
||||
integer(),
|
||||
integer(),
|
||||
String.t() | nil,
|
||||
String.t()
|
||||
) :: :ok | {:error, atom()}
|
||||
defp create_connection_with_wormhole_type(
|
||||
map_id,
|
||||
source_system_id,
|
||||
target_system_id,
|
||||
wormhole_type,
|
||||
char_id
|
||||
) do
|
||||
conn_attrs = %{
|
||||
"solar_system_source" => source_system_id,
|
||||
"solar_system_target" => target_system_id,
|
||||
"type" => 0,
|
||||
"wormhole_type" => wormhole_type
|
||||
}
|
||||
|
||||
case Connections.create(conn_attrs, map_id, char_id) do
|
||||
{:ok, :created} ->
|
||||
Logger.info(
|
||||
"[create_signature] Auto-created connection #{source_system_id} <-> #{target_system_id} (type: #{wormhole_type || "unknown"})"
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
{:skip, :exists} ->
|
||||
# Connection already exists (race condition), update it instead
|
||||
update_connection_wormhole_type(map_id, source_system_id, target_system_id, wormhole_type)
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
"[create_connection_with_wormhole_type] Failed to create connection: #{inspect(error)}"
|
||||
)
|
||||
|
||||
{:error, :connection_create_failed}
|
||||
end
|
||||
end
|
||||
|
||||
# Updates the wormhole type and ship size for an existing connection
|
||||
@spec update_connection_wormhole_type(String.t(), integer(), integer(), String.t() | nil) ::
|
||||
:ok | {:error, atom()}
|
||||
defp update_connection_wormhole_type(_map_id, _source, _target, nil), do: :ok
|
||||
|
||||
defp update_connection_wormhole_type(map_id, source_system_id, target_system_id, wormhole_type) do
|
||||
# Get ship size from wormhole type
|
||||
ship_size_type = EVEUtil.get_wh_size(wormhole_type)
|
||||
|
||||
if not is_nil(ship_size_type) do
|
||||
case Server.update_connection_ship_size_type(map_id, %{
|
||||
solar_system_source_id: source_system_id,
|
||||
solar_system_target_id: target_system_id,
|
||||
ship_size_type: ship_size_type
|
||||
}) do
|
||||
:ok ->
|
||||
Logger.info(
|
||||
"[create_signature] Updated connection #{source_system_id} <-> #{target_system_id} ship_size_type to #{ship_size_type} (wormhole: #{wormhole_type})"
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
"[update_connection_wormhole_type] Failed to update ship size: #{inspect(error)}"
|
||||
)
|
||||
|
||||
{:error, :ship_size_update_failed}
|
||||
end
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def update_signature(
|
||||
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
|
||||
@@ -249,4 +469,197 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
end
|
||||
|
||||
def delete_signature(_conn, _sig_id), do: {:error, :missing_params}
|
||||
|
||||
@doc """
|
||||
Links a signature to a target system, creating the association between
|
||||
the signature and the wormhole connection to that system.
|
||||
|
||||
This also:
|
||||
- Updates the signature's group to "Wormhole"
|
||||
- Sets the target system's linked_sig_eve_id
|
||||
- Copies temporary_name from signature to target system
|
||||
- Updates connection time_status and ship_size_type from signature data
|
||||
"""
|
||||
@spec link_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def link_signature(
|
||||
%{assigns: %{map_id: map_id}} = _conn,
|
||||
sig_id,
|
||||
%{"solar_system_target" => solar_system_target}
|
||||
)
|
||||
when is_integer(solar_system_target) do
|
||||
with {:ok, signature} <- MapSystemSignature.by_id(sig_id),
|
||||
{:ok, source_system} <- MapSystem.by_id(signature.system_id),
|
||||
true <- source_system.map_id == map_id,
|
||||
target_system when not is_nil(target_system) <-
|
||||
WandererApp.Map.find_system_by_location(map_id, %{solar_system_id: solar_system_target}) do
|
||||
# Update signature group to Wormhole and set linked_system_id
|
||||
{:ok, updated_signature} =
|
||||
signature
|
||||
|> MapSystemSignature.update_group!(%{group: "Wormhole"})
|
||||
|> MapSystemSignature.update_linked_system(%{linked_system_id: solar_system_target})
|
||||
|
||||
# Update target system if it has no linked signature or is already linked to the same signature
|
||||
if is_nil(target_system.linked_sig_eve_id) or
|
||||
target_system.linked_sig_eve_id == signature.eve_id do
|
||||
# Set the target system's linked_sig_eve_id
|
||||
Server.update_system_linked_sig_eve_id(map_id, %{
|
||||
solar_system_id: solar_system_target,
|
||||
linked_sig_eve_id: signature.eve_id
|
||||
})
|
||||
|
||||
# Copy temporary_name if present
|
||||
if not is_nil(signature.temporary_name) do
|
||||
Server.update_system_temporary_name(map_id, %{
|
||||
solar_system_id: solar_system_target,
|
||||
temporary_name: signature.temporary_name
|
||||
})
|
||||
end
|
||||
|
||||
# Update connection time_status from signature custom_info
|
||||
signature_time_status =
|
||||
if not is_nil(signature.custom_info) do
|
||||
case Jason.decode(signature.custom_info) do
|
||||
{:ok, map} -> Map.get(map, "time_status")
|
||||
{:error, _} -> nil
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
# Update connection ship_size_type from signature wormhole type
|
||||
signature_ship_size_type = EVEUtil.get_wh_size(signature.type)
|
||||
|
||||
# Back-link detection: if current signature yields no ship_size_type (e.g., K162),
|
||||
# look for a forward signature in the target system that links back to our source
|
||||
{signature_time_status, signature_ship_size_type} =
|
||||
if is_nil(signature_ship_size_type) do
|
||||
case Server.SignaturesImpl.find_forward_signature(
|
||||
target_system.id,
|
||||
source_system.solar_system_id
|
||||
) do
|
||||
nil ->
|
||||
{signature_time_status, signature_ship_size_type}
|
||||
|
||||
forward_sig ->
|
||||
Logger.info(
|
||||
"[link_signature] Back-link detected: " <>
|
||||
"using forward sig type=#{forward_sig.type} from target system"
|
||||
)
|
||||
|
||||
forward_ship_size = EVEUtil.get_wh_size(forward_sig.type)
|
||||
|
||||
forward_time_status =
|
||||
if is_nil(signature_time_status) and not is_nil(forward_sig.custom_info) do
|
||||
case Jason.decode(forward_sig.custom_info) do
|
||||
{:ok, map} -> Map.get(map, "time_status")
|
||||
{:error, _} -> nil
|
||||
end
|
||||
else
|
||||
signature_time_status
|
||||
end
|
||||
|
||||
{forward_time_status, forward_ship_size}
|
||||
end
|
||||
else
|
||||
{signature_time_status, signature_ship_size_type}
|
||||
end
|
||||
|
||||
if not is_nil(signature_time_status) do
|
||||
Server.update_connection_time_status(map_id, %{
|
||||
solar_system_source_id: source_system.solar_system_id,
|
||||
solar_system_target_id: solar_system_target,
|
||||
time_status: signature_time_status
|
||||
})
|
||||
end
|
||||
|
||||
if not is_nil(signature_ship_size_type) do
|
||||
Server.update_connection_ship_size_type(map_id, %{
|
||||
solar_system_source_id: source_system.solar_system_id,
|
||||
solar_system_target_id: solar_system_target,
|
||||
ship_size_type: signature_ship_size_type
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
# Broadcast update
|
||||
Server.Impl.broadcast!(map_id, :signatures_updated, source_system.solar_system_id)
|
||||
|
||||
# Return the updated signature
|
||||
result =
|
||||
updated_signature
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, source_system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
{:ok, result}
|
||||
else
|
||||
false ->
|
||||
{:error, :not_found}
|
||||
|
||||
nil ->
|
||||
{:error, :target_system_not_found}
|
||||
|
||||
{:error, %Ash.Error.Query.NotFound{}} ->
|
||||
{:error, :not_found}
|
||||
|
||||
err ->
|
||||
Logger.error("[link_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def link_signature(_conn, _sig_id, %{"solar_system_target" => _}),
|
||||
do: {:error, :invalid_solar_system_target}
|
||||
|
||||
def link_signature(_conn, _sig_id, _params), do: {:error, :missing_params}
|
||||
|
||||
@doc """
|
||||
Unlinks a signature from its target system.
|
||||
"""
|
||||
@spec unlink_signature(Plug.Conn.t(), String.t()) :: {:ok, map()} | {:error, atom()}
|
||||
def unlink_signature(%{assigns: %{map_id: map_id}} = _conn, sig_id) do
|
||||
with {:ok, signature} <- MapSystemSignature.by_id(sig_id),
|
||||
{:ok, source_system} <- MapSystem.by_id(signature.system_id),
|
||||
:ok <- if(source_system.map_id == map_id, do: :ok, else: {:error, :not_found}),
|
||||
:ok <- if(not is_nil(signature.linked_system_id), do: :ok, else: {:error, :not_linked}) do
|
||||
# Clear the target system's linked_sig_eve_id
|
||||
Server.update_system_linked_sig_eve_id(map_id, %{
|
||||
solar_system_id: signature.linked_system_id,
|
||||
linked_sig_eve_id: nil
|
||||
})
|
||||
|
||||
# Clear the signature's linked_system_id using the wrapper for logging
|
||||
{:ok, updated_signature} =
|
||||
Server.SignaturesImpl.update_signature_linked_system(signature, %{
|
||||
linked_system_id: nil
|
||||
})
|
||||
|
||||
# Broadcast update
|
||||
Server.Impl.broadcast!(map_id, :signatures_updated, source_system.solar_system_id)
|
||||
|
||||
# Return the updated signature
|
||||
result =
|
||||
updated_signature
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, source_system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
{:ok, result}
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, :not_linked} ->
|
||||
{:error, :not_linked}
|
||||
|
||||
{:error, %Ash.Error.Query.NotFound{}} ->
|
||||
{:error, :not_found}
|
||||
|
||||
err ->
|
||||
Logger.error("[unlink_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def unlink_signature(_conn, _sig_id), do: {:error, :missing_params}
|
||||
end
|
||||
|
||||
@@ -36,7 +36,8 @@ defmodule WandererApp.Map.Operations.Systems do
|
||||
# Private helper for batch upsert
|
||||
defp create_system_batch(%{map_id: map_id, user_id: user_id, char_id: char_id}, params) do
|
||||
with {:ok, solar_system_id} <- fetch_system_id(params) do
|
||||
update_existing = fetch_update_existing(params, false)
|
||||
# Default to true so re-submitting with new position updates the system
|
||||
update_existing = fetch_update_existing(params, true)
|
||||
|
||||
map_id
|
||||
|> WandererApp.Map.check_location(%{solar_system_id: solar_system_id})
|
||||
@@ -46,9 +47,13 @@ defmodule WandererApp.Map.Operations.Systems do
|
||||
|
||||
{:error, :already_exists} ->
|
||||
if update_existing do
|
||||
do_update_system(map_id, user_id, char_id, solar_system_id, params)
|
||||
# Mark as skip so it counts as "updated" not "created"
|
||||
case do_update_system(map_id, user_id, char_id, solar_system_id, params) do
|
||||
{:ok, _} -> {:skip, :updated}
|
||||
error -> error
|
||||
end
|
||||
else
|
||||
:ok
|
||||
{:skip, :already_exists}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -200,16 +205,22 @@ defmodule WandererApp.Map.Operations.Systems do
|
||||
|
||||
defp normalize_coordinates(%{"coordinates" => %{"x" => x, "y" => y}})
|
||||
when is_number(x) and is_number(y),
|
||||
do: %{x: x, y: y}
|
||||
do: %{"x" => x, "y" => y}
|
||||
|
||||
defp normalize_coordinates(%{coordinates: %{x: x, y: y}}) when is_number(x) and is_number(y),
|
||||
do: %{x: x, y: y}
|
||||
do: %{"x" => x, "y" => y}
|
||||
|
||||
defp normalize_coordinates(params) do
|
||||
%{
|
||||
x: params |> Map.get("position_x", Map.get(params, :position_x, 0)),
|
||||
y: params |> Map.get("position_y", Map.get(params, :position_y, 0))
|
||||
}
|
||||
x = params |> Map.get("position_x", Map.get(params, :position_x))
|
||||
y = params |> Map.get("position_y", Map.get(params, :position_y))
|
||||
|
||||
# Only return coordinates if both x and y are provided
|
||||
# Otherwise return nil to let the server use auto-positioning
|
||||
if is_number(x) and is_number(y) do
|
||||
%{"x" => x, "y" => y}
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_system_updates(map_id, system_id, attrs, %{x: x, y: y}) do
|
||||
|
||||
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, count \\ 1) 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: count,
|
||||
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
|
||||
@@ -595,6 +595,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
time_status = get_extra_info(extra_info, "time_status", time_status)
|
||||
mass_status = get_extra_info(extra_info, "mass_status", 0)
|
||||
locked = get_extra_info(extra_info, "locked", false)
|
||||
wormhole_type = get_extra_info(extra_info, "wormhole_type", nil)
|
||||
|
||||
{:ok, connection} =
|
||||
WandererApp.MapConnectionRepo.create(%{
|
||||
@@ -605,7 +606,8 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
ship_size_type: ship_size_type,
|
||||
time_status: time_status,
|
||||
mass_status: mass_status,
|
||||
locked: locked
|
||||
locked: locked,
|
||||
wormhole_type: wormhole_type
|
||||
})
|
||||
|
||||
if connection_type == @connection_type_wormhole do
|
||||
@@ -915,8 +917,10 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
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
|
||||
# No stargate = wormhole connection
|
||||
{:ok, []} -> true
|
||||
# Stargate exists or error
|
||||
_ -> false
|
||||
end
|
||||
else
|
||||
false
|
||||
|
||||
@@ -72,7 +72,6 @@ defmodule WandererApp.Map.Server.PingsImpl do
|
||||
type: type
|
||||
} = _ping_info
|
||||
) do
|
||||
|
||||
result = WandererApp.MapPingsRepo.get_by_id(ping_id)
|
||||
|
||||
case result do
|
||||
|
||||
@@ -109,8 +109,10 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
nil ->
|
||||
MapSystemSignature.create!(sig)
|
||||
|
||||
_ ->
|
||||
:noop
|
||||
existing ->
|
||||
# If signature already exists, update it instead of ignoring
|
||||
# This handles the case where frontend sends existing sigs as "added"
|
||||
apply_update_signature(map_id, existing, sig)
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -273,6 +275,21 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
|
||||
defp maybe_update_connection_mass_status(_map_id, _old_sig, _updated_sig), do: :ok
|
||||
|
||||
@doc """
|
||||
Finds the "forward" signature in a target system that links back to the source system.
|
||||
Used for back-link detection: when a K162 is linked from System B → System A,
|
||||
finds the existing signature in System A that already links to System B (e.g., H296).
|
||||
"""
|
||||
def find_forward_signature(target_system_uuid, source_solar_system_id) do
|
||||
target_system_uuid
|
||||
|> MapSystemSignature.by_system_id!()
|
||||
|> Enum.find(fn sig -> sig.linked_system_id == source_solar_system_id end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.warning("[find_forward_signature] Error: #{inspect(e)}")
|
||||
nil
|
||||
end
|
||||
|
||||
@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
|
||||
@@ -317,7 +334,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
@doc false
|
||||
defp parse_signatures(signatures, character_eve_id, system_id) do
|
||||
Enum.map(signatures, fn sig ->
|
||||
%{
|
||||
base = %{
|
||||
system_id: system_id,
|
||||
eve_id: sig["eve_id"],
|
||||
name: sig["name"],
|
||||
@@ -331,6 +348,15 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
character_eve_id: Map.get(sig, "character_eve_id", character_eve_id),
|
||||
deleted: false
|
||||
}
|
||||
|
||||
# Only include linked_system_id when explicitly provided in the payload.
|
||||
# Frontend sends "linked_system" (object), not "linked_system_id" (integer).
|
||||
# Including nil would silently clear the DB value via the Ash :update action.
|
||||
if Map.has_key?(sig, "linked_system_id") do
|
||||
Map.put(base, :linked_system_id, sig["linked_system_id"])
|
||||
else
|
||||
base
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -3,4 +3,7 @@ defmodule WandererApp.MapTransactionRepo do
|
||||
|
||||
def create(transaction),
|
||||
do: WandererApp.Api.MapTransaction.create(transaction)
|
||||
|
||||
def top_donators(map_id, after_date \\ nil),
|
||||
do: WandererApp.Api.MapTransaction.top_donators(%{map_id: map_id, after: after_date})
|
||||
end
|
||||
|
||||
268
lib/wanderer_app/route_builder_client.ex
Normal file
268
lib/wanderer_app/route_builder_client.ex
Normal file
@@ -0,0 +1,268 @@
|
||||
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"])
|
||||
@available_routes_by ["blueLoot", "redLoot", "thera", "turnur", "so_cleaning", "trade_hubs"]
|
||||
|
||||
def available_routes_by(), do: @available_routes_by
|
||||
|
||||
def find_closest(
|
||||
%{
|
||||
origin: origin,
|
||||
flag: flag,
|
||||
connections: connections,
|
||||
avoid: avoid,
|
||||
count: count,
|
||||
type: type,
|
||||
security_type: security_type
|
||||
} = payload
|
||||
) do
|
||||
url = "#{WandererApp.Env.custom_route_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
|
||||
161
lib/wanderer_app/server/turnur_data_fetcher.ex
Normal file
161
lib/wanderer_app/server/turnur_data_fetcher.ex
Normal file
@@ -0,0 +1,161 @@
|
||||
defmodule WandererApp.Server.TurnurDataFetcher do
|
||||
@moduledoc false
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
@name :turnur_data_fetcher
|
||||
@system_name "turnur"
|
||||
|
||||
defstruct [
|
||||
:retries_count,
|
||||
:restart_timeout
|
||||
]
|
||||
|
||||
@eve_scout_base_url "https://api.eve-scout.com/v2/public"
|
||||
@refresh_timeout :timer.minutes(1)
|
||||
|
||||
@initial_state %{
|
||||
retries_count: 5,
|
||||
restart_timeout: @refresh_timeout
|
||||
}
|
||||
|
||||
def get_chain_pairs(params) do
|
||||
case WandererApp.Cache.get(@name) do
|
||||
nil ->
|
||||
{:ok, []}
|
||||
|
||||
data ->
|
||||
{:ok,
|
||||
data
|
||||
|> Enum.filter(fn item -> _is_filtered(item, params) end)
|
||||
|> Enum.map(fn item ->
|
||||
%{
|
||||
first: item.source_solar_system_id,
|
||||
second: item.destination_solar_system_id
|
||||
}
|
||||
end)}
|
||||
end
|
||||
end
|
||||
|
||||
defp _is_filtered(%{ship_size_type: 0}, %{
|
||||
include_frig: false
|
||||
}),
|
||||
do: false
|
||||
|
||||
defp _is_filtered(%{time_status: 1}, %{
|
||||
include_eol: false
|
||||
}),
|
||||
do: false
|
||||
|
||||
defp _is_filtered(%{time_status: 2}, %{
|
||||
include_mass_crit: false
|
||||
}),
|
||||
do: false
|
||||
|
||||
defp _is_filtered(_, _), do: true
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start(__MODULE__, opts, name: @name)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
Logger.info("#{__MODULE__} started")
|
||||
|
||||
{:ok, @initial_state, {:continue, :start}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def terminate(_reason, _state) do
|
||||
:ok
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:stop, _, state), do: {:stop, :normal, :ok, state}
|
||||
|
||||
@impl true
|
||||
def handle_call(:error, _, state), do: {:stop, :error, :ok, state}
|
||||
|
||||
@impl true
|
||||
def handle_continue(:start, state) do
|
||||
Process.send_after(self(), :refresh_data, 500)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
:refresh_data,
|
||||
state
|
||||
) do
|
||||
Task.async(fn -> load_data() end)
|
||||
Process.send_after(self(), :refresh_data, @refresh_timeout)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({ref, result}, state) do
|
||||
Process.demonitor(ref, [:flush])
|
||||
|
||||
case result do
|
||||
{:ok, data} ->
|
||||
_cache_items(data)
|
||||
{:noreply, state}
|
||||
|
||||
_ ->
|
||||
Logger.error("#{__MODULE__} failed to load data")
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(_action, state),
|
||||
do: {:noreply, state}
|
||||
|
||||
defp load_data() do
|
||||
case Req.get("#{@eve_scout_base_url}/signatures", params: [system_name: @system_name]) do
|
||||
{:ok, %{status: 200, body: body}} ->
|
||||
{:ok, body |> _get_infos()}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
|
||||
_ ->
|
||||
{:error, "Request failed"}
|
||||
end
|
||||
end
|
||||
|
||||
defp _get_infos(data) do
|
||||
data
|
||||
|> Enum.map(&_get_info/1)
|
||||
end
|
||||
|
||||
defp _get_info(%{
|
||||
"in_system_id" => in_system_id,
|
||||
"max_ship_size" => max_ship_size,
|
||||
"out_system_id" => out_system_id,
|
||||
"remaining_hours" => remaining_hours
|
||||
}) do
|
||||
%{
|
||||
source_solar_system_id: in_system_id,
|
||||
destination_solar_system_id: out_system_id,
|
||||
mass_status: 0,
|
||||
time_status: _get_time_status(remaining_hours),
|
||||
ship_size_type: _get_ship_size(max_ship_size)
|
||||
}
|
||||
end
|
||||
|
||||
defp _get_ship_size("small"), do: 0
|
||||
defp _get_ship_size("medium"), do: 1
|
||||
defp _get_ship_size("large"), do: 1
|
||||
defp _get_ship_size("xlarge"), do: 2
|
||||
defp _get_ship_size(_), do: 1
|
||||
|
||||
defp _get_time_status(remaining_hours) when remaining_hours < 2, do: 0
|
||||
defp _get_time_status(_), do: 1
|
||||
|
||||
defp _cache_items([]), do: WandererApp.Cache.put(@name, [])
|
||||
|
||||
defp _cache_items(items), do: WandererApp.Cache.put(@name, items)
|
||||
end
|
||||
@@ -256,6 +256,11 @@ defmodule WandererAppWeb.Layouts do
|
||||
Admin
|
||||
</.link>
|
||||
</li>
|
||||
<li :if={@show_admin}>
|
||||
<.link navigate="/admin/characters">
|
||||
Characters
|
||||
</.link>
|
||||
</li>
|
||||
<li :if={@show_admin}>
|
||||
<.link navigate="/admin/errors">
|
||||
Errors
|
||||
|
||||
@@ -41,14 +41,18 @@
|
||||
<div class="absolute rounded-m top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black opacity-75 group-hover:opacity-25 transition-opacity duration-300">
|
||||
</div>
|
||||
<div class="absolute w-full bottom-2 p-4">
|
||||
<% {first_part, second_part} = case String.split(post.title, ":", parts: 2) do
|
||||
[first, second] -> {first, second}
|
||||
[first] -> {first, nil}
|
||||
end %>
|
||||
<% {first_part, second_part} =
|
||||
case String.split(post.title, ":", parts: 2) do
|
||||
[first, second] -> {first, second}
|
||||
[first] -> {first, nil}
|
||||
end %>
|
||||
<h3 class="!m-0 !text-s font-bold break-normal ccp-font whitespace-nowrap text-white">
|
||||
{first_part}
|
||||
</h3>
|
||||
<p :if={second_part} class="!m-0 !text-s text-white text-ellipsis overflow-hidden whitespace-nowrap ccp-font">
|
||||
<p
|
||||
:if={second_part}
|
||||
class="!m-0 !text-s text-white text-ellipsis overflow-hidden whitespace-nowrap ccp-font"
|
||||
>
|
||||
{second_part}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -487,10 +487,17 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
)
|
||||
|
||||
def create(conn, params) do
|
||||
# Support both batch format {"systems": [...], "connections": [...]}
|
||||
# and single system format {"solar_system_id": ..., ...}
|
||||
# Support multiple formats:
|
||||
# 1. Batch format: {"systems": [...], "connections": [...]}
|
||||
# 2. Wrapped batch format: {"data": {"systems": [...], "connections": [...]}}
|
||||
# 3. Single system format: {"solar_system_id": ..., ...}
|
||||
{systems, connections} =
|
||||
cond do
|
||||
Map.has_key?(params, "data") and is_map(params["data"]) ->
|
||||
# Wrapped batch format - extract from data wrapper
|
||||
data = params["data"]
|
||||
{Map.get(data, "systems", []), Map.get(data, "connections", [])}
|
||||
|
||||
Map.has_key?(params, "systems") ->
|
||||
# Batch format
|
||||
{Map.get(params, "systems", []), Map.get(params, "connections", [])}
|
||||
|
||||
@@ -190,9 +190,37 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
The `character_eve_id` field is optional. If provided, it must be a valid character
|
||||
that exists in the database, otherwise a 422 error will be returned. If not provided,
|
||||
the signature will be associated with the map owner's character.
|
||||
|
||||
## Auto-add System Behavior
|
||||
|
||||
If the `solar_system_id` is not already on the map, it will be automatically added.
|
||||
The system must be a valid EVE Online solar system ID.
|
||||
|
||||
## Linked System and Connection Behavior
|
||||
|
||||
If `linked_system_id` is provided (for wormhole signatures):
|
||||
- The linked system will be automatically added to the map if not present
|
||||
- A connection will be created between the source and linked systems if one doesn't exist
|
||||
- If a connection already exists, its ship size will be updated based on the wormhole `type`
|
||||
- The wormhole `type` (e.g., "H296", "C2", "K162") is used to determine connection ship size:
|
||||
- H296 → XL/Freighter size (1B kg max mass)
|
||||
- N770, D845 → Large size (375M kg max mass)
|
||||
- etc.
|
||||
"""
|
||||
operation(:create,
|
||||
summary: "Create a new signature",
|
||||
description: """
|
||||
Creates a new cosmic signature in the specified solar system.
|
||||
|
||||
**Auto-add behavior**: If the solar_system_id is not already on the map, it will be
|
||||
automatically added. The system must be a valid EVE Online solar system ID.
|
||||
|
||||
**Linked system behavior**: If linked_system_id is provided:
|
||||
- The linked system is auto-added to the map if not present
|
||||
- A wormhole connection is auto-created between the systems
|
||||
- The connection's ship_size_type is inferred from the wormhole type (e.g., H296 → XL)
|
||||
- If the connection already exists, its ship size is updated based on the wormhole type
|
||||
""",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
@@ -218,7 +246,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
error: %OpenApiSpex.Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"Error type (e.g., 'invalid_character', 'system_not_found', 'missing_params')"
|
||||
"Error type (e.g., 'invalid_character', 'invalid_solar_system', 'missing_params')"
|
||||
}
|
||||
},
|
||||
example: %{error: "invalid_character"}
|
||||
@@ -311,4 +339,117 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Link a signature to a target system.
|
||||
|
||||
This creates the association between a wormhole signature and the system it leads to.
|
||||
It also updates the connection's time_status and ship_size_type based on the signature data.
|
||||
"""
|
||||
operation(:link,
|
||||
summary: "Link a signature to a target system",
|
||||
description: """
|
||||
Links a wormhole signature to its destination system. This operation:
|
||||
- Sets the signature's linked_system_id to the target system
|
||||
- Updates the signature's group to "Wormhole"
|
||||
- Sets the target system's linked_sig_eve_id (if not already set)
|
||||
- Copies temporary_name from signature to target system
|
||||
- Updates the connection's time_status and ship_size_type from signature data
|
||||
""",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true
|
||||
],
|
||||
id: [in: :path, description: "Signature UUID", type: :string, required: true]
|
||||
],
|
||||
request_body:
|
||||
{"Link request", "application/json",
|
||||
%OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_target: %OpenApiSpex.Schema{
|
||||
type: :integer,
|
||||
description: "Target solar system ID to link to"
|
||||
}
|
||||
},
|
||||
required: [:solar_system_target],
|
||||
example: %{solar_system_target: 31_001_922}
|
||||
}},
|
||||
responses: [
|
||||
ok:
|
||||
{"Linked signature", "application/json",
|
||||
%OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @signature_schema},
|
||||
example: %{data: @signature_schema.example}
|
||||
}},
|
||||
unprocessable_entity:
|
||||
{"Error", "application/json",
|
||||
%OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{
|
||||
type: :string,
|
||||
description: "Error type"
|
||||
}
|
||||
},
|
||||
example: %{error: "target_system_not_found"}
|
||||
}}
|
||||
]
|
||||
)
|
||||
|
||||
def link(conn, %{"id" => id} = params) do
|
||||
case MapOperations.link_signature(conn, id, params) do
|
||||
{:ok, sig} -> json(conn, %{data: sig})
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unlink a signature from its target system.
|
||||
"""
|
||||
operation(:unlink,
|
||||
summary: "Unlink a signature from its target system",
|
||||
description: "Removes the link between a signature and its destination system.",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true
|
||||
],
|
||||
id: [in: :path, description: "Signature UUID", type: :string, required: true]
|
||||
],
|
||||
responses: [
|
||||
ok:
|
||||
{"Unlinked signature", "application/json",
|
||||
%OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @signature_schema},
|
||||
example: %{data: Map.put(@signature_schema.example, :linked_system_id, nil)}
|
||||
}},
|
||||
unprocessable_entity:
|
||||
{"Error", "application/json",
|
||||
%OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{
|
||||
type: :string,
|
||||
description: "Error type"
|
||||
}
|
||||
},
|
||||
example: %{error: "not_linked"}
|
||||
}}
|
||||
]
|
||||
)
|
||||
|
||||
def unlink(conn, %{"id" => id}) do
|
||||
case MapOperations.unlink_signature(conn, id) do
|
||||
{:ok, sig} -> json(conn, %{data: sig})
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
27
lib/wanderer_app_web/controllers/route_builder_controller.ex
Normal file
27
lib/wanderer_app_web/controllers/route_builder_controller.ex
Normal file
@@ -0,0 +1,27 @@
|
||||
defmodule WandererAppWeb.RouteBuilderController do
|
||||
use WandererAppWeb, :controller
|
||||
|
||||
require Logger
|
||||
|
||||
def find_closest(conn, params) do
|
||||
payload = %{
|
||||
origin: Map.get(params, "origin") || Map.get(params, :origin),
|
||||
flag: Map.get(params, "flag") || Map.get(params, :flag) || "shortest",
|
||||
connections: Map.get(params, "connections") || Map.get(params, :connections) || [],
|
||||
avoid: Map.get(params, "avoid") || Map.get(params, :avoid) || [],
|
||||
count: Map.get(params, "count") || Map.get(params, :count) || 1,
|
||||
type: Map.get(params, "type") || Map.get(params, :type) || "blueLoot"
|
||||
}
|
||||
|
||||
case WandererApp.RouteBuilderClient.find_closest(payload) do
|
||||
{:ok, body} ->
|
||||
json(conn, body)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("[RouteBuilderController] find_closest failed: #{inspect(reason)}")
|
||||
conn
|
||||
|> put_status(:bad_gateway)
|
||||
|> json(%{error: "route_builder_failed"})
|
||||
end
|
||||
end
|
||||
end
|
||||
157
lib/wanderer_app_web/live/admin/admin_characters_live.ex
Normal file
157
lib/wanderer_app_web/live/admin/admin_characters_live.ex
Normal file
@@ -0,0 +1,157 @@
|
||||
defmodule WandererAppWeb.AdminCharactersLive do
|
||||
@moduledoc """
|
||||
Admin LiveView for viewing all registered characters on the server.
|
||||
"""
|
||||
use WandererAppWeb, :live_view
|
||||
|
||||
alias Phoenix.LiveView.AsyncResult
|
||||
|
||||
@characters_per_page 50
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"user_id" => user_id} = _session, socket)
|
||||
when not is_nil(user_id) and is_connected?(socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(
|
||||
characters: AsyncResult.loading(),
|
||||
search_term: "",
|
||||
show_deleted: true,
|
||||
page: 1,
|
||||
per_page: @characters_per_page,
|
||||
sort_by: :name,
|
||||
sort_dir: :asc
|
||||
)
|
||||
|> load_characters_async()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(
|
||||
characters: AsyncResult.loading(),
|
||||
search_term: "",
|
||||
show_deleted: true,
|
||||
page: 1,
|
||||
per_page: @characters_per_page,
|
||||
sort_by: :name,
|
||||
sort_dir: :asc
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) when is_connected?(socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(_params, _url, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket
|
||||
|> assign(:active_page, :admin)
|
||||
|> assign(:page_title, "Admin - Characters")
|
||||
end
|
||||
|
||||
defp load_characters_async(socket) do
|
||||
socket
|
||||
|> assign_async(:characters, fn -> load_all_characters() end)
|
||||
end
|
||||
|
||||
defp load_all_characters do
|
||||
case WandererApp.Api.Character.admin_all() do
|
||||
{:ok, characters} ->
|
||||
{:ok, %{characters: characters}}
|
||||
|
||||
_ ->
|
||||
{:ok, %{characters: []}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("search", %{"value" => term}, socket) do
|
||||
{:noreply, socket |> assign(:search_term, term) |> assign(:page, 1)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_deleted", _params, socket) do
|
||||
{:noreply,
|
||||
socket |> assign(:show_deleted, not socket.assigns.show_deleted) |> assign(:page, 1)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("sort", %{"field" => field}, socket) do
|
||||
field = String.to_existing_atom(field)
|
||||
|
||||
{sort_by, sort_dir} =
|
||||
if socket.assigns.sort_by == field do
|
||||
{field, toggle_dir(socket.assigns.sort_dir)}
|
||||
else
|
||||
{field, :asc}
|
||||
end
|
||||
|
||||
{:noreply, socket |> assign(sort_by: sort_by, sort_dir: sort_dir, page: 1)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("page", %{"page" => page}, socket) do
|
||||
{:noreply, socket |> assign(:page, String.to_integer(page))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(_event, _params, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def filter_characters(characters, search_term, show_deleted) do
|
||||
characters
|
||||
|> Enum.filter(fn char ->
|
||||
(show_deleted or not char.deleted) and
|
||||
(search_term == "" or
|
||||
String.contains?(String.downcase(char.name || ""), String.downcase(search_term)) or
|
||||
String.contains?(
|
||||
String.downcase(char.corporation_name || ""),
|
||||
String.downcase(search_term)
|
||||
) or
|
||||
String.contains?(
|
||||
String.downcase(char.alliance_name || ""),
|
||||
String.downcase(search_term)
|
||||
))
|
||||
end)
|
||||
end
|
||||
|
||||
def sort_characters(characters, sort_by, sort_dir) do
|
||||
Enum.sort_by(characters, &sort_value(&1, sort_by), sort_dir)
|
||||
end
|
||||
|
||||
defp sort_value(char, :name), do: String.downcase(char.name || "")
|
||||
defp sort_value(char, :corporation), do: String.downcase(char.corporation_name || "")
|
||||
defp sort_value(char, :alliance), do: String.downcase(char.alliance_name || "")
|
||||
defp sort_value(char, :user), do: String.downcase(user_name(char.user))
|
||||
defp sort_value(char, :registered), do: char.inserted_at || ~U[1970-01-01 00:00:00Z]
|
||||
|
||||
defp toggle_dir(:asc), do: :desc
|
||||
defp toggle_dir(:desc), do: :asc
|
||||
|
||||
def paginate(items, page, per_page) do
|
||||
items
|
||||
|> Enum.drop((page - 1) * per_page)
|
||||
|> Enum.take(per_page)
|
||||
end
|
||||
|
||||
def total_pages(items, per_page) do
|
||||
max(1, ceil(length(items) / per_page))
|
||||
end
|
||||
|
||||
def format_date(nil), do: "-"
|
||||
|
||||
def format_date(datetime) do
|
||||
Calendar.strftime(datetime, "%Y-%m-%d %H:%M")
|
||||
end
|
||||
|
||||
def user_name(nil), do: "Unlinked"
|
||||
def user_name(%{name: name}), do: name
|
||||
end
|
||||
166
lib/wanderer_app_web/live/admin/admin_characters_live.html.heex
Normal file
166
lib/wanderer_app_web/live/admin/admin_characters_live.html.heex
Normal file
@@ -0,0 +1,166 @@
|
||||
<main class="w-full h-full col-span-2 lg:col-span-1 p-4 pl-20 overflow-auto">
|
||||
<div class="page-content">
|
||||
<div class="container-fluid px-[0.625rem]">
|
||||
<!-- Header -->
|
||||
<div class="grid grid-cols-1 pb-6">
|
||||
<div class="md:flex items-center justify-between px-[2px]">
|
||||
<h4 class="text-[18px] font-medium text-gray-800 mb-sm-0 grow dark:text-gray-100 mb-2 md:mb-0">
|
||||
Admin - Characters
|
||||
</h4>
|
||||
<.link navigate={~p"/admin"} class="btn btn-ghost btn-sm">
|
||||
<.icon name="hero-arrow-left-solid" class="w-4 h-4" /> Back to Admin
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filters -->
|
||||
<div class="card dark:bg-zinc-800 dark:border-zinc-600 mb-4">
|
||||
<div class="card-body flex flex-row gap-4 items-center">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name, corporation, or alliance..."
|
||||
value={@search_term}
|
||||
phx-keyup="search"
|
||||
phx-debounce="300"
|
||||
name="search"
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
checked={@show_deleted}
|
||||
phx-click="toggle_deleted"
|
||||
/>
|
||||
<span class="text-sm">Show deleted</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Characters Table -->
|
||||
<div class="card dark:bg-zinc-800 dark:border-zinc-600">
|
||||
<div class="card-body">
|
||||
<.async_result :let={characters} assign={@characters}>
|
||||
<:loading>
|
||||
<div class="flex justify-center p-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
</:loading>
|
||||
<:failed :let={reason}>
|
||||
<div class="alert alert-error">{inspect(reason)}</div>
|
||||
</:failed>
|
||||
|
||||
<% filtered = filter_characters(characters, @search_term, @show_deleted) %>
|
||||
<% sorted = sort_characters(filtered, @sort_by, @sort_dir) %>
|
||||
<% paginated = paginate(sorted, @page, @per_page) %>
|
||||
|
||||
<div class="overflow-x-auto !max-h-[60vh] !overflow-y-auto">
|
||||
<table class="table table-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
:for={
|
||||
{label, field} <- [
|
||||
{"Character", :name},
|
||||
{"Corporation", :corporation},
|
||||
{"Alliance", :alliance},
|
||||
{"User Account", :user},
|
||||
{"Registered", :registered}
|
||||
]
|
||||
}
|
||||
phx-click="sort"
|
||||
phx-value-field={field}
|
||||
class="cursor-pointer select-none hover:bg-base-200"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
{label}
|
||||
<span :if={@sort_by == field}>
|
||||
<.icon :if={@sort_dir == :asc} name="hero-chevron-up" class="w-3 h-3" />
|
||||
<.icon
|
||||
:if={@sort_dir == :desc}
|
||||
name="hero-chevron-down"
|
||||
class="w-3 h-3"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="admin-characters" phx-update="replace">
|
||||
<tr :for={char <- paginated} id={"char-#{char.id}"}>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<.avatar url={member_icon_url(char.eve_id)} label={char.name} />
|
||||
<span class={if char.deleted, do: "line-through text-gray-500", else: ""}>
|
||||
{char.name}
|
||||
</span>
|
||||
<span :if={char.deleted} class="badge badge-error badge-sm">
|
||||
Deleted
|
||||
</span>
|
||||
<span :if={char.online} class="badge badge-success badge-sm">
|
||||
Online
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span :if={char.corporation_name}>
|
||||
{char.corporation_name}
|
||||
<span :if={char.corporation_ticker} class="text-gray-400">
|
||||
[{char.corporation_ticker}]
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span :if={char.alliance_name}>
|
||||
{char.alliance_name}
|
||||
<span :if={char.alliance_ticker} class="text-gray-400">
|
||||
[{char.alliance_ticker}]
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>{user_name(char.user)}</td>
|
||||
<td>
|
||||
<span class="text-sm">{format_date(char.inserted_at)}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div :if={length(filtered) > @per_page} class="flex items-center justify-between mt-4">
|
||||
<span class="text-sm text-gray-400">
|
||||
Page {@page} of {total_pages(filtered, @per_page)} ({length(filtered)} characters)
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
phx-click="page"
|
||||
phx-value-page={max(1, @page - 1)}
|
||||
disabled={@page <= 1}
|
||||
class={"btn btn-sm btn-ghost " <> if(@page <= 1, do: "btn-disabled", else: "")}
|
||||
>
|
||||
<.icon name="hero-chevron-left" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
phx-click="page"
|
||||
phx-value-page={min(total_pages(filtered, @per_page), @page + 1)}
|
||||
disabled={@page >= total_pages(filtered, @per_page)}
|
||||
class={"btn btn-sm btn-ghost " <> if(@page >= total_pages(filtered, @per_page), do: "btn-disabled", else: "")}
|
||||
>
|
||||
<.icon name="hero-chevron-right" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div :if={length(filtered) == 0} class="text-center py-8 text-gray-400">
|
||||
No characters found
|
||||
</div>
|
||||
</.async_result>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -24,6 +24,18 @@
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card dark:bg-zinc-800 dark:border-zinc-600">
|
||||
<div class="card-body">
|
||||
<span class="text-gray-400 dark:text-gray-400">Characters</span>
|
||||
<.link
|
||||
class="btn mt-2 w-full btn-neutral rounded-none"
|
||||
navigate={~p"/admin/characters"}
|
||||
>
|
||||
<.icon name="hero-users-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">View All Characters</h3>
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
<div :if={@restrict_maps_creation?} class="card dark:bg-zinc-800 dark:border-zinc-600">
|
||||
<div class="card-body">
|
||||
<.button class="mt-2" type="button" phx-click="create-map">
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
<.icon name="hero-gift-solid" class="w-4 h-4 text-green-400 flex-shrink-0" />
|
||||
<span class="text-sm text-gray-300">
|
||||
Support development by using promocode
|
||||
<code class="ml-1 px-1.5 py-0.5 bg-stone-800 rounded text-green-400 text-xs font-mono">WANDERER</code>
|
||||
<code class="ml-1 px-1.5 py-0.5 bg-stone-800 rounded text-green-400 text-xs font-mono">
|
||||
WANDERER
|
||||
</code>
|
||||
<span class="ml-1">at official</span>
|
||||
</span>
|
||||
<a
|
||||
|
||||
@@ -713,6 +713,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
main_character_eve_id: main_character_eve_id,
|
||||
following_character_eve_id: following_character_eve_id,
|
||||
is_subscription_active: is_subscription_active,
|
||||
available_routes_by: WandererApp.RouteBuilderClient.available_routes_by(),
|
||||
user_permissions: user_permissions,
|
||||
characters: map_characters,
|
||||
options: options,
|
||||
|
||||
@@ -5,6 +5,13 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
|
||||
|
||||
alias WandererAppWeb.{MapEventHandler, MapCoreEventHandler}
|
||||
|
||||
@alpha_routes_limit_by_type %{
|
||||
"trade_hubs" => 5,
|
||||
:trade_hubs => 5
|
||||
}
|
||||
@default_alpha_routes_limit 1
|
||||
@paid_routes_limit 15
|
||||
|
||||
def handle_server_event(
|
||||
%{
|
||||
event: :routes,
|
||||
@@ -43,6 +50,25 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
|
||||
}
|
||||
)
|
||||
|
||||
def handle_server_event(
|
||||
%{
|
||||
event: :routes_list_by,
|
||||
payload: {solar_system_id, %{routes: routes, systems_static_data: systems_static_data}}
|
||||
},
|
||||
socket
|
||||
),
|
||||
do:
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"routes_list_by",
|
||||
%{
|
||||
solar_system_id: solar_system_id,
|
||||
loading: false,
|
||||
routes: routes,
|
||||
systems_static_data: systems_static_data
|
||||
}
|
||||
)
|
||||
|
||||
def handle_server_event(event, socket),
|
||||
do: MapCoreEventHandler.handle_server_event(event, socket)
|
||||
|
||||
@@ -142,6 +168,41 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"get_routes_by",
|
||||
%{"system_id" => solar_system_id, "routes_settings" => routes_settings} = event,
|
||||
%{assigns: %{map_id: map_id, map_loaded?: true}} = socket
|
||||
) do
|
||||
routes_type = Map.get(event, "type", "blueLoot")
|
||||
security_type = Map.get(event, "securityType", "both")
|
||||
is_subscription_active? = Map.get(socket.assigns, :is_subscription_active?, false)
|
||||
routes_limit =
|
||||
if is_subscription_active? == true do
|
||||
@paid_routes_limit
|
||||
else
|
||||
Map.get(@alpha_routes_limit_by_type, routes_type, @default_alpha_routes_limit)
|
||||
end
|
||||
routes_settings =
|
||||
routes_settings
|
||||
|> get_routes_settings()
|
||||
|> Map.put(:security_type, security_type)
|
||||
|
||||
Task.async(fn ->
|
||||
{:ok, routes} =
|
||||
WandererApp.Map.RoutesBy.find(
|
||||
map_id,
|
||||
solar_system_id,
|
||||
routes_settings,
|
||||
routes_type,
|
||||
routes_limit
|
||||
)
|
||||
|
||||
{:routes_list_by, {solar_system_id, routes}}
|
||||
end)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"add_hub",
|
||||
%{"system_id" => solar_system_id} = _event,
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
require Logger
|
||||
|
||||
alias WandererAppWeb.{MapEventHandler, MapCoreEventHandler}
|
||||
alias WandererApp.Map.Server.SignaturesImpl
|
||||
alias WandererApp.Utils.EVEUtil
|
||||
|
||||
def handle_server_event(
|
||||
@@ -279,7 +280,8 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
linked_system_id: solar_system_target
|
||||
})
|
||||
|
||||
if is_nil(target_system.linked_sig_eve_id) do
|
||||
if is_nil(target_system.linked_sig_eve_id) or
|
||||
target_system.linked_sig_eve_id == signature_eve_id do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.update_system_linked_sig_eve_id(%{
|
||||
solar_system_id: solar_system_target,
|
||||
@@ -301,6 +303,37 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
nil
|
||||
end
|
||||
|
||||
signature_ship_size_type = EVEUtil.get_wh_size(signature.type)
|
||||
|
||||
# Back-link detection: if current signature yields no ship_size_type (e.g., K162),
|
||||
# look for a forward signature in the target system that links back to our source
|
||||
{signature_time_status, signature_ship_size_type} =
|
||||
if is_nil(signature_ship_size_type) do
|
||||
case SignaturesImpl.find_forward_signature(target_system.id, solar_system_source) do
|
||||
nil ->
|
||||
{signature_time_status, signature_ship_size_type}
|
||||
|
||||
forward_sig ->
|
||||
Logger.info(
|
||||
"[link_signature_to_system] Back-link detected: " <>
|
||||
"using forward sig type=#{forward_sig.type} from target system"
|
||||
)
|
||||
|
||||
forward_ship_size = EVEUtil.get_wh_size(forward_sig.type)
|
||||
|
||||
forward_time_status =
|
||||
if is_nil(signature_time_status) and not is_nil(forward_sig.custom_info) do
|
||||
forward_sig.custom_info |> Jason.decode!() |> Map.get("time_status")
|
||||
else
|
||||
signature_time_status
|
||||
end
|
||||
|
||||
{forward_time_status, forward_ship_size}
|
||||
end
|
||||
else
|
||||
{signature_time_status, signature_ship_size_type}
|
||||
end
|
||||
|
||||
if not is_nil(signature_time_status) do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.update_connection_time_status(%{
|
||||
@@ -310,8 +343,6 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
})
|
||||
end
|
||||
|
||||
signature_ship_size_type = EVEUtil.get_wh_size(signature.type)
|
||||
|
||||
if not is_nil(signature_ship_size_type) do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.update_connection_ship_size_type(%{
|
||||
|
||||
@@ -101,11 +101,13 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
|
||||
@map_routes_events [
|
||||
:routes,
|
||||
:user_routes
|
||||
:user_routes,
|
||||
:routes_list_by
|
||||
]
|
||||
|
||||
@map_routes_ui_events [
|
||||
"get_routes",
|
||||
"get_routes_by",
|
||||
"get_user_routes",
|
||||
"set_autopilot_waypoint",
|
||||
"add_hub",
|
||||
|
||||
@@ -126,7 +126,10 @@
|
||||
<li
|
||||
class={[
|
||||
"p-unselectable-text",
|
||||
classes("p-tabview-selected p-highlight": false)
|
||||
classes(
|
||||
"p-tabview-selected p-highlight":
|
||||
@active_subscription_tab == "top_donators"
|
||||
)
|
||||
]}
|
||||
role="presentation"
|
||||
data-pc-name=""
|
||||
@@ -140,12 +143,11 @@
|
||||
aria-selected="false"
|
||||
aria-disabled="false"
|
||||
data-pc-section="headeraction"
|
||||
phx-click="change_settings_tab"
|
||||
phx-value-tab="balance"
|
||||
phx-click="change_subscription_tab"
|
||||
phx-value-tab="top_donators"
|
||||
>
|
||||
<span class="p-tabview-title" data-pc-section="headertitle">
|
||||
<.icon name="hero-arrow-up-solid" class="w-4 h-4" /> Top Donators
|
||||
<span class="badge">coming soon</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -186,6 +188,19 @@
|
||||
(@user_permissions || %{}) |> Map.get(:delete_map, false) |> Kernel.not()
|
||||
}
|
||||
/>
|
||||
|
||||
<.live_component
|
||||
:if={
|
||||
@active_subscription_tab == "top_donators" &&
|
||||
not is_nil(assigns |> Map.get(:map_id))
|
||||
}
|
||||
module={WandererAppWeb.Maps.MapTopDonatorsComponent}
|
||||
id="map-top-donators-component"
|
||||
map_id={@map_id}
|
||||
notify_to={self()}
|
||||
event_name="top_donators_event"
|
||||
current_user={@current_user}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
defmodule WandererAppWeb.Maps.MapTopDonatorsComponent do
|
||||
use WandererAppWeb, :live_component
|
||||
use LiveViewEvents
|
||||
|
||||
require Logger
|
||||
|
||||
alias BetterNumber, as: Number
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok,
|
||||
assign(socket, top_donators: [], period: "all", image_base_url: "https://images.evetech.net")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(%{map_id: map_id} = assigns, socket) do
|
||||
socket = handle_info_or_assign(socket, assigns)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(map_id: map_id)
|
||||
|> load_top_donators()
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("change_period", %{"period" => period}, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(period: period)
|
||||
|> load_top_donators()
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp load_top_donators(%{assigns: %{map_id: map_id, period: period}} = socket) do
|
||||
after_date = period_to_date(period)
|
||||
|
||||
case WandererApp.Api.MapTransaction.top_donators(%{map_id: map_id, after: after_date}) do
|
||||
{:ok, donators} ->
|
||||
enriched = enrich_with_characters(donators)
|
||||
assign(socket, top_donators: enriched)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Failed to load top donators: #{inspect(reason)}")
|
||||
assign(socket, top_donators: [])
|
||||
end
|
||||
end
|
||||
|
||||
defp period_to_date("all"), do: nil
|
||||
defp period_to_date("30d"), do: DateTime.utc_now() |> DateTime.add(-30, :day)
|
||||
defp period_to_date("7d"), do: DateTime.utc_now() |> DateTime.add(-7, :day)
|
||||
defp period_to_date(_), do: nil
|
||||
|
||||
defp enrich_with_characters(donators) do
|
||||
donators
|
||||
|> Enum.map(fn %{user_id: user_id, total_amount: total_amount} ->
|
||||
case WandererApp.Api.Character.active_by_user(%{user_id: user_id}) do
|
||||
{:ok, [character | _]} ->
|
||||
%{
|
||||
character_name: character.name,
|
||||
eve_id: character.eve_id,
|
||||
corporation_name: character.corporation_name,
|
||||
corporation_ticker: character.corporation_ticker,
|
||||
total_amount: total_amount
|
||||
}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="map-top-donators">
|
||||
<div class="flex gap-2 mb-4">
|
||||
<button
|
||||
type="button"
|
||||
class={[
|
||||
"btn btn-sm",
|
||||
if(@period == "all", do: "btn-primary", else: "btn-ghost")
|
||||
]}
|
||||
phx-click="change_period"
|
||||
phx-value-period="all"
|
||||
phx-target={@myself}
|
||||
>
|
||||
All Time
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={[
|
||||
"btn btn-sm",
|
||||
if(@period == "30d", do: "btn-primary", else: "btn-ghost")
|
||||
]}
|
||||
phx-click="change_period"
|
||||
phx-value-period="30d"
|
||||
phx-target={@myself}
|
||||
>
|
||||
30 Days
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={[
|
||||
"btn btn-sm",
|
||||
if(@period == "7d", do: "btn-primary", else: "btn-ghost")
|
||||
]}
|
||||
phx-click="change_period"
|
||||
phx-value-period="7d"
|
||||
phx-target={@myself}
|
||||
>
|
||||
7 Days
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div :if={@top_donators == []} class="text-center text-gray-400 py-8">
|
||||
No donations found for this period.
|
||||
</div>
|
||||
|
||||
<div :if={@top_donators != []} class="space-y-2">
|
||||
<div
|
||||
:for={{donator, index} <- Enum.with_index(@top_donators)}
|
||||
class="flex items-center gap-3 p-2 rounded-lg bg-base-200/50"
|
||||
>
|
||||
<span class="text-lg font-bold text-gray-400 w-6 text-right">
|
||||
{index + 1}
|
||||
</span>
|
||||
<img
|
||||
src={"#{@image_base_url}/characters/#{donator.eve_id}/portrait?size=64"}
|
||||
class="w-10 h-10 rounded-full"
|
||||
alt={donator.character_name}
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium text-white truncate">
|
||||
{donator.character_name}
|
||||
</div>
|
||||
<div :if={donator.corporation_name} class="text-xs text-gray-400 truncate">
|
||||
[{donator.corporation_ticker}] {donator.corporation_name}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right font-mono text-sm text-green-400">
|
||||
ISK {donator.total_amount |> Number.to_human(units: ["", "K", "M", "B", "T", "P"])}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
@@ -299,6 +299,8 @@ defmodule WandererAppWeb.Router do
|
||||
resources "/structures", MapSystemStructureAPIController, except: [:new, :edit]
|
||||
get "/structure-timers", MapSystemStructureAPIController, :structure_timers
|
||||
resources "/signatures", MapSystemSignatureAPIController, except: [:new, :edit]
|
||||
post "/signatures/:id/link", MapSystemSignatureAPIController, :link
|
||||
delete "/signatures/:id/link", MapSystemSignatureAPIController, :unlink
|
||||
get "/user-characters", MapAPIController, :show_user_characters
|
||||
get "/tracked-characters", MapAPIController, :show_tracked_characters
|
||||
end
|
||||
@@ -341,6 +343,11 @@ defmodule WandererAppWeb.Router do
|
||||
get "/system-static-info", CommonAPIController, :show_system_static
|
||||
end
|
||||
|
||||
scope "/route", WandererAppWeb do
|
||||
pipe_through [:api]
|
||||
post "/findClosest", RouteBuilderController, :find_closest
|
||||
end
|
||||
|
||||
scope "/api" do
|
||||
pipe_through [:api_spec]
|
||||
get "/openapi", OpenApiSpex.Plug.RenderSpec, :show
|
||||
@@ -506,6 +513,7 @@ defmodule WandererAppWeb.Router do
|
||||
live("/maps", AdminMapsLive, :index)
|
||||
live("/maps/:id/edit", AdminMapsLive, :edit)
|
||||
live("/maps/:id/acls", AdminMapsLive, :view_acls)
|
||||
live("/characters", AdminCharactersLive, :index)
|
||||
end
|
||||
|
||||
error_tracker_dashboard("/errors",
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.93.0"
|
||||
@version "1.95.0"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
14
mix.lock
14
mix.lock
@@ -11,7 +11,7 @@
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"},
|
||||
"castore": {:hex, :castore, "1.0.16", "8a4f9a7c8b81cda88231a08fe69e3254f16833053b23fa63274b05cbc61d2a1e", [:mix], [], "hexpm", "33689203a0eaaf02fcd0e86eadfbcf1bd636100455350592e7e2628564022aaf"},
|
||||
"certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
|
||||
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
|
||||
"cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"conv_case": {:hex, :conv_case, "0.2.3", "c1455c27d3c1ffcdd5f17f1e91f40b8a0bc0a337805a6e8302f441af17118ed8", [:mix], [], "hexpm", "88f29a3d97d1742f9865f7e394ed3da011abb7c5e8cc104e676fdef6270d4b4a"},
|
||||
@@ -43,18 +43,18 @@
|
||||
"ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"},
|
||||
"ex_rated": {:hex, :ex_rated, "2.1.0", "d40e6fe35097b10222df2db7bb5dd801d57211bac65f29063de5f201c2a6aebc", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm", "936c155337253ed6474f06d941999dd3a9cf0fe767ec99a59f2d2989dc2cc13f"},
|
||||
"excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"},
|
||||
"expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"},
|
||||
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
|
||||
"exsync": {:hex, :exsync, "0.4.1", "0a14fe4bfcb80a509d8a0856be3dd070fffe619b9ba90fec13c58b316c176594", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "cefb22aa805ec97ffc5b75a4e1dc54bcaf781e8b32564bf74abbe5803d1b5178"},
|
||||
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
||||
"finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"},
|
||||
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
|
||||
"fresh": {:hex, :fresh, "0.4.4", "9d67a1d97112e70f4dfabd63b40e4b182ef64dfa84a2d9ee175eb4e34591e9f7", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.5", [hex: :mint, repo: "hexpm", optional: false]}, {:mint_web_socket, "~> 1.0", [hex: :mint_web_socket, repo: "hexpm", optional: false]}], "hexpm", "ba21d3fa0aa77bf18ca397e4c851de7432bb3f9c170a1645a16e09e4bba54315"},
|
||||
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
|
||||
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
|
||||
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
|
||||
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
|
||||
"git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"},
|
||||
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
|
||||
"hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"},
|
||||
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
|
||||
"heroicons": {:hex, :heroicons, "0.5.5", "c2bcb05a90f010df246a5a2a2b54cac15483b5de137b2ef0bead77fcdf06e21a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "2f4bf929440fecd5191ba9f40e5009b0f75dc993d765c0e4d068fcb7026d6da1"},
|
||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
@@ -74,7 +74,7 @@
|
||||
"merkle_map": {:hex, :merkle_map, "0.2.1", "01a88c87a6b9fb594c67c17ebaf047ee55ffa34e74297aa583ed87148006c4c8", [:mix], [], "hexpm", "fed4d143a5c8166eee4fa2b49564f3c4eace9cb252f0a82c1613bba905b2d04d"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
|
||||
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
|
||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||
"mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"},
|
||||
@@ -138,10 +138,10 @@
|
||||
"tesla": {:hex, :tesla, "1.11.0", "81b2b10213dddb27105ec6102d9eb0cc93d7097a918a0b1594f2dfd1a4601190", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b83ab5d4c2d202e1ea2b7e17a49f788d49a699513d7c4f08f2aef2c281be69db"},
|
||||
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.3.11", "b68f3e91f74d564ae20b70d981bbf7097dde084343c14ae8a33e5b5fbb3d6f37", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "555c18c62027f45d9c80df389c3d01d86ba11014652c00be26e33b1b64e98d29"},
|
||||
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
|
||||
"timex": {:hex, :timex, "3.7.13", "0688ce11950f5b65e154e42b47bf67b15d3bc0e0c3def62199991b8a8079a1e2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "09588e0522669328e973b8b4fd8741246321b3f0d32735b589f78b136e6d4c54"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"},
|
||||
"ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
|
||||
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
|
||||
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},
|
||||
"version_tasks": {:hex, :version_tasks, "0.12.0", "df384f454369f5f922a541cdc21da2db643c7424c03994986dab2b1702a5b724", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}], "hexpm", "c85e0ec9ad498795609ad849b6dbc668876cecb993fce1f4073016a5b87ee430"},
|
||||
|
||||
947
priv/repo/data/route_by_systems/blueloot.json
Normal file
947
priv/repo/data/route_by_systems/blueloot.json
Normal file
@@ -0,0 +1,947 @@
|
||||
{
|
||||
"type_id": 30747,
|
||||
"generated_at": "2026-01-30T13:53:23.834Z",
|
||||
"bands": [
|
||||
"high",
|
||||
"low"
|
||||
],
|
||||
"system_ids_by_band": {
|
||||
"high": [
|
||||
30000055,
|
||||
30000053,
|
||||
30000159,
|
||||
30000160,
|
||||
30000187,
|
||||
30000133,
|
||||
30000181,
|
||||
30001359,
|
||||
30001391,
|
||||
30001395,
|
||||
30001397,
|
||||
30001676,
|
||||
30001679,
|
||||
30001677,
|
||||
30002558,
|
||||
30002569,
|
||||
30002571,
|
||||
30002568,
|
||||
30002572,
|
||||
30002771,
|
||||
30002800,
|
||||
30002815,
|
||||
30002816,
|
||||
30002988,
|
||||
30002992,
|
||||
30002993,
|
||||
30003017,
|
||||
30003018,
|
||||
30003024,
|
||||
30003025,
|
||||
30003029,
|
||||
30003030,
|
||||
30003048,
|
||||
30003053,
|
||||
30003055,
|
||||
30003389,
|
||||
30003394,
|
||||
30003402,
|
||||
30003409,
|
||||
30003412,
|
||||
30003447,
|
||||
30003449,
|
||||
30003469,
|
||||
30003404,
|
||||
30003413,
|
||||
30002191,
|
||||
30002193,
|
||||
30002252,
|
||||
30003553,
|
||||
30003554,
|
||||
30003555,
|
||||
30002190,
|
||||
30002187,
|
||||
30004077,
|
||||
30004078,
|
||||
30004079,
|
||||
30004083,
|
||||
30004084,
|
||||
30004111,
|
||||
30004112,
|
||||
30004114,
|
||||
30005009,
|
||||
30005011,
|
||||
30005017,
|
||||
30005018,
|
||||
30005039,
|
||||
30005040,
|
||||
30005043,
|
||||
30005052,
|
||||
30005054,
|
||||
30005204,
|
||||
30005199
|
||||
],
|
||||
"low": [
|
||||
30000017,
|
||||
30000040,
|
||||
30000041,
|
||||
30000072,
|
||||
30000074,
|
||||
30000162,
|
||||
30000163,
|
||||
30000164,
|
||||
30000196,
|
||||
30000197,
|
||||
30001390,
|
||||
30001361,
|
||||
30002414,
|
||||
30002415,
|
||||
30002418,
|
||||
30002559,
|
||||
30002560,
|
||||
30002769,
|
||||
30002975,
|
||||
30002977,
|
||||
30002980,
|
||||
30002059,
|
||||
30002065,
|
||||
30003467,
|
||||
30002058,
|
||||
30002067,
|
||||
30003556,
|
||||
30004239,
|
||||
30004240,
|
||||
30004241,
|
||||
30004288,
|
||||
30004291,
|
||||
30004296,
|
||||
30005010,
|
||||
30005020,
|
||||
30005030,
|
||||
30005031,
|
||||
30005034,
|
||||
30005035,
|
||||
30005275,
|
||||
30005276
|
||||
]
|
||||
},
|
||||
"system_stations": {
|
||||
"30000017": [
|
||||
{
|
||||
"station_id": 60014071,
|
||||
"name": "Futzchag IX - Moon 9 - Thukker Mix Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60014074,
|
||||
"name": "Futzchag II - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30000040": [
|
||||
{
|
||||
"station_id": 60014095,
|
||||
"name": "Uzistoon VII - Moon 2 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30000041": [
|
||||
{
|
||||
"station_id": 60014098,
|
||||
"name": "Bairshir IV - Moon 11 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30000053": [
|
||||
{
|
||||
"station_id": 60014137,
|
||||
"name": "Ibaria III - Thukker Mix Warehouse"
|
||||
}
|
||||
],
|
||||
"30000055": [
|
||||
{
|
||||
"station_id": 60014140,
|
||||
"name": "Zemalu IX - Moon 2 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30000072": [
|
||||
{
|
||||
"station_id": 60014068,
|
||||
"name": "Nakah I - Moon 1 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30000074": [
|
||||
{
|
||||
"station_id": 60014065,
|
||||
"name": "Hasateem VI - Moon 12 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30000133": [
|
||||
{
|
||||
"station_id": 60001810,
|
||||
"name": "Hirtamon VII - Moon 6 - Zainou Biotech Production"
|
||||
},
|
||||
{
|
||||
"station_id": 60001807,
|
||||
"name": "Hirtamon VII - Moon 5 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30000159": [
|
||||
{
|
||||
"station_id": 60010195,
|
||||
"name": "Ikami X - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30000160": [
|
||||
{
|
||||
"station_id": 60010192,
|
||||
"name": "Reisen VI - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30000162": [
|
||||
{
|
||||
"station_id": 60001801,
|
||||
"name": "Maila IV - Zainou Biotech Production"
|
||||
},
|
||||
{
|
||||
"station_id": 60001804,
|
||||
"name": "Maila VI - Moon 1 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30000163": [
|
||||
{
|
||||
"station_id": 60010198,
|
||||
"name": "Akora IX - Moon 19 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30000164": [
|
||||
{
|
||||
"station_id": 60010201,
|
||||
"name": "Messoya VIII - Moon 6 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30000181": [
|
||||
{
|
||||
"station_id": 60001783,
|
||||
"name": "Korsiki III - Moon 15 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30000187": [
|
||||
{
|
||||
"station_id": 60001786,
|
||||
"name": "Wuos VI - Zainou Biotech Research Center"
|
||||
}
|
||||
],
|
||||
"30000196": [
|
||||
{
|
||||
"station_id": 60001798,
|
||||
"name": "Otosela V - Moon 13 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30000197": [
|
||||
{
|
||||
"station_id": 60001795,
|
||||
"name": "Uemon VIII - Moon 10 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30001359": [
|
||||
{
|
||||
"station_id": 60001780,
|
||||
"name": "Semiki IV - Zainou Biohazard Containment Facility"
|
||||
}
|
||||
],
|
||||
"30001361": [
|
||||
{
|
||||
"station_id": 60001777,
|
||||
"name": "Aurohunen III - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30001390": [
|
||||
{
|
||||
"station_id": 60001816,
|
||||
"name": "Pakkonen IV - Moon 11 - Zainou Biotech Research Center"
|
||||
}
|
||||
],
|
||||
"30001391": [
|
||||
{
|
||||
"station_id": 60001813,
|
||||
"name": "Piekura VIII - Moon 15 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30001395": [
|
||||
{
|
||||
"station_id": 60001768,
|
||||
"name": "Ylandoki II - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30001397": [
|
||||
{
|
||||
"station_id": 60001765,
|
||||
"name": "Isseras IV - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30001676": [
|
||||
{
|
||||
"station_id": 60008527,
|
||||
"name": "Mimen X - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008521,
|
||||
"name": "Mimen VIII - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30001677": [
|
||||
{
|
||||
"station_id": 60008518,
|
||||
"name": "Thashkarai VII - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30001679": [
|
||||
{
|
||||
"station_id": 60008524,
|
||||
"name": "Unkah VI - Moon 7 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002058": [
|
||||
{
|
||||
"station_id": 60014134,
|
||||
"name": "Ardar IV - Moon 2 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30002059": [
|
||||
{
|
||||
"station_id": 60014131,
|
||||
"name": "Auner VIII - Moon 10 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30002065": [
|
||||
{
|
||||
"station_id": 60014143,
|
||||
"name": "Lasleinur IV - Moon 16 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30002067": [
|
||||
{
|
||||
"station_id": 60014146,
|
||||
"name": "Brin V - Moon 7 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30002187": [
|
||||
{
|
||||
"station_id": 60008494,
|
||||
"name": "Amarr VIII (Oris) - Emperor Family Academy"
|
||||
}
|
||||
],
|
||||
"30002190": [
|
||||
{
|
||||
"station_id": 60008500,
|
||||
"name": "Mabnen IV - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002191": [
|
||||
{
|
||||
"station_id": 60008503,
|
||||
"name": "Toshabia VI - Moon 6 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002193": [
|
||||
{
|
||||
"station_id": 60008497,
|
||||
"name": "Kehour VIII - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002252": [
|
||||
{
|
||||
"station_id": 60010240,
|
||||
"name": "Bika III - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010243,
|
||||
"name": "Bika VIII - Moon 11 - CreoDron Warehouse"
|
||||
},
|
||||
{
|
||||
"station_id": 60010246,
|
||||
"name": "Bika V - Moon 1 - CreoDron Warehouse"
|
||||
},
|
||||
{
|
||||
"station_id": 60010249,
|
||||
"name": "Bika VII - Moon 1 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002414": [
|
||||
{
|
||||
"station_id": 60010183,
|
||||
"name": "Klingt IX - Moon 11 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002415": [
|
||||
{
|
||||
"station_id": 60010180,
|
||||
"name": "Weld IV - Moon 4 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002418": [
|
||||
{
|
||||
"station_id": 60010186,
|
||||
"name": "Hegfunden VIII - Moon 14 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010189,
|
||||
"name": "Hegfunden VIII - Moon 26 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002558": [
|
||||
{
|
||||
"station_id": 60010168,
|
||||
"name": "Endrulf VIII - Moon 1 - CreoDron Warehouse"
|
||||
},
|
||||
{
|
||||
"station_id": 60010171,
|
||||
"name": "Endrulf IV - Moon 1 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30002559": [
|
||||
{
|
||||
"station_id": 60010174,
|
||||
"name": "Ingunn V - Moon 21 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002560": [
|
||||
{
|
||||
"station_id": 60010177,
|
||||
"name": "Gultratren V - Moon 22 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002568": [
|
||||
{
|
||||
"station_id": 60010297,
|
||||
"name": "Onga X - Moon 11 - CreoDron Warehouse"
|
||||
},
|
||||
{
|
||||
"station_id": 60010291,
|
||||
"name": "Onga VI - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002569": [
|
||||
{
|
||||
"station_id": 60010294,
|
||||
"name": "Osaumuni VII - Moon 16 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002571": [
|
||||
{
|
||||
"station_id": 60010288,
|
||||
"name": "Oremmulf IX - Moon 6 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60014110,
|
||||
"name": "Oremmulf V - Moon 20 - Thukker Mix Warehouse"
|
||||
}
|
||||
],
|
||||
"30002572": [
|
||||
{
|
||||
"station_id": 60014107,
|
||||
"name": "Hurjafren VII - Moon 25 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30002769": [
|
||||
{
|
||||
"station_id": 60001819,
|
||||
"name": "Enderailen IV - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30002771": [
|
||||
{
|
||||
"station_id": 60001822,
|
||||
"name": "Kulelen V - Moon 8 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30002800": [
|
||||
{
|
||||
"station_id": 60001792,
|
||||
"name": "Haatomo VI - Moon 6 - Zainou Biotech Production"
|
||||
},
|
||||
{
|
||||
"station_id": 60001789,
|
||||
"name": "Haatomo VII - Moon 7 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30002815": [
|
||||
{
|
||||
"station_id": 60001774,
|
||||
"name": "Isenairos V - Moon 7 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30002816": [
|
||||
{
|
||||
"station_id": 60001771,
|
||||
"name": "Saila VIII - Moon 16 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30002975": [
|
||||
{
|
||||
"station_id": 60008551,
|
||||
"name": "Roushzar II - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002977": [
|
||||
{
|
||||
"station_id": 60008542,
|
||||
"name": "Arayar VII - Moon 16 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002980": [
|
||||
{
|
||||
"station_id": 60008545,
|
||||
"name": "Sosan II - Emperor Family Academy"
|
||||
},
|
||||
{
|
||||
"station_id": 60008548,
|
||||
"name": "Sosan III - Moon 4 - Emperor Family Academy"
|
||||
}
|
||||
],
|
||||
"30002988": [
|
||||
{
|
||||
"station_id": 60008536,
|
||||
"name": "Nakatre II - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002992": [
|
||||
{
|
||||
"station_id": 60008530,
|
||||
"name": "Akes V - Moon 2 - Emperor Family Academy"
|
||||
}
|
||||
],
|
||||
"30002993": [
|
||||
{
|
||||
"station_id": 60008533,
|
||||
"name": "Riavayed IX - Moon 2 - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008539,
|
||||
"name": "Riavayed II - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30003017": [
|
||||
{
|
||||
"station_id": 60010258,
|
||||
"name": "Harerget VIII - Moon 1 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010252,
|
||||
"name": "Harerget V - Moon 1 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003018": [
|
||||
{
|
||||
"station_id": 60010261,
|
||||
"name": "Azer III - Moon 6 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010255,
|
||||
"name": "Azer VI - Moon 1 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30003024": [
|
||||
{
|
||||
"station_id": 60010219,
|
||||
"name": "Marosier IV - Moon 2 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003025": [
|
||||
{
|
||||
"station_id": 60010216,
|
||||
"name": "Lirsautton I - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003029": [
|
||||
{
|
||||
"station_id": 60010222,
|
||||
"name": "Jaschercis IV - Moon 2 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003030": [
|
||||
{
|
||||
"station_id": 60010225,
|
||||
"name": "Ardallabier III - Moon 14 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003048": [
|
||||
{
|
||||
"station_id": 60010120,
|
||||
"name": "Carirgnottin VIII - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003053": [
|
||||
{
|
||||
"station_id": 60010126,
|
||||
"name": "Avele V - Moon 11 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30003055": [
|
||||
{
|
||||
"station_id": 60010129,
|
||||
"name": "Aydoteaux II - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010123,
|
||||
"name": "Aydoteaux VIII - Moon 12 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30003389": [
|
||||
{
|
||||
"station_id": 60014077,
|
||||
"name": "Altrinur XI - Moon 3 - Thukker Mix Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60014080,
|
||||
"name": "Altrinur XII - Moon 2 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003394": [
|
||||
{
|
||||
"station_id": 60014125,
|
||||
"name": "Freatlidur V - Moon 4 - Thukker Mix Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60014128,
|
||||
"name": "Freatlidur VII - Moon 3 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003402": [
|
||||
{
|
||||
"station_id": 60014083,
|
||||
"name": "Totkubad III - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003404": [
|
||||
{
|
||||
"station_id": 60014086,
|
||||
"name": "Agtver VI - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003409": [
|
||||
{
|
||||
"station_id": 60014101,
|
||||
"name": "Leurtmar III - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003412": [
|
||||
{
|
||||
"station_id": 60005719,
|
||||
"name": "Elgoi VI - Moon 1 - Eifyr and Co. Biotech Production"
|
||||
},
|
||||
{
|
||||
"station_id": 60014104,
|
||||
"name": "Elgoi VIII - Moon 19 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003413": [
|
||||
{
|
||||
"station_id": 60005722,
|
||||
"name": "Eram V - Moon 2 - Eifyr and Co. Biotech Production"
|
||||
}
|
||||
],
|
||||
"30003447": [
|
||||
{
|
||||
"station_id": 60010144,
|
||||
"name": "Josekorn IV - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010150,
|
||||
"name": "Josekorn VIII - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010153,
|
||||
"name": "Josekorn X - Moon 1 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003449": [
|
||||
{
|
||||
"station_id": 60010147,
|
||||
"name": "Hakeri XI - Moon 5 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003467": [
|
||||
{
|
||||
"station_id": 60014116,
|
||||
"name": "Frulegur IX - Moon 5 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003469": [
|
||||
{
|
||||
"station_id": 60014113,
|
||||
"name": "Hodrold VII - Moon 8 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003553": [
|
||||
{
|
||||
"station_id": 60010141,
|
||||
"name": "Warouh VII - Moon 1 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003554": [
|
||||
{
|
||||
"station_id": 60010132,
|
||||
"name": "Jambu VI - Moon 3 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003555": [
|
||||
{
|
||||
"station_id": 60010138,
|
||||
"name": "Bittanshal VII - Moon 9 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003556": [
|
||||
{
|
||||
"station_id": 60010135,
|
||||
"name": "Arton II - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30004077": [
|
||||
{
|
||||
"station_id": 60008509,
|
||||
"name": "Hiroudeh VIII - Moon 3 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30004078": [
|
||||
{
|
||||
"station_id": 60008515,
|
||||
"name": "Dresi I - Moon 18 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30004079": [
|
||||
{
|
||||
"station_id": 60008506,
|
||||
"name": "Aphend VII - Moon 4 - Emperor Family Academy"
|
||||
}
|
||||
],
|
||||
"30004083": [
|
||||
{
|
||||
"station_id": 60008512,
|
||||
"name": "Gensela X - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30004084": [
|
||||
{
|
||||
"station_id": 60010228,
|
||||
"name": "Ghesis V - Moon 2 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010231,
|
||||
"name": "Ghesis V - Moon 9 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010234,
|
||||
"name": "Ghesis V - Moon 13 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010237,
|
||||
"name": "Ghesis V - Moon 3 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30004111": [
|
||||
{
|
||||
"station_id": 60010279,
|
||||
"name": "Yarebap VII - Moon 8 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30004112": [
|
||||
{
|
||||
"station_id": 60010276,
|
||||
"name": "Mandoo III - Moon 11 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010282,
|
||||
"name": "Mandoo III - Moon 5 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30004114": [
|
||||
{
|
||||
"station_id": 60010285,
|
||||
"name": "Peyiri XI - Moon 21 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30004239": [
|
||||
{
|
||||
"station_id": 60008566,
|
||||
"name": "Kamih VII - Moon 4 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30004240": [
|
||||
{
|
||||
"station_id": 60008569,
|
||||
"name": "Hier IV - Moon 3 - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008572,
|
||||
"name": "Hier VII - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30004241": [
|
||||
{
|
||||
"station_id": 60008575,
|
||||
"name": "Jasson I - Moon 4 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30004288": [
|
||||
{
|
||||
"station_id": 60010267,
|
||||
"name": "Ghekon V - Moon 5 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010273,
|
||||
"name": "Ghekon II - Moon 2 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30004291": [
|
||||
{
|
||||
"station_id": 60010270,
|
||||
"name": "Anohel VI - Moon 14 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30004296": [
|
||||
{
|
||||
"station_id": 60010264,
|
||||
"name": "Bapraya IV - Moon 1 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30005009": [
|
||||
{
|
||||
"station_id": 60010159,
|
||||
"name": "Allebin VIII - Moon 4 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30005010": [
|
||||
{
|
||||
"station_id": 60010162,
|
||||
"name": "Atlulle VIII - Moon 6 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010165,
|
||||
"name": "Atlulle III - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30005011": [
|
||||
{
|
||||
"station_id": 60010156,
|
||||
"name": "Droselory VI - Moon 17 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30005017": [
|
||||
{
|
||||
"station_id": 60010207,
|
||||
"name": "Yona VI - Moon 5 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30005018": [
|
||||
{
|
||||
"station_id": 60010210,
|
||||
"name": "Noghere VII - Moon 15 - CreoDron Warehouse"
|
||||
},
|
||||
{
|
||||
"station_id": 60010213,
|
||||
"name": "Noghere VIII - Moon 18 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30005020": [
|
||||
{
|
||||
"station_id": 60010204,
|
||||
"name": "Seyllin VIII - Moon 14 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30005030": [
|
||||
{
|
||||
"station_id": 60008578,
|
||||
"name": "Fensi V - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005031": [
|
||||
{
|
||||
"station_id": 60008584,
|
||||
"name": "Nebian VIII - Moon 4 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005034": [
|
||||
{
|
||||
"station_id": 60008581,
|
||||
"name": "Bridi II - Moon 1 - Emperor Family Academy"
|
||||
}
|
||||
],
|
||||
"30005035": [
|
||||
{
|
||||
"station_id": 60008587,
|
||||
"name": "Ami XI - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005039": [
|
||||
{
|
||||
"station_id": 60008611,
|
||||
"name": "Leva II - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008602,
|
||||
"name": "Leva XI - Moon 8 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005040": [
|
||||
{
|
||||
"station_id": 60008608,
|
||||
"name": "Nishah VII - Moon 5 - Emperor Family Treasury"
|
||||
}
|
||||
],
|
||||
"30005043": [
|
||||
{
|
||||
"station_id": 60008605,
|
||||
"name": "Nakregde VII - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005052": [
|
||||
{
|
||||
"station_id": 60008554,
|
||||
"name": "Soumi V - Moon 4 - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008557,
|
||||
"name": "Soumi I - Moon 1 - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008563,
|
||||
"name": "Soumi VII - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005054": [
|
||||
{
|
||||
"station_id": 60008560,
|
||||
"name": "Nare VI - Moon 16 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005199": [
|
||||
{
|
||||
"station_id": 60012739,
|
||||
"name": "Tar III - Secure Commerce Commission Depository"
|
||||
}
|
||||
],
|
||||
"30005204": [
|
||||
{
|
||||
"station_id": 60012736,
|
||||
"name": "Yulai III - Moon 1 - Secure Commerce Commission Depository"
|
||||
}
|
||||
],
|
||||
"30005275": [
|
||||
{
|
||||
"station_id": 60008596,
|
||||
"name": "Azedi III - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005276": [
|
||||
{
|
||||
"station_id": 60008599,
|
||||
"name": "Sharza VII - Moon 3 - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008590,
|
||||
"name": "Sharza VII - Moon 5 - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008593,
|
||||
"name": "Sharza VI - Moon 4 - Emperor Family Bureau"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
839
priv/repo/data/route_by_systems/redloot.json
Normal file
839
priv/repo/data/route_by_systems/redloot.json
Normal file
@@ -0,0 +1,839 @@
|
||||
{
|
||||
"type_id": 89219,
|
||||
"generated_at": "2026-01-30T13:54:13.047Z",
|
||||
"bands": [
|
||||
"high",
|
||||
"low"
|
||||
],
|
||||
"system_ids_by_band": {
|
||||
"high": [
|
||||
30000009,
|
||||
30000010,
|
||||
30000024,
|
||||
30000025,
|
||||
30000030,
|
||||
30000051,
|
||||
30000052,
|
||||
30000084,
|
||||
30000087,
|
||||
30000201,
|
||||
30000202,
|
||||
30001380,
|
||||
30001384,
|
||||
30001644,
|
||||
30001646,
|
||||
30001669,
|
||||
30001671,
|
||||
30001674,
|
||||
30001689,
|
||||
30001690,
|
||||
30001693,
|
||||
30002724,
|
||||
30002762,
|
||||
30002763,
|
||||
30002764,
|
||||
30002766,
|
||||
30003058,
|
||||
30003374,
|
||||
30003375,
|
||||
30003376,
|
||||
30003378,
|
||||
30003428,
|
||||
30003429,
|
||||
30003430,
|
||||
30003431,
|
||||
30003432,
|
||||
30002242,
|
||||
30002259,
|
||||
30002260,
|
||||
30002262,
|
||||
30003859,
|
||||
30003860,
|
||||
30003861,
|
||||
30003862,
|
||||
30004248,
|
||||
30004249,
|
||||
30004250,
|
||||
30004251,
|
||||
30004253,
|
||||
30005069,
|
||||
30005078,
|
||||
30005198,
|
||||
30005199,
|
||||
30005200,
|
||||
30005204,
|
||||
30005205,
|
||||
30005206,
|
||||
30005315,
|
||||
30005319,
|
||||
30005322,
|
||||
30005323
|
||||
],
|
||||
"low": [
|
||||
30000012,
|
||||
30000014,
|
||||
30000015,
|
||||
30000205,
|
||||
30001385,
|
||||
30002402,
|
||||
30002404,
|
||||
30002406,
|
||||
30002407,
|
||||
30002414,
|
||||
30002419,
|
||||
30002420,
|
||||
30002725,
|
||||
30002726,
|
||||
30002728,
|
||||
30002730,
|
||||
30003057,
|
||||
30003059,
|
||||
30003061,
|
||||
30002060,
|
||||
30002062,
|
||||
30002065,
|
||||
30002246,
|
||||
30002249,
|
||||
30003818,
|
||||
30003819,
|
||||
30004280,
|
||||
30004281,
|
||||
30004284,
|
||||
30005079,
|
||||
30005080,
|
||||
30005328
|
||||
]
|
||||
},
|
||||
"system_stations": {
|
||||
"30000009": [
|
||||
{
|
||||
"station_id": 60012295,
|
||||
"name": "Sooma X - CONCORD Academy"
|
||||
}
|
||||
],
|
||||
"30000010": [
|
||||
{
|
||||
"station_id": 60012304,
|
||||
"name": "Chidah V - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012307,
|
||||
"name": "Chidah VIII - Moon 17 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000012": [
|
||||
{
|
||||
"station_id": 60012292,
|
||||
"name": "Asabona IX - Moon 5 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30000014": [
|
||||
{
|
||||
"station_id": 60012298,
|
||||
"name": "Shamahi IX - Moon 12 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30000015": [
|
||||
{
|
||||
"station_id": 60012301,
|
||||
"name": "Sendaya V - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30000024": [
|
||||
{
|
||||
"station_id": 60013027,
|
||||
"name": "Kiereend VII - Moon 3 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000025": [
|
||||
{
|
||||
"station_id": 60013030,
|
||||
"name": "Rashy VI - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000030": [
|
||||
{
|
||||
"station_id": 60013024,
|
||||
"name": "Kasrasi IX - Moon 7 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000051": [
|
||||
{
|
||||
"station_id": 60012949,
|
||||
"name": "Juddi VII - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30000052": [
|
||||
{
|
||||
"station_id": 60012943,
|
||||
"name": "Maspah V - Moon 6 - DED Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012946,
|
||||
"name": "Maspah IV - Moon 7 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000084": [
|
||||
{
|
||||
"station_id": 60013012,
|
||||
"name": "Asghatil IX - Moon 3 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000087": [
|
||||
{
|
||||
"station_id": 60013006,
|
||||
"name": "Gelhan V - Moon 10 - DED Logistic Support"
|
||||
},
|
||||
{
|
||||
"station_id": 60013009,
|
||||
"name": "Gelhan V - Moon 1 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000201": [
|
||||
{
|
||||
"station_id": 60012313,
|
||||
"name": "Uchoshi I - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012322,
|
||||
"name": "Uchoshi IX - Moon 2 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000202": [
|
||||
{
|
||||
"station_id": 60012325,
|
||||
"name": "Mastakomon IX - Moon 2 - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60013042,
|
||||
"name": "Mastakomon IX - Moon 3 - DED Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012310,
|
||||
"name": "Mastakomon V - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012316,
|
||||
"name": "Mastakomon IX - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012319,
|
||||
"name": "Mastakomon VIII - Moon 1 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60013048,
|
||||
"name": "Mastakomon XI - Moon 2 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000205": [
|
||||
{
|
||||
"station_id": 60013045,
|
||||
"name": "Obe VI - Moon 2 - DED Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30001380": [
|
||||
{
|
||||
"station_id": 60012328,
|
||||
"name": "Vellaine V - Moon 2 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30001384": [
|
||||
{
|
||||
"station_id": 60012331,
|
||||
"name": "Autaris VIII - Moon 5 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012340,
|
||||
"name": "Autaris IV - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012343,
|
||||
"name": "Autaris I - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30001385": [
|
||||
{
|
||||
"station_id": 60012334,
|
||||
"name": "Jan VI - Moon 21 - CONCORD Academy"
|
||||
}
|
||||
],
|
||||
"30001644": [
|
||||
{
|
||||
"station_id": 60013033,
|
||||
"name": "Tividu IV - Moon 10 - DED Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60013036,
|
||||
"name": "Tividu IV - Moon 3 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30001646": [
|
||||
{
|
||||
"station_id": 60013039,
|
||||
"name": "Goram VII - Moon 4 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30001669": [
|
||||
{
|
||||
"station_id": 60012982,
|
||||
"name": "Pimebeka VII - Moon 13 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30001671": [
|
||||
{
|
||||
"station_id": 60012985,
|
||||
"name": "Tash-Murkon Prime III - Moon 1 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30001674": [
|
||||
{
|
||||
"station_id": 60012979,
|
||||
"name": "Hilaban II - Moon 5 - DED Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30001689": [
|
||||
{
|
||||
"station_id": 60012964,
|
||||
"name": "Asesamy VI - Moon 8 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30001690": [
|
||||
{
|
||||
"station_id": 60012967,
|
||||
"name": "Hostni VII - Moon 18 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30001693": [
|
||||
{
|
||||
"station_id": 60012961,
|
||||
"name": "Perdan VI - Moon 16 - DED Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30002060": [
|
||||
{
|
||||
"station_id": 60012970,
|
||||
"name": "Evati IX - Moon 1 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002062": [
|
||||
{
|
||||
"station_id": 60012976,
|
||||
"name": "Todifrauan VII - Moon 8 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002065": [
|
||||
{
|
||||
"station_id": 60012973,
|
||||
"name": "Lasleinur VI - Moon 17 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002242": [
|
||||
{
|
||||
"station_id": 60012937,
|
||||
"name": "Mamenkhanar IX - Moon 11 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30002246": [
|
||||
{
|
||||
"station_id": 60012940,
|
||||
"name": "Neziel I - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002249": [
|
||||
{
|
||||
"station_id": 60012934,
|
||||
"name": "Ruchy I - DED Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30002259": [
|
||||
{
|
||||
"station_id": 60012958,
|
||||
"name": "Sahdil III - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30002260": [
|
||||
{
|
||||
"station_id": 60012952,
|
||||
"name": "Esteban VIII - Moon 4 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30002262": [
|
||||
{
|
||||
"station_id": 60012955,
|
||||
"name": "Nalu VII - Moon 7 - DED Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30002402": [
|
||||
{
|
||||
"station_id": 60012457,
|
||||
"name": "Istodard IX - Moon 5 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012460,
|
||||
"name": "Istodard IX - Moon 16 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002404": [
|
||||
{
|
||||
"station_id": 60012469,
|
||||
"name": "Half VII - Moon 4 - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012454,
|
||||
"name": "Half VII - Moon 1 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002406": [
|
||||
{
|
||||
"station_id": 60012463,
|
||||
"name": "Hedaleolfarber VII - Moon 17 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30002407": [
|
||||
{
|
||||
"station_id": 60012466,
|
||||
"name": "Altbrard IX - Moon 8 - CONCORD Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30002414": [
|
||||
{
|
||||
"station_id": 60012430,
|
||||
"name": "Klingt VIII - CONCORD Logistic Support"
|
||||
},
|
||||
{
|
||||
"station_id": 60012433,
|
||||
"name": "Klingt IX - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012418,
|
||||
"name": "Klingt III - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002419": [
|
||||
{
|
||||
"station_id": 60012421,
|
||||
"name": "Aeditide V - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002420": [
|
||||
{
|
||||
"station_id": 60012424,
|
||||
"name": "Egbinger XII - CONCORD Academy"
|
||||
},
|
||||
{
|
||||
"station_id": 60012427,
|
||||
"name": "Egbinger V - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30002724": [
|
||||
{
|
||||
"station_id": 60012502,
|
||||
"name": "Assiettes IV - Moon 1 - CONCORD Logistic Support"
|
||||
}
|
||||
],
|
||||
"30002725": [
|
||||
{
|
||||
"station_id": 60012499,
|
||||
"name": "Goinard III - Moon 2 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012490,
|
||||
"name": "Goinard IV - Moon 2 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002726": [
|
||||
{
|
||||
"station_id": 60012505,
|
||||
"name": "Raeghoscon VIII - CONCORD Logistic Support"
|
||||
}
|
||||
],
|
||||
"30002728": [
|
||||
{
|
||||
"station_id": 60012496,
|
||||
"name": "Lermireve VIII - Moon 15 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30002730": [
|
||||
{
|
||||
"station_id": 60012493,
|
||||
"name": "Esmes IV - Moon 2 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30002762": [
|
||||
{
|
||||
"station_id": 60012511,
|
||||
"name": "Yashunen VII - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012514,
|
||||
"name": "Yashunen VII - Moon 2 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002763": [
|
||||
{
|
||||
"station_id": 60012523,
|
||||
"name": "Tennen VIII - Moon 4 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002764": [
|
||||
{
|
||||
"station_id": 60012517,
|
||||
"name": "Hatakani VI - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012508,
|
||||
"name": "Hatakani VI - Moon 10 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002766": [
|
||||
{
|
||||
"station_id": 60012520,
|
||||
"name": "Iivinen VIII - Moon 10 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30003057": [
|
||||
{
|
||||
"station_id": 60012373,
|
||||
"name": "Groothese X - Moon 13 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003058": [
|
||||
{
|
||||
"station_id": 60012367,
|
||||
"name": "Olide VI - Moon 10 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012370,
|
||||
"name": "Olide VI - Moon 14 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012379,
|
||||
"name": "Olide IV - Moon 7 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30003059": [
|
||||
{
|
||||
"station_id": 60012364,
|
||||
"name": "Adeel VIII - Moon 1 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30003061": [
|
||||
{
|
||||
"station_id": 60012376,
|
||||
"name": "Mormelot I - CONCORD Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30003374": [
|
||||
{
|
||||
"station_id": 60012355,
|
||||
"name": "Arlulf III - Moon 10 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012358,
|
||||
"name": "Arlulf VI - Moon 1 - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012361,
|
||||
"name": "Arlulf III - Moon 11 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30003375": [
|
||||
{
|
||||
"station_id": 60012346,
|
||||
"name": "Brundakur IV - Moon 1 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003376": [
|
||||
{
|
||||
"station_id": 60012349,
|
||||
"name": "Stirht VII - Moon 14 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003378": [
|
||||
{
|
||||
"station_id": 60012352,
|
||||
"name": "Nedegulf VII - Moon 4 - CONCORD Academy"
|
||||
}
|
||||
],
|
||||
"30003428": [
|
||||
{
|
||||
"station_id": 60012286,
|
||||
"name": "Hilfhurmur VIII - Moon 6 - CONCORD Logistic Support"
|
||||
}
|
||||
],
|
||||
"30003429": [
|
||||
{
|
||||
"station_id": 60012277,
|
||||
"name": "Geffur VII - Moon 8 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003430": [
|
||||
{
|
||||
"station_id": 60012289,
|
||||
"name": "Oppold III - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30003431": [
|
||||
{
|
||||
"station_id": 60012280,
|
||||
"name": "Tratokard II - Moon 1 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003432": [
|
||||
{
|
||||
"station_id": 60012283,
|
||||
"name": "Lumegen III - CONCORD Academy"
|
||||
},
|
||||
{
|
||||
"station_id": 60012274,
|
||||
"name": "Lumegen IV - Moon 2 - CONCORD Academy"
|
||||
}
|
||||
],
|
||||
"30003818": [
|
||||
{
|
||||
"station_id": 60012439,
|
||||
"name": "Aulbres X - Moon 2 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012442,
|
||||
"name": "Aulbres VII - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012445,
|
||||
"name": "Aulbres X - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012448,
|
||||
"name": "Aulbres VII - Moon 16 - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012451,
|
||||
"name": "Aulbres V - Moon 4 - CONCORD Logistic Support"
|
||||
}
|
||||
],
|
||||
"30003819": [
|
||||
{
|
||||
"station_id": 60012436,
|
||||
"name": "Barleguet IV - Moon 2 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003859": [
|
||||
{
|
||||
"station_id": 60012403,
|
||||
"name": "Neyi VII - Moon 7 - CONCORD Academy"
|
||||
}
|
||||
],
|
||||
"30003860": [
|
||||
{
|
||||
"station_id": 60012406,
|
||||
"name": "Kihtaled VIII - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012409,
|
||||
"name": "Kihtaled VIII - Moon 17 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003861": [
|
||||
{
|
||||
"station_id": 60012400,
|
||||
"name": "Ipref IV - Moon 5 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012415,
|
||||
"name": "Ipref II - Moon 1 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30003862": [
|
||||
{
|
||||
"station_id": 60012412,
|
||||
"name": "Agil VI - Moon 2 - CONCORD Logistic Support"
|
||||
}
|
||||
],
|
||||
"30004248": [
|
||||
{
|
||||
"station_id": 60012382,
|
||||
"name": "Haimeh IX - Moon 16 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30004249": [
|
||||
{
|
||||
"station_id": 60012394,
|
||||
"name": "Avada V - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30004250": [
|
||||
{
|
||||
"station_id": 60012391,
|
||||
"name": "Chibi VI - Moon 15 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30004251": [
|
||||
{
|
||||
"station_id": 60012397,
|
||||
"name": "Mishi VIII - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012385,
|
||||
"name": "Mishi VII - Moon 4 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30004253": [
|
||||
{
|
||||
"station_id": 60012388,
|
||||
"name": "Pahineh V - Moon 1 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30004280": [
|
||||
{
|
||||
"station_id": 60013003,
|
||||
"name": "Nalnifan IV - Moon 2 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30004281": [
|
||||
{
|
||||
"station_id": 60013000,
|
||||
"name": "Jerhesh VI - Moon 11 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30004284": [
|
||||
{
|
||||
"station_id": 60012997,
|
||||
"name": "Defsunun IV - Moon 1 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005069": [
|
||||
{
|
||||
"station_id": 60013015,
|
||||
"name": "Nahol X - Moon 2 - DED Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60013018,
|
||||
"name": "Nahol II - Moon 1 - DED Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60013021,
|
||||
"name": "Nahol IV - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005078": [
|
||||
{
|
||||
"station_id": 60012484,
|
||||
"name": "Keproh VIII - Moon 3 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005079": [
|
||||
{
|
||||
"station_id": 60012478,
|
||||
"name": "Zatamaka VII - Moon 2 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012481,
|
||||
"name": "Zatamaka X - Moon 2 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012472,
|
||||
"name": "Zatamaka XI - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30005080": [
|
||||
{
|
||||
"station_id": 60012475,
|
||||
"name": "Rannoze V - Moon 8 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012487,
|
||||
"name": "Rannoze VII - Moon 2 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005198": [
|
||||
{
|
||||
"station_id": 60012265,
|
||||
"name": "Pakhshi IX - Moon 20 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30005199": [
|
||||
{
|
||||
"station_id": 60012739,
|
||||
"name": "Tar III - Secure Commerce Commission Depository"
|
||||
}
|
||||
],
|
||||
"30005200": [
|
||||
{
|
||||
"station_id": 60012259,
|
||||
"name": "Tekaima I - Moon 1 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012268,
|
||||
"name": "Tekaima V - Moon 9 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005204": [
|
||||
{
|
||||
"station_id": 60012271,
|
||||
"name": "Yulai VIII - Moon 10 - CONCORD Logistic Support"
|
||||
},
|
||||
{
|
||||
"station_id": 60012736,
|
||||
"name": "Yulai III - Moon 1 - Secure Commerce Commission Depository"
|
||||
},
|
||||
{
|
||||
"station_id": 60012916,
|
||||
"name": "Yulai VIII - Moon 12 - DED Logistic Support"
|
||||
},
|
||||
{
|
||||
"station_id": 60012256,
|
||||
"name": "Yulai IX (Kjarval) - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012922,
|
||||
"name": "Yulai X - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005205": [
|
||||
{
|
||||
"station_id": 60012919,
|
||||
"name": "Tarta IX - Moon 14 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005206": [
|
||||
{
|
||||
"station_id": 60012262,
|
||||
"name": "Kemerk V - Moon 10 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30005315": [
|
||||
{
|
||||
"station_id": 60012994,
|
||||
"name": "Eletta VII - Moon 7 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30005319": [
|
||||
{
|
||||
"station_id": 60012988,
|
||||
"name": "Raneilles V - Moon 2 - DED Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012991,
|
||||
"name": "Raneilles III - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005322": [
|
||||
{
|
||||
"station_id": 60012931,
|
||||
"name": "Scolluzer VI - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30005323": [
|
||||
{
|
||||
"station_id": 60012925,
|
||||
"name": "Sortet VI - Moon 5 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30005328": [
|
||||
{
|
||||
"station_id": 60012928,
|
||||
"name": "Reblier VIII - Moon 7 - DED Logistic Support"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
290
priv/repo/data/route_by_systems/ss_cleaning.json
Normal file
290
priv/repo/data/route_by_systems/ss_cleaning.json
Normal file
@@ -0,0 +1,290 @@
|
||||
{
|
||||
"type_id": null,
|
||||
"generated_at": "2026-02-03T08:42:38.039Z",
|
||||
"bands": [
|
||||
"low"
|
||||
],
|
||||
"system_ids_by_band": {
|
||||
"high": [],
|
||||
"low": [
|
||||
30000012,
|
||||
30000014,
|
||||
30000015,
|
||||
30000205,
|
||||
30001385,
|
||||
30002060,
|
||||
30002062,
|
||||
30002065,
|
||||
30002246,
|
||||
30002249,
|
||||
30002402,
|
||||
30002404,
|
||||
30002406,
|
||||
30002407,
|
||||
30002414,
|
||||
30002419,
|
||||
30002420,
|
||||
30002725,
|
||||
30002726,
|
||||
30002728,
|
||||
30002730,
|
||||
30003057,
|
||||
30003059,
|
||||
30003061,
|
||||
30003818,
|
||||
30003819,
|
||||
30004280,
|
||||
30004281,
|
||||
30004284,
|
||||
30005079,
|
||||
30005080,
|
||||
30005328
|
||||
]
|
||||
},
|
||||
"system_stations": {
|
||||
"30000012": [
|
||||
{
|
||||
"station_id": 60012292,
|
||||
"name": "Asabona IX - Moon 5 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30000014": [
|
||||
{
|
||||
"station_id": 60012298,
|
||||
"name": "Shamahi IX - Moon 12 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30000015": [
|
||||
{
|
||||
"station_id": 60012301,
|
||||
"name": "Sendaya V - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30000205": [
|
||||
{
|
||||
"station_id": 60013045,
|
||||
"name": "Obe VI - Moon 2 - DED Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30001385": [
|
||||
{
|
||||
"station_id": 60012334,
|
||||
"name": "Jan VI - Moon 21 - CONCORD Academy"
|
||||
}
|
||||
],
|
||||
"30002060": [
|
||||
{
|
||||
"station_id": 60012970,
|
||||
"name": "Evati IX - Moon 1 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002062": [
|
||||
{
|
||||
"station_id": 60012976,
|
||||
"name": "Todifrauan VII - Moon 8 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002065": [
|
||||
{
|
||||
"station_id": 60012973,
|
||||
"name": "Lasleinur VI - Moon 17 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002246": [
|
||||
{
|
||||
"station_id": 60012940,
|
||||
"name": "Neziel I - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002249": [
|
||||
{
|
||||
"station_id": 60012934,
|
||||
"name": "Ruchy I - DED Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30002402": [
|
||||
{
|
||||
"station_id": 60012457,
|
||||
"name": "Istodard IX - Moon 5 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012460,
|
||||
"name": "Istodard IX - Moon 16 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002404": [
|
||||
{
|
||||
"station_id": 60012454,
|
||||
"name": "Half VII - Moon 1 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012469,
|
||||
"name": "Half VII - Moon 4 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002406": [
|
||||
{
|
||||
"station_id": 60012463,
|
||||
"name": "Hedaleolfarber VII - Moon 17 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30002407": [
|
||||
{
|
||||
"station_id": 60012466,
|
||||
"name": "Altbrard IX - Moon 8 - CONCORD Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30002414": [
|
||||
{
|
||||
"station_id": 60012418,
|
||||
"name": "Klingt III - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012430,
|
||||
"name": "Klingt VIII - CONCORD Logistic Support"
|
||||
},
|
||||
{
|
||||
"station_id": 60012433,
|
||||
"name": "Klingt IX - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002419": [
|
||||
{
|
||||
"station_id": 60012421,
|
||||
"name": "Aeditide V - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002420": [
|
||||
{
|
||||
"station_id": 60012424,
|
||||
"name": "Egbinger XII - CONCORD Academy"
|
||||
},
|
||||
{
|
||||
"station_id": 60012427,
|
||||
"name": "Egbinger V - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30002725": [
|
||||
{
|
||||
"station_id": 60012490,
|
||||
"name": "Goinard IV - Moon 2 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012499,
|
||||
"name": "Goinard III - Moon 2 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002726": [
|
||||
{
|
||||
"station_id": 60012505,
|
||||
"name": "Raeghoscon VIII - CONCORD Logistic Support"
|
||||
}
|
||||
],
|
||||
"30002728": [
|
||||
{
|
||||
"station_id": 60012496,
|
||||
"name": "Lermireve VIII - Moon 15 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30002730": [
|
||||
{
|
||||
"station_id": 60012493,
|
||||
"name": "Esmes IV - Moon 2 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30003057": [
|
||||
{
|
||||
"station_id": 60012373,
|
||||
"name": "Groothese X - Moon 13 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003059": [
|
||||
{
|
||||
"station_id": 60012364,
|
||||
"name": "Adeel VIII - Moon 1 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30003061": [
|
||||
{
|
||||
"station_id": 60012376,
|
||||
"name": "Mormelot I - CONCORD Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30003818": [
|
||||
{
|
||||
"station_id": 60012439,
|
||||
"name": "Aulbres X - Moon 2 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012442,
|
||||
"name": "Aulbres VII - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012445,
|
||||
"name": "Aulbres X - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012448,
|
||||
"name": "Aulbres VII - Moon 16 - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012451,
|
||||
"name": "Aulbres V - Moon 4 - CONCORD Logistic Support"
|
||||
}
|
||||
],
|
||||
"30003819": [
|
||||
{
|
||||
"station_id": 60012436,
|
||||
"name": "Barleguet IV - Moon 2 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30004280": [
|
||||
{
|
||||
"station_id": 60013003,
|
||||
"name": "Nalnifan IV - Moon 2 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30004281": [
|
||||
{
|
||||
"station_id": 60013000,
|
||||
"name": "Jerhesh VI - Moon 11 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30004284": [
|
||||
{
|
||||
"station_id": 60012997,
|
||||
"name": "Defsunun IV - Moon 1 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005079": [
|
||||
{
|
||||
"station_id": 60012472,
|
||||
"name": "Zatamaka XI - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012478,
|
||||
"name": "Zatamaka VII - Moon 2 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012481,
|
||||
"name": "Zatamaka X - Moon 2 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30005080": [
|
||||
{
|
||||
"station_id": 60012475,
|
||||
"name": "Rannoze V - Moon 8 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012487,
|
||||
"name": "Rannoze VII - Moon 2 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005328": [
|
||||
{
|
||||
"station_id": 60012928,
|
||||
"name": "Reblier VIII - Moon 7 - DED Logistic Support"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
355
priv/repo/data/route_by_systems/trade_hubs.json
Normal file
355
priv/repo/data/route_by_systems/trade_hubs.json
Normal file
@@ -0,0 +1,355 @@
|
||||
{
|
||||
"generated_at": "2026-02-03T09:56:41.703Z",
|
||||
"systems": [
|
||||
{
|
||||
"system_id": 30000142,
|
||||
"system_name": "Jita",
|
||||
"stations": [
|
||||
{
|
||||
"station_id": 60000361,
|
||||
"name": "Jita IV - Moon 6 - Ytiri Storage"
|
||||
},
|
||||
{
|
||||
"station_id": 60000364,
|
||||
"name": "Jita V - Moon 14 - Ytiri Storage"
|
||||
},
|
||||
{
|
||||
"station_id": 60000451,
|
||||
"name": "Jita IV - Moon 6 - Hyasyoda Corporation Refinery"
|
||||
},
|
||||
{
|
||||
"station_id": 60000463,
|
||||
"name": "Jita VI - Hyasyoda Corporation Refinery"
|
||||
},
|
||||
{
|
||||
"station_id": 60002953,
|
||||
"name": "Jita V - Moon 17 - Caldari Constructions Production Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60002959,
|
||||
"name": "Jita IV - Moon 10 - Caldari Constructions Production Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60003055,
|
||||
"name": "Jita IV - Moon 11 - Expert Housing Production Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60003460,
|
||||
"name": "Jita IV - Moon 10 - Caldari Business Tribunal Law School"
|
||||
},
|
||||
{
|
||||
"station_id": 60003463,
|
||||
"name": "Jita VII - Moon 2 - Caldari Business Tribunal Law School"
|
||||
},
|
||||
{
|
||||
"station_id": 60003466,
|
||||
"name": "Jita IV - Moon 4 - Caldari Business Tribunal Bureau Offices"
|
||||
},
|
||||
{
|
||||
"station_id": 60003469,
|
||||
"name": "Jita IV - Caldari Business Tribunal Information Center"
|
||||
},
|
||||
{
|
||||
"station_id": 60003757,
|
||||
"name": "Jita IV - Moon 5 - Caldari Navy Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60003760,
|
||||
"name": "Jita IV - Moon 4 - Caldari Navy Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60004423,
|
||||
"name": "Jita IV - Moon 6 - Caldari Provisions School"
|
||||
},
|
||||
{
|
||||
"station_id": 60015169,
|
||||
"name": "Jita VI - Paragon Fulfillment Center"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"system_id": 30002187,
|
||||
"system_name": "Amarr",
|
||||
"stations": [
|
||||
{
|
||||
"station_id": 60002569,
|
||||
"name": "Amarr VIII (Oris) - Moon 4 - Expert Distribution Retail Center"
|
||||
},
|
||||
{
|
||||
"station_id": 60008494,
|
||||
"name": "Amarr VIII (Oris) - Emperor Family Academy"
|
||||
},
|
||||
{
|
||||
"station_id": 60008950,
|
||||
"name": "Amarr VI (Zorast) - Moon 2 - Theology Council Tribunal"
|
||||
},
|
||||
{
|
||||
"station_id": 60015168,
|
||||
"name": "Amarr VII (Nemantizor) - Paragon Fulfillment Center"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"system_id": 30002659,
|
||||
"system_name": "Dodixie",
|
||||
"stations": [
|
||||
{
|
||||
"station_id": 60001864,
|
||||
"name": "Dodixie IX - Moon 9 - Nugoeihuvi Corporation Development Studio"
|
||||
},
|
||||
{
|
||||
"station_id": 60001867,
|
||||
"name": "Dodixie VIII - Moon 3 - Nugoeihuvi Corporation Development Studio"
|
||||
},
|
||||
{
|
||||
"station_id": 60011866,
|
||||
"name": "Dodixie IX - Moon 20 - Federation Navy Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60015170,
|
||||
"name": "Dodixie V - Paragon Fulfillment Center"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"system_id": 30002053,
|
||||
"system_name": "Hek",
|
||||
"stations": [
|
||||
{
|
||||
"station_id": 60004516,
|
||||
"name": "Hek IV - Krusual Tribe Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60005236,
|
||||
"name": "Hek II - Moon 1 - Minmatar Mining Corporation Mineral Reserve"
|
||||
},
|
||||
{
|
||||
"station_id": 60005686,
|
||||
"name": "Hek VIII - Moon 12 - Boundless Creation Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60011287,
|
||||
"name": "Hek VIII - Moon 17 - Aliastra Retail Center"
|
||||
},
|
||||
{
|
||||
"station_id": 60015140,
|
||||
"name": "Hek VII - Tribal Liberation Force Logistic Support"
|
||||
},
|
||||
{
|
||||
"station_id": 60015171,
|
||||
"name": "Hek IX - Paragon Fulfillment Center"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"system_id": 30002510,
|
||||
"system_name": "Rens",
|
||||
"stations": [
|
||||
{
|
||||
"station_id": 60004588,
|
||||
"name": "Rens VI - Moon 8 - Brutor Tribe Treasury"
|
||||
},
|
||||
{
|
||||
"station_id": 60004594,
|
||||
"name": "Rens VII - Moon 17 - Brutor Tribe Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60005725,
|
||||
"name": "Rens VI - Six Kin Development Warehouse"
|
||||
},
|
||||
{
|
||||
"station_id": 60009106,
|
||||
"name": "Rens VIII - Moon 3 - TransStellar Shipping Storage"
|
||||
},
|
||||
{
|
||||
"station_id": 60012721,
|
||||
"name": "Rens VII - Moon 20 - Sisters of EVE Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012724,
|
||||
"name": "Rens VII - Moon 21 - Sisters of EVE Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012727,
|
||||
"name": "Rens VII - Moon 19 - Sisters of EVE Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60015172,
|
||||
"name": "Rens V - Paragon Fulfillment Center"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"system_ids": [
|
||||
30000142,
|
||||
30002187,
|
||||
30002659,
|
||||
30002053,
|
||||
30002510
|
||||
],
|
||||
"system_stations": {
|
||||
"30000142": [
|
||||
{
|
||||
"station_id": 60000361,
|
||||
"name": "Jita IV - Moon 6 - Ytiri Storage"
|
||||
},
|
||||
{
|
||||
"station_id": 60000364,
|
||||
"name": "Jita V - Moon 14 - Ytiri Storage"
|
||||
},
|
||||
{
|
||||
"station_id": 60000451,
|
||||
"name": "Jita IV - Moon 6 - Hyasyoda Corporation Refinery"
|
||||
},
|
||||
{
|
||||
"station_id": 60000463,
|
||||
"name": "Jita VI - Hyasyoda Corporation Refinery"
|
||||
},
|
||||
{
|
||||
"station_id": 60002953,
|
||||
"name": "Jita V - Moon 17 - Caldari Constructions Production Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60002959,
|
||||
"name": "Jita IV - Moon 10 - Caldari Constructions Production Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60003055,
|
||||
"name": "Jita IV - Moon 11 - Expert Housing Production Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60003460,
|
||||
"name": "Jita IV - Moon 10 - Caldari Business Tribunal Law School"
|
||||
},
|
||||
{
|
||||
"station_id": 60003463,
|
||||
"name": "Jita VII - Moon 2 - Caldari Business Tribunal Law School"
|
||||
},
|
||||
{
|
||||
"station_id": 60003466,
|
||||
"name": "Jita IV - Moon 4 - Caldari Business Tribunal Bureau Offices"
|
||||
},
|
||||
{
|
||||
"station_id": 60003469,
|
||||
"name": "Jita IV - Caldari Business Tribunal Information Center"
|
||||
},
|
||||
{
|
||||
"station_id": 60003757,
|
||||
"name": "Jita IV - Moon 5 - Caldari Navy Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60003760,
|
||||
"name": "Jita IV - Moon 4 - Caldari Navy Assembly Plant",
|
||||
"special": true
|
||||
},
|
||||
{
|
||||
"station_id": 60004423,
|
||||
"name": "Jita IV - Moon 6 - Caldari Provisions School"
|
||||
},
|
||||
{
|
||||
"station_id": 60015169,
|
||||
"name": "Jita VI - Paragon Fulfillment Center"
|
||||
}
|
||||
],
|
||||
"30002053": [
|
||||
{
|
||||
"station_id": 60004516,
|
||||
"name": "Hek IV - Krusual Tribe Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60005236,
|
||||
"name": "Hek II - Moon 1 - Minmatar Mining Corporation Mineral Reserve"
|
||||
},
|
||||
{
|
||||
"station_id": 60005686,
|
||||
"name": "Hek VIII - Moon 12 - Boundless Creation Factory",
|
||||
"special": true
|
||||
},
|
||||
{
|
||||
"station_id": 60011287,
|
||||
"name": "Hek VIII - Moon 17 - Aliastra Retail Center"
|
||||
},
|
||||
{
|
||||
"station_id": 60015140,
|
||||
"name": "Hek VII - Tribal Liberation Force Logistic Support"
|
||||
},
|
||||
{
|
||||
"station_id": 60015171,
|
||||
"name": "Hek IX - Paragon Fulfillment Center"
|
||||
}
|
||||
],
|
||||
"30002187": [
|
||||
{
|
||||
"station_id": 60002569,
|
||||
"name": "Amarr VIII (Oris) - Moon 4 - Expert Distribution Retail Center"
|
||||
},
|
||||
{
|
||||
"station_id": 60008494,
|
||||
"name": "Amarr VIII (Oris) - Emperor Family Academy",
|
||||
"special": true
|
||||
},
|
||||
{
|
||||
"station_id": 60008950,
|
||||
"name": "Amarr VI (Zorast) - Moon 2 - Theology Council Tribunal"
|
||||
},
|
||||
{
|
||||
"station_id": 60015168,
|
||||
"name": "Amarr VII (Nemantizor) - Paragon Fulfillment Center"
|
||||
}
|
||||
],
|
||||
"30002510": [
|
||||
{
|
||||
"station_id": 60004588,
|
||||
"name": "Rens VI - Moon 8 - Brutor Tribe Treasury",
|
||||
"special": true
|
||||
},
|
||||
{
|
||||
"station_id": 60004594,
|
||||
"name": "Rens VII - Moon 17 - Brutor Tribe Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60005725,
|
||||
"name": "Rens VI - Six Kin Development Warehouse"
|
||||
},
|
||||
{
|
||||
"station_id": 60009106,
|
||||
"name": "Rens VIII - Moon 3 - TransStellar Shipping Storage"
|
||||
},
|
||||
{
|
||||
"station_id": 60012721,
|
||||
"name": "Rens VII - Moon 20 - Sisters of EVE Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012724,
|
||||
"name": "Rens VII - Moon 21 - Sisters of EVE Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012727,
|
||||
"name": "Rens VII - Moon 19 - Sisters of EVE Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60015172,
|
||||
"name": "Rens V - Paragon Fulfillment Center"
|
||||
}
|
||||
],
|
||||
"30002659": [
|
||||
{
|
||||
"station_id": 60001864,
|
||||
"name": "Dodixie IX - Moon 9 - Nugoeihuvi Corporation Development Studio"
|
||||
},
|
||||
{
|
||||
"station_id": 60001867,
|
||||
"name": "Dodixie VIII - Moon 3 - Nugoeihuvi Corporation Development Studio"
|
||||
},
|
||||
{
|
||||
"station_id": 60011866,
|
||||
"name": "Dodixie IX - Moon 20 - Federation Navy Assembly Plant",
|
||||
"special": true
|
||||
},
|
||||
{
|
||||
"station_id": 60015170,
|
||||
"name": "Dodixie V - Paragon Fulfillment Center"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,9 @@ defmodule WandererApp.Map.CorporationChangePermissionTest do
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
@test_corp_id_a 98000001
|
||||
@test_corp_id_b 98000002
|
||||
@test_alliance_id_a 99000001
|
||||
@test_corp_id_a 98_000_001
|
||||
@test_corp_id_b 98_000_002
|
||||
@test_alliance_id_a 99_000_001
|
||||
|
||||
setup do
|
||||
# Configure the PubSubMock to forward to real Phoenix.PubSub for broadcast testing
|
||||
@@ -70,7 +70,8 @@ defmodule WandererApp.Map.CorporationChangePermissionTest do
|
||||
simulate_corporation_change(character, @test_corp_id_b)
|
||||
|
||||
# Should receive :update_permissions broadcast
|
||||
assert_receive :update_permissions, 1000,
|
||||
assert_receive :update_permissions,
|
||||
1000,
|
||||
"Should receive :update_permissions when corporation changes"
|
||||
end
|
||||
|
||||
@@ -94,7 +95,8 @@ defmodule WandererApp.Map.CorporationChangePermissionTest do
|
||||
simulate_alliance_removal(character)
|
||||
|
||||
# Should receive :update_permissions broadcast
|
||||
assert_receive :update_permissions, 1000,
|
||||
assert_receive :update_permissions,
|
||||
1000,
|
||||
"Should receive :update_permissions when alliance is removed"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -116,6 +116,7 @@ defmodule WandererApp.Map.Server.AclScopesPropagationTest do
|
||||
|
||||
# Fetch again to confirm persistence
|
||||
{:ok, refetched_map} = WandererApp.MapRepo.get(map.id, [])
|
||||
|
||||
assert refetched_map.scopes == [:wormholes, :hi, :low, :null],
|
||||
"Refetched map should have updated scopes"
|
||||
end
|
||||
|
||||
@@ -577,35 +577,55 @@ defmodule WandererApp.Map.Server.MapScopesTest do
|
||||
# All should be valid because no stargates exist in test data = wormhole connections
|
||||
|
||||
# Hi-Sec combinations
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @ls_system_id) == true,
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @ls_system_id) ==
|
||||
true,
|
||||
"Hi->Low should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @ns_system_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @ns_system_id) ==
|
||||
true,
|
||||
"Hi->Null should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @pochven_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @pochven_id) ==
|
||||
true,
|
||||
"Hi->Pochven should be valid"
|
||||
|
||||
# Low-Sec combinations
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ls_system_id, @hs_system_id) == true,
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ls_system_id, @hs_system_id) ==
|
||||
true,
|
||||
"Low->Hi should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ls_system_id, @ns_system_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ls_system_id, @ns_system_id) ==
|
||||
true,
|
||||
"Low->Null should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ls_system_id, @pochven_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ls_system_id, @pochven_id) ==
|
||||
true,
|
||||
"Low->Pochven should be valid"
|
||||
|
||||
# Null-Sec combinations
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ns_system_id, @hs_system_id) == true,
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ns_system_id, @hs_system_id) ==
|
||||
true,
|
||||
"Null->Hi should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ns_system_id, @ls_system_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ns_system_id, @ls_system_id) ==
|
||||
true,
|
||||
"Null->Low should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ns_system_id, @pochven_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ns_system_id, @pochven_id) ==
|
||||
true,
|
||||
"Null->Pochven should be valid"
|
||||
|
||||
# Pochven combinations
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @pochven_id, @hs_system_id) == true,
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @pochven_id, @hs_system_id) ==
|
||||
true,
|
||||
"Pochven->Hi should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @pochven_id, @ls_system_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @pochven_id, @ls_system_id) ==
|
||||
true,
|
||||
"Pochven->Low should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @pochven_id, @ns_system_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @pochven_id, @ns_system_id) ==
|
||||
true,
|
||||
"Pochven->Null should be valid"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -464,9 +464,8 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
Task.async(fn ->
|
||||
params = %{"solar_system_id" => 30_000_140 + i}
|
||||
result = Signatures.create_signature(conn, params)
|
||||
# We expect either system_not_found (system doesn't exist in test)
|
||||
# or the MapTestHelpers would have caught the map server error
|
||||
assert {:error, :system_not_found} = result
|
||||
# Fake solar_system_ids aren't in EVE static data, so we get :invalid_solar_system
|
||||
assert {:error, :invalid_solar_system} = result
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user