Compare commits

..

18 Commits

Author SHA1 Message Date
Dmitry Popov
f4ddc8dc8b Merge pull request #530 from s-no1ukno/main
feat(map): Update Owners on Multiple Structures
2026-01-29 19:37:27 +04:00
Dmitry Popov
ac9b46e24d Merge pull request #585 from guarzo/guarzo/addsysfromapi
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2026-01-27 12:21:35 +04:00
Guarzo
40d0a0777a fix: adding system when linked signature is provided 2026-01-27 03:10:33 +00:00
Dmitry Popov
608792d99a Merge pull request #584 from guarzo/guarzo/autoadd
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
feat: auto add system on sig addition
2026-01-26 22:45:57 +04:00
Guarzo
dc9e0c821e feat: auto add system on sig addition 2026-01-26 13:47:37 +00:00
Dmitry Popov
79d4fd0e43 Merge pull request #582 from guarzo/guarzo/evenmoredev
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
fix: saving updates to unknown sigs
2026-01-25 15:20:19 +04:00
Guarzo
5d03c1ecc7 fix: saving updates to unknown sigs 2026-01-25 01:50:14 +00:00
Dmitry Popov
2eef05495e Merge pull request #580 from guarzo/guarzo/moreapidev
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
fix: wh position and sig type change
2026-01-24 02:07:53 +04:00
Guarzo
f724455a1e fix: wh position and sig type change 2026-01-23 16:01:52 +00:00
Dmitry Popov
33bbb3425c Merge pull request #579 from guarzo/guarzo/apidev
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
fix: api updates and linked sig addition
2026-01-21 00:04:01 +04:00
Guarzo
a919bd9038 fix: api updates and linked sig addition 2026-01-20 17:55:30 +00:00
Dmitry Popov
8ae34cd94a Merge pull request #577 from guarzo/guarzo/apisigfixes
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
fix: api fixes and format
2026-01-16 16:06:34 +04:00
Guarzo
2f38da52e8 fix: api fixes and format 2026-01-16 08:39:19 +00:00
Jordan Snow
a7d6b06332 feat(map): Reviewed changes
Adding the changes from first review of PR #530. This includes cleanup,
wrapping callbacks in a `useCallback()` hook, and inclusion of clsx
wrapper for styling.
2025-10-23 22:06:42 -06:00
Jordan Snow
8f6da817db Fix: Wrong file added to commits
This file should not have been added to previous commits, and was only
changed to allow for a fix in my local dev environment.
2025-10-19 12:26:28 -06:00
Jordan Snow
378f22a1ef feat(map): Logic for multiple owner updates
Finished all the logic for updating owners on multiple structures in a
single system.
2025-10-18 21:43:44 -06:00
Jordan Snow
14730097b2 feat(map) Adding all the things to the modal
Added a bunch of text and formatting to the system structures owners
dialog box
2025-10-18 20:26:28 -06:00
Jordan Snow
e8bff3098a feat(map): wip New Dialog for Structure Owners
Added the new modal to be able to update all structures within a system
in a single update.
2025-10-18 19:24:19 -06:00
48 changed files with 968 additions and 2782 deletions

View File

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

View File

@@ -1,22 +1,21 @@
import React, { RefObject, useCallback, useMemo } from 'react';
import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { CharacterTypeRaw, SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
import { 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, RouteStationSummary } from '@/hooks/Mapper/types/routes.ts';
import { Route } 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';
export interface ContextMenuSystemInfoProps {
systemStatics: Map<number, SolarSystemStaticInfoRaw>;
hubs: string[];
contextMenuRef: RefObject<ContextMenu>;
systemId: string | undefined;
systemIdFrom?: string | undefined;
@@ -38,104 +37,11 @@ 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);
if (chars.length === 0) {
return [
{
label: 'Stations',
icon: PrimeIcons.MAP_MARKER,
items: [{ label: 'No online characters', disabled: true }],
},
];
}
return [
{
label: 'Stations',
icon: PrimeIcons.MAP_MARKER,
items: stations.map(station => {
const destinationId = station.station_id.toString();
if (chars.length === 1) {
return {
label: station.station_name,
items: getStationWaypointItems(destinationId, chars.slice(0, 1)),
};
}
return {
label: station.station_name,
className: 'w-[500px]',
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;
@@ -144,10 +50,6 @@ 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,
@@ -167,20 +69,15 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
{ separator: true },
...getJumpPlannerMenu(system, routes),
...getWaypointMenu(systemId, system.system_class),
...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,
},
]
: []),
{
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
? [
{
@@ -197,18 +94,15 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
systems,
getJumpPlannerMenu,
getWaypointMenu,
getStationsMenu,
hubs,
onHubToggle,
onAddSystem,
onOpenSettings,
toggleHubCommand,
routes,
]);
return (
<>
<ContextMenu className={classes.ContextMenu} model={items} ref={contextMenuRef} breakpoint="767px" />
<ContextMenu model={items} ref={contextMenuRef} breakpoint="767px" />
</>
);
};

View File

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

View File

@@ -7,7 +7,6 @@ import {
SystemStructures,
WRoutesPublic,
WRoutesUser,
WRoutesBy,
WSystemKills,
} from '@/hooks/Mapper/components/mapInterface/widgets';
@@ -19,7 +18,6 @@ export enum WidgetsIds {
signatures = 'signatures',
local = 'local',
routes = 'routes',
routesBy = 'routesBy',
structures = 'structures',
kills = 'kills',
comments = 'comments',
@@ -62,13 +60,6 @@ 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 },
@@ -121,10 +112,6 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
id: WidgetsIds.routes,
label: 'Routes',
},
{
id: WidgetsIds.routesBy,
label: 'Routes By',
},
{
id: WidgetsIds.userRoutes,
label: 'User Routes',

View File

@@ -86,13 +86,6 @@ export const RoutesWidgetContent = () => {
[handleClick],
);
// useEffect(() => {
// // eslint-disable-next-line no-console
// console.log('JOipP', `loading`, loading);
// }, [loading]);
if (isRestricted && !isSubscriptionActive) {
return (
<div className="w-full h-full flex items-center justify-center">
@@ -115,7 +108,6 @@ export const RoutesWidgetContent = () => {
return <div className="w-full h-full flex justify-center items-center select-none">Routes not set</div>;
}
return (
<>
<LoadingWrapper loading={loading}>
@@ -137,6 +129,7 @@ export const RoutesWidgetContent = () => {
offset: 10,
}}
/>
<SystemView
systemId={route.destination.toString()}
className={clsx('select-none text-center cursor-context-menu')}
@@ -145,7 +138,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>
@@ -154,7 +147,9 @@ export const RoutesWidgetContent = () => {
})}
</div>
</LoadingWrapper>
<ContextMenuSystemInfo
hubs={hubs}
routes={preparedRoutes}
systems={systems}
systemStatics={systemStatics}
@@ -167,10 +162,9 @@ export const RoutesWidgetContent = () => {
type RoutesWidgetCompProps = {
title: ReactNode | string;
renderContent?: (content: ReactNode, compact: boolean) => ReactNode;
};
export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps) => {
export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
const { data, update, addHubCommand } = useRouteProvider();
@@ -189,7 +183,7 @@ export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => addHubCommand?.(item.value.toString()),
async item => addHubCommand(item.value.toString()),
[addHubCommand],
);
@@ -197,17 +191,15 @@ export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps
<Widget
label={
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
<div className="select-none flex items-center gap-2">{title}</div>
<span className="select-none">{title}</span>
<LayoutEventBlocker className="flex items-center gap-2">
{addHubCommand && (
<WdImgButton
className={PrimeIcons.PLUS_CIRCLE}
onClick={onAddSystem}
tooltip={{
content: 'Click here to add new system to routes',
}}
/>
)}
<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
@@ -231,38 +223,24 @@ export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps
</div>
}
>
{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>
)}
<RoutesWidgetContent />
<RoutesSettingsDialog visible={routeSettingsVisible} setVisible={setRouteSettingsVisible} />
{addHubCommand && (
<AddSystemDialog
title="Add system to routes"
visible={openAddSystem}
setVisible={() => setOpenAddSystem(false)}
onSubmit={handleSubmitAddSystem}
/>
)}
<AddSystemDialog
title="Add system to routes"
visible={openAddSystem}
setVisible={() => setOpenAddSystem(false)}
onSubmit={handleSubmitAddSystem}
/>
</Widget>
);
};
export const RoutesWidget = forwardRef<RoutesImperativeHandle, RoutesWidgetProps & RoutesWidgetCompProps>(
({ title, renderContent, ...props }, ref) => {
({ title, ...props }, ref) => {
return (
<RoutesProvider {...props} ref={ref}>
<RoutesWidgetComp title={title} renderContent={renderContent} />
<RoutesWidgetComp title={title} />
</RoutesProvider>
);
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,158 @@
import React, { useCallback, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { AutoComplete } from 'primereact/autocomplete';
import clsx from 'clsx';
import { StructureItem } from '../helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
import { useToast } from '@/hooks/Mapper/ToastProvider';
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;
// fetch corporation ticker if we have an ownerId
for (const structure of editData) {
if (structure.ownerId) {
try {
// TODO fix it
const { ticker } = await outCommand({
type: OutCommand.getCorporationTicker,
data: { corp_id: structure.ownerId },
});
structure.ownerTicker = ticker ?? '';
} catch (err) {
console.error('Failed to fetch ticker:', err);
structure.ownerTicker = '';
}
}
}
onSave(editData);
onClose()
};
return (
<Dialog
visible={visible}
onHide={onClose}
header={'Update All Structure Owners'}
className={clsx('myStructuresOwnersDialog', 'text-stone-200 w-full max-w-md')}
>
<div className="flex flex-col gap-2 text-[14px]">
<div className="flex gap-2">
Updating the corporation name below will update all structures currently
saved within the system.
</div>
<hr />
<div className="flex flex-col gap-2">
<label className="grid grid-cols-[100px_1fr] gap-2 items-start mt-2">
<span className="mt-1">Structures to update:</span>
<ul>
{structures && structures.map((item, i) => (
<li key={i}>{item.structureType || 'Unknown Type'} - {item.name}</li>
))}
</ul>
</label>
</div>
<hr />
<div>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<span>Owner:</span>
<AutoComplete
id="owner"
value={ownerInput}
suggestions={ownerSuggestions}
completeMethod={searchOwners}
minLength={3}
delay={400}
field="label"
placeholder="Corporation name..."
onChange={e => setOwnerInput(e.value)}
onSelect={e => handleSelectOwner(e.value)}
/>
</label>
</div>
</div>
<div className="flex justify-end items-center gap-2 mt-4">
<WdButton label="Save" className="p-button-sm" onClick={handleSaveClick} />
</div>
</Dialog>
);
};

View File

@@ -1,136 +0,0 @@
import { useCallback, useMemo, useRef, useState } 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';
export type RoutesByType = 'blueLoot' | 'redLoot';
export type RoutesBySecurityType = 'both' | 'low' | 'high';
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',
},
];
const ROUTES_BY_SECURITY_OPTIONS = [
{ label: 'All', value: 'both' },
{ label: 'High', value: 'high' },
{ label: 'Low', value: 'low' },
];
export const WRoutesBy = ({ type = 'blueLoot', title = 'Routes By' }: WRoutesByProps) => {
const {
outCommand,
storedSettings: { settingsRoutes, settingsRoutesUpdate },
data,
} = useMapRootState();
const [criteriaType, setCriteriaType] = useState<RoutesByType>(type);
const [securityType, setSecurityType] = useState<RoutesBySecurityType>('both');
const routesListBy = data.routesListBy;
const loadRoutesCommand: LoadRoutesCommand = useCallback(
async (systemId, routesSettings) => {
await outCommand({
type: OutCommand.getRoutesBy,
data: {
system_id: systemId,
type: criteriaType,
securityType: securityType || 'both',
routes_settings: routesSettings,
},
});
},
[outCommand, criteriaType, securityType],
);
const hubs = useMemo(() => routesListBy?.routes?.map(route => route.destination.toString()) ?? [], [routesListBy]);
const { loading: internalLoading } = useLoadRoutesBy({
data: settingsRoutes,
loadRoutesCommand,
routesList: routesListBy,
deps: [criteriaType, securityType],
});
const ref = useRef<HTMLDivElement>(null);
const compactSmall = useMaxWidth(ref, 180);
const compactMiddle = useMaxWidth(ref, 245);
return (
<RoutesWidget
title={title}
renderContent={(content /*, compact*/) => (
<div className="h-full grid grid-rows-[1fr_auto]" ref={ref}>
{content}
<div className="flex items-center gap-2 justify-end mb-2 px-2 pt-2">
{!compactSmall && (
<Dropdown
value={securityType}
options={ROUTES_BY_SECURITY_OPTIONS}
onChange={e => setSecurityType(e.value)}
className="w-[90px] [&_span]:!text-[12px]"
/>
)}
<Dropdown
value={criteriaType}
itemTemplate={e => {
return (
<div className="flex items-center gap-2">
<img src={e.icon} height="18" width="18"></img>
<span className="text-[12px]">{e.label}</span>
</div>
);
}}
valueTemplate={e => {
if (compactMiddle) {
return (
<div className="flex items-center gap-2 min-w-[50px]">
<img src={e.icon} height="18" width="18"></img>
</div>
);
}
return (
<div className="flex items-center gap-2">
<img src={e.icon} height="18" width="18"></img>
<span className="text-[12px]">{e.label}</span>
</div>
);
}}
options={ROUTES_BY_OPTIONS}
onChange={e => setCriteriaType(e.value)}
className={clsx({
['w-[130px]']: !compactMiddle,
['w-[65px]']: compactMiddle,
})}
/>
</div>
</div>
)}
data={settingsRoutes}
update={settingsRoutesUpdate}
hubs={hubs}
routesList={routesListBy}
loading={internalLoading}
/>
);
};

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import {
MapUnionTypes,
OutCommandHandler,
SolarSystemConnection,
StringBoolean,
TrackingCharacter,
UseCharactersCacheData,
UseCommentsData,
@@ -75,7 +76,6 @@ const INITIAL_DATA: MapRootData = {
userHubs: [],
routes: undefined,
userRoutes: undefined,
routesListBy: undefined,
kills: [],
connections: [],
detailedKills: {},

View File

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

View File

@@ -38,7 +38,6 @@ import {
useMapInit,
useMapUpdated,
useRoutes,
useRoutesListBy,
useUserRoutes,
} from './api';
@@ -62,7 +61,6 @@ 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();
@@ -117,9 +115,6 @@ 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);

View File

@@ -25,7 +25,6 @@ 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',
@@ -63,7 +62,6 @@ export type Command =
| Commands.detailedKillsUpdated
| Commands.routes
| Commands.userRoutes
| Commands.routesListBy
| Commands.selectSystem
| Commands.selectSystems
| Commands.centerSystem
@@ -123,7 +121,6 @@ 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;
@@ -202,7 +199,6 @@ export interface CommandData {
[Commands.mapUpdated]: CommandMapUpdated;
[Commands.routes]: CommandRoutes;
[Commands.userRoutes]: CommandUserRoutes;
[Commands.routesListBy]: CommandRoutesListBy;
[Commands.killsUpdated]: CommandKillsUpdated;
[Commands.detailedKillsUpdated]: CommandDetailedKillsUpdated;
[Commands.selectSystem]: CommandSelectSystem;
@@ -236,7 +232,6 @@ export enum OutCommand {
deleteUserHub = 'delete_user_hub',
getRoutes = 'get_routes',
getUserRoutes = 'get_user_routes',
getRoutesBy = 'get_routes_by',
getCharacterJumps = 'get_character_jumps',
getStructures = 'get_structures',
getSignatures = 'get_signatures',

View File

@@ -20,7 +20,6 @@ export type MapUnionTypes = {
systemSignatures: Record<string, SystemSignature[]>;
routes?: RoutesList;
userRoutes?: RoutesList;
routesListBy?: RoutesList;
kills: Record<number, number>;
connections: SolarSystemConnection[];
userPermissions: Partial<UserPermissions>;

View File

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

View File

@@ -67,10 +67,6 @@ wanderer_kills_base_url =
config_dir
|> get_var_from_path_or_env("WANDERER_KILLS_BASE_URL", "ws://wanderer-kills:4004")
route_builder_base_url =
config_dir
|> get_var_from_path_or_env("WANDERER_ROUTE_BUILDER_BASE_URL", "http://localhost:2001")
map_subscriptions_enabled =
config_dir
|> get_var_from_path_or_env("WANDERER_MAP_SUBSCRIPTIONS_ENABLED", "false")
@@ -178,7 +174,6 @@ config :wanderer_app,
character_api_disabled: character_api_disabled,
wanderer_kills_service_enabled: wanderer_kills_service_enabled,
wanderer_kills_base_url: wanderer_kills_base_url,
route_builder_base_url: route_builder_base_url,
map_subscriptions_enabled: map_subscriptions_enabled,
map_connection_auto_expire_hours: map_connection_auto_expire_hours,
map_connection_auto_eol_hours: map_connection_auto_eol_hours,

View File

@@ -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

View File

@@ -14,7 +14,6 @@ defmodule WandererApp.Env do
def base_url(), do: get_key(:web_app_url, "<BASE_URL>")
def base_metrics_only(), do: get_key(:base_metrics_only, false)
def custom_route_base_url(), do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
def route_builder_base_url(), do: get_key(:route_builder_base_url, "http://localhost:2001")
def invites(), do: get_key(:invites, false)
def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)

View File

@@ -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} ->

View File

@@ -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

View File

@@ -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
# -- Wormholespecific 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}

View File

@@ -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,161 @@ 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})
# Only update target system if it doesn't already have a linked signature
if is_nil(target_system.linked_sig_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
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
# Update connection ship_size_type from signature wormhole type
signature_ship_size_type = EVEUtil.get_wh_size(signature.type)
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

View File

@@ -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

View File

@@ -1,295 +0,0 @@
defmodule WandererApp.Map.RoutesBy do
@moduledoc """
Routes-by helper that uses the local route builder service.
"""
require Logger
@minimum_route_attrs [
:system_class,
:class_title,
:security,
:triglavian_invasion_status,
:solar_system_id,
:solar_system_name,
:region_name,
:is_shattered
]
@default_routes_settings %{
path_type: "shortest",
include_mass_crit: true,
include_eol: false,
include_frig: true,
include_cruise: true,
avoid_wormholes: false,
avoid_pochven: false,
avoid_edencom: false,
avoid_triglavian: false,
include_thera: true,
avoid: []
}
@zarzakh_system 30_100_000
@default_avoid_systems [@zarzakh_system]
@get_link_pairs_advanced_params [
:include_mass_crit,
:include_eol,
:include_frig
]
def find(map_id, origin, routes_settings, type) do
origin = parse_origin(origin)
routes_settings = @default_routes_settings |> Map.merge(routes_settings || %{})
connections = build_connections(map_id, routes_settings)
avoidance_list = build_avoidance_list(routes_settings)
security_type =
routes_settings
|> Map.get(:security_type, "both")
|> normalize_security_type()
payload = %{
origin: origin,
flag: routes_settings.path_type,
connections: connections,
avoid: avoidance_list,
count: 40,
type: type,
security_type: security_type
}
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)
}
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

View File

@@ -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

View File

@@ -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

View File

@@ -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)
@@ -327,6 +329,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
group: sig["group"],
type: Map.get(sig, "type"),
custom_info: Map.get(sig, "custom_info"),
linked_system_id: Map.get(sig, "linked_system_id"),
# Use character_eve_id from sig if provided, otherwise use the default
character_eve_id: Map.get(sig, "character_eve_id", character_eve_id),
deleted: false

View File

@@ -1,107 +0,0 @@
defmodule WandererApp.RouteBuilderClient do
@moduledoc """
HTTP client for the local route builder service.
"""
require Logger
@timeout_opts [pool_timeout: 5_000, receive_timeout: :timer.seconds(30)]
@loot_dir Path.join(["repo", "data", "route_by_systems"])
def find_closest(%{
origin: origin,
flag: flag,
connections: connections,
avoid: avoid,
count: count,
type: type,
security_type: security_type
}) do
url = "#{WandererApp.Env.route_builder_base_url()}/route/findClosest"
destinations = destinations_for(type, security_type)
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) do
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)
{:error, reason} ->
Logger.error("[RouteBuilderClient] Failed to load loot data: #{inspect(reason)}")
[]
end
end
def stations_for(type) do
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
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(_), do: load_loot_file("blueloot.json")
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

View File

@@ -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>

View File

@@ -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", [])}

View File

@@ -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

View File

@@ -1,27 +0,0 @@
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

View File

@@ -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

View File

@@ -43,25 +43,6 @@ 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)
@@ -161,33 +142,6 @@ 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")
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_list_by, {solar_system_id, routes}}
end)
{:noreply, socket}
end
def handle_ui_event(
"add_hub",
%{"system_id" => solar_system_id} = _event,

View File

@@ -101,13 +101,11 @@ defmodule WandererAppWeb.MapEventHandler do
@map_routes_events [
:routes,
:user_routes,
:routes_list_by
:user_routes
]
@map_routes_ui_events [
"get_routes",
"get_routes_by",
"get_user_routes",
"set_autopilot_waypoint",
"add_hub",

View File

@@ -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,11 +343,6 @@ 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

View File

@@ -1,947 +0,0 @@
{
"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"
}
]
}
}

View File

@@ -1,839 +0,0 @@
{
"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"
}
]
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)