mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-02 14:02:37 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d59d709f1 | ||
|
|
4343e9070c | ||
|
|
b62373fb5f | ||
|
|
3da98f8e56 | ||
|
|
494d24952e | ||
|
|
8a6b17bd7b | ||
|
|
d2e859a74e | ||
|
|
4a78d55d22 | ||
|
|
dc252b8c4b | ||
|
|
c433205e89 | ||
|
|
d6bc5b57b1 | ||
|
|
280a286266 | ||
|
|
d5c18b5de3 | ||
|
|
7452e5d011 | ||
|
|
71674b0d52 | ||
|
|
5b4824bd5d | ||
|
|
deda16a7da | ||
|
|
0b7c067de7 | ||
|
|
0d0db8c129 | ||
|
|
9f1b7994a3 | ||
|
|
378df0ac70 | ||
|
|
0e4a132f69 | ||
|
|
631746375d | ||
|
|
7dc01dad54 | ||
|
|
8a9807d3e5 | ||
|
|
39df3c97ce | ||
|
|
46c1ccdfcc | ||
|
|
8817536038 | ||
|
|
c3bb23a6ee | ||
|
|
7e9c4c575e | ||
|
|
5a70eee91e | ||
|
|
228f6990a1 | ||
|
|
d80ed0e70e | ||
|
|
4576c75737 | ||
|
|
99dcf49fbc | ||
|
|
6fb3edbfd6 |
80
CHANGELOG.md
80
CHANGELOG.md
@@ -2,6 +2,86 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.77.9](https://github.com/wanderer-industries/wanderer/compare/v1.77.8...v1.77.9) (2025-09-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Auto select following char system on start
|
||||
|
||||
## [v1.77.8](https://github.com/wanderer-industries/wanderer/compare/v1.77.7...v1.77.8) (2025-09-03)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking
|
||||
|
||||
## [v1.77.7](https://github.com/wanderer-industries/wanderer/compare/v1.77.6...v1.77.7) (2025-09-03)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking
|
||||
|
||||
## [v1.77.6](https://github.com/wanderer-industries/wanderer/compare/v1.77.5...v1.77.6) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking, added grace period to reduce false-positive cases
|
||||
|
||||
## [v1.77.5](https://github.com/wanderer-industries/wanderer/compare/v1.77.4...v1.77.5) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* resolve tracking issues
|
||||
|
||||
## [v1.77.4](https://github.com/wanderer-industries/wanderer/compare/v1.77.3...v1.77.4) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* pr feedback
|
||||
|
||||
* ensure pub/sub occurs after acl api change
|
||||
|
||||
## [v1.77.3](https://github.com/wanderer-industries/wanderer/compare/v1.77.2...v1.77.3) (2025-08-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
## [v1.77.2](https://github.com/wanderer-industries/wanderer/compare/v1.77.1...v1.77.2) (2025-08-28)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* update system signature api to return correct system id
|
||||
|
||||
## [v1.77.1](https://github.com/wanderer-industries/wanderer/compare/v1.77.0...v1.77.1) (2025-08-28)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import type { PanelPosition } from '@reactflow/core';
|
||||
import clsx from 'clsx';
|
||||
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
@@ -16,8 +23,6 @@ import ReactFlow, {
|
||||
import 'reactflow/dist/style.css';
|
||||
import classes from './Map.module.scss';
|
||||
import { MapProvider, useMapState } from './MapProvider';
|
||||
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
|
||||
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import {
|
||||
ContextMenuConnection,
|
||||
ContextMenuRoot,
|
||||
@@ -26,14 +31,9 @@ import {
|
||||
useContextMenuRootHandlers,
|
||||
} from './components';
|
||||
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import clsx from 'clsx';
|
||||
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
|
||||
import { useBackgroundVars } from './hooks/useBackgroundVars';
|
||||
import type { PanelPosition } from '@reactflow/core';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
|
||||
|
||||
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { useEventBuffer } from '@/hooks/Mapper/hooks';
|
||||
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
@@ -11,6 +13,20 @@ export const useMapInit = () => {
|
||||
const ref = useRef({ rf, data, update });
|
||||
ref.current = { update, data, rf };
|
||||
|
||||
const updateSystems = useCallback((systems: SolarSystemRawType[]) => {
|
||||
const { rf } = ref.current;
|
||||
rf.setNodes(systems.map(convertSystem2Node));
|
||||
}, []);
|
||||
|
||||
const { handleEvent: handleUpdateSystems } = useEventBuffer<any>(updateSystems);
|
||||
|
||||
const updateEdges = useCallback((connections: SolarSystemConnection[]) => {
|
||||
const { rf } = ref.current;
|
||||
rf.setEdges(connections.map(convertConnection2Edge));
|
||||
}, []);
|
||||
|
||||
const { handleEvent: handleUpdateConnections } = useEventBuffer<any>(updateEdges);
|
||||
|
||||
return useCallback(
|
||||
({
|
||||
systems,
|
||||
@@ -24,7 +40,6 @@ export const useMapInit = () => {
|
||||
hubs,
|
||||
}: CommandInit) => {
|
||||
const { update } = ref.current;
|
||||
const { rf } = ref.current;
|
||||
|
||||
const updateData: Partial<MapData> = {};
|
||||
|
||||
@@ -63,11 +78,13 @@ export const useMapInit = () => {
|
||||
update(updateData);
|
||||
|
||||
if (systems) {
|
||||
rf.setNodes(systems.map(convertSystem2Node));
|
||||
handleUpdateSystems(systems);
|
||||
// rf.setNodes(systems.map(convertSystem2Node));
|
||||
}
|
||||
|
||||
if (connections) {
|
||||
rf.setEdges(connections.map(convertConnection2Edge));
|
||||
handleUpdateConnections(connections);
|
||||
// rf.setEdges(connections.map(convertConnection2Edge));
|
||||
}
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandSelectSystems } from '@/hooks/Mapper/types';
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import { CommandSelectSystems } from '@/hooks/Mapper/types';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
|
||||
export const useSelectSystems = (onSelectionChange: OnMapSelectionChange) => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
|
||||
import {
|
||||
CommandAddConnections,
|
||||
CommandAddSystems,
|
||||
@@ -19,8 +18,11 @@ import {
|
||||
CommandUpdateSystems,
|
||||
MapHandlers,
|
||||
} from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
|
||||
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import {
|
||||
useCenterSystem,
|
||||
useCommandsCharacters,
|
||||
useCommandsConnections,
|
||||
useMapAddSystems,
|
||||
@@ -28,10 +30,8 @@ import {
|
||||
useMapInit,
|
||||
useMapRemoveSystems,
|
||||
useMapUpdateSystems,
|
||||
useCenterSystem,
|
||||
useSelectSystems,
|
||||
} from './api';
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
|
||||
export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange: OnMapSelectionChange) => {
|
||||
const mapInit = useMapInit();
|
||||
@@ -49,91 +49,87 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
const { charactersUpdated, presentCharacters, characterAdded, characterRemoved, characterUpdated } =
|
||||
useCommandsCharacters();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return {
|
||||
command(type, data) {
|
||||
switch (type) {
|
||||
case Commands.init:
|
||||
mapInit(data as CommandInit);
|
||||
break;
|
||||
case Commands.addSystems:
|
||||
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
|
||||
break;
|
||||
case Commands.updateSystems:
|
||||
mapUpdateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems:
|
||||
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
|
||||
break;
|
||||
case Commands.addConnections:
|
||||
setTimeout(() => addConnections(data as CommandAddConnections), 100);
|
||||
break;
|
||||
case Commands.removeConnections:
|
||||
setTimeout(() => removeConnections(data as CommandRemoveConnections), 100);
|
||||
break;
|
||||
case Commands.charactersUpdated:
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
break;
|
||||
case Commands.characterAdded:
|
||||
characterAdded(data as CommandCharacterAdded);
|
||||
break;
|
||||
case Commands.characterRemoved:
|
||||
characterRemoved(data as CommandCharacterRemoved);
|
||||
break;
|
||||
case Commands.characterUpdated:
|
||||
characterUpdated(data as CommandCharacterUpdated);
|
||||
break;
|
||||
case Commands.presentCharacters:
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.updateConnection:
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
break;
|
||||
case Commands.mapUpdated:
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
break;
|
||||
case Commands.killsUpdated:
|
||||
killsUpdated(data as CommandKillsUpdated);
|
||||
break;
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
command(type, data) {
|
||||
switch (type) {
|
||||
case Commands.init:
|
||||
mapInit(data as CommandInit);
|
||||
break;
|
||||
case Commands.addSystems:
|
||||
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
|
||||
break;
|
||||
case Commands.updateSystems:
|
||||
mapUpdateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems:
|
||||
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
|
||||
break;
|
||||
case Commands.addConnections:
|
||||
setTimeout(() => addConnections(data as CommandAddConnections), 100);
|
||||
break;
|
||||
case Commands.removeConnections:
|
||||
setTimeout(() => removeConnections(data as CommandRemoveConnections), 100);
|
||||
break;
|
||||
case Commands.charactersUpdated:
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
break;
|
||||
case Commands.characterAdded:
|
||||
characterAdded(data as CommandCharacterAdded);
|
||||
break;
|
||||
case Commands.characterRemoved:
|
||||
characterRemoved(data as CommandCharacterRemoved);
|
||||
break;
|
||||
case Commands.characterUpdated:
|
||||
characterUpdated(data as CommandCharacterUpdated);
|
||||
break;
|
||||
case Commands.presentCharacters:
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.updateConnection:
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
break;
|
||||
case Commands.mapUpdated:
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
break;
|
||||
case Commands.killsUpdated:
|
||||
killsUpdated(data as CommandKillsUpdated);
|
||||
break;
|
||||
|
||||
case Commands.centerSystem:
|
||||
setTimeout(() => {
|
||||
const systemId = `${data}`;
|
||||
centerSystem(systemId as CommandSelectSystem);
|
||||
}, 100);
|
||||
break;
|
||||
case Commands.centerSystem:
|
||||
setTimeout(() => {
|
||||
const systemId = `${data}`;
|
||||
centerSystem(systemId as CommandSelectSystem);
|
||||
}, 100);
|
||||
break;
|
||||
|
||||
case Commands.selectSystem:
|
||||
selectSystems({ systems: [data as string], delay: 500 });
|
||||
break;
|
||||
case Commands.selectSystem:
|
||||
selectSystems({ systems: [data as string], delay: 500 });
|
||||
break;
|
||||
|
||||
case Commands.selectSystems:
|
||||
selectSystems(data as CommandSelectSystems);
|
||||
break;
|
||||
case Commands.selectSystems:
|
||||
selectSystems(data as CommandSelectSystems);
|
||||
break;
|
||||
|
||||
case Commands.pingAdded:
|
||||
case Commands.pingCancelled:
|
||||
case Commands.routes:
|
||||
case Commands.signaturesUpdated:
|
||||
case Commands.linkSignatureToSystem:
|
||||
case Commands.detailedKillsUpdated:
|
||||
case Commands.characterActivityData:
|
||||
case Commands.trackingCharactersData:
|
||||
case Commands.updateActivity:
|
||||
case Commands.updateTracking:
|
||||
case Commands.userSettingsUpdated:
|
||||
// do nothing
|
||||
break;
|
||||
case Commands.pingAdded:
|
||||
case Commands.pingCancelled:
|
||||
case Commands.routes:
|
||||
case Commands.signaturesUpdated:
|
||||
case Commands.linkSignatureToSystem:
|
||||
case Commands.detailedKillsUpdated:
|
||||
case Commands.characterActivityData:
|
||||
case Commands.trackingCharactersData:
|
||||
case Commands.updateActivity:
|
||||
case Commands.updateTracking:
|
||||
case Commands.userSettingsUpdated:
|
||||
// do nothing
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Map handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
default:
|
||||
console.warn(`Map handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
|
||||
|
||||
const useThrottle = () => {
|
||||
const throttleSeed = useRef<number | null>(null);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './useClipboard';
|
||||
export * from './useConfirmPopup';
|
||||
export * from './useEventBuffer';
|
||||
export * from './useHotkey';
|
||||
export * from './usePageVisibility';
|
||||
export * from './useSkipContextMenu';
|
||||
export * from './useThrottle';
|
||||
export * from './useConfirmPopup';
|
||||
|
||||
41
assets/js/hooks/Mapper/hooks/useEventBuffer.ts
Normal file
41
assets/js/hooks/Mapper/hooks/useEventBuffer.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import debounce from 'lodash.debounce';
|
||||
import { useCallback, useRef } from 'react';
|
||||
export type UseEventBufferHandler<T> = (event: T) => void;
|
||||
|
||||
export const useEventBuffer = <T>(handler: UseEventBufferHandler<T>) => {
|
||||
// @ts-ignore
|
||||
const eventsBufferRef = useRef<T[]>([]);
|
||||
|
||||
const eventTick = useCallback(
|
||||
debounce(() => {
|
||||
if (eventsBufferRef.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event = eventsBufferRef.current.shift()!;
|
||||
handler(event);
|
||||
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
|
||||
|
||||
if (eventsBufferRef.current.length > 0) {
|
||||
eventTick();
|
||||
}
|
||||
}, 10),
|
||||
[],
|
||||
);
|
||||
const eventTickRef = useRef(eventTick);
|
||||
eventTickRef.current = eventTick;
|
||||
|
||||
// @ts-ignore
|
||||
const handleEvent = useCallback(event => {
|
||||
if (!eventTickRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventsBufferRef.current.push(event);
|
||||
eventTickRef.current();
|
||||
}, []);
|
||||
|
||||
return { handleEvent };
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { CommandInit } from '@/hooks/Mapper/types';
|
||||
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
import { CommandInit } from '@/hooks/Mapper/types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useMapInit = () => {
|
||||
const { update } = useMapRootState();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ForwardedRef, useImperativeHandle } from 'react';
|
||||
import {
|
||||
CommandAddConnections,
|
||||
CommandAddSystems,
|
||||
@@ -8,24 +7,25 @@ import {
|
||||
CommandCharactersUpdated,
|
||||
CommandCharacterUpdated,
|
||||
CommandCommentAdd,
|
||||
CommandCommentRemoved,
|
||||
CommandInit,
|
||||
CommandLinkSignatureToSystem,
|
||||
CommandMapUpdated,
|
||||
CommandPingAdded,
|
||||
CommandPingCancelled,
|
||||
CommandPresentCharacters,
|
||||
CommandRemoveConnections,
|
||||
CommandRemoveSystems,
|
||||
CommandRoutes,
|
||||
Commands,
|
||||
CommandSignaturesUpdated,
|
||||
CommandTrackingCharactersData,
|
||||
CommandUpdateConnection,
|
||||
CommandUpdateSystems,
|
||||
CommandUserSettingsUpdated,
|
||||
Commands,
|
||||
MapHandlers,
|
||||
CommandCommentRemoved,
|
||||
CommandPingAdded,
|
||||
CommandPingCancelled,
|
||||
} from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ForwardedRef, useImperativeHandle } from 'react';
|
||||
|
||||
import {
|
||||
useCommandComments,
|
||||
@@ -39,9 +39,9 @@ import {
|
||||
useUserRoutes,
|
||||
} from './api';
|
||||
|
||||
import { useCommandsActivity } from './api/useCommandsActivity';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { DetailedKill } from '../../types/kills';
|
||||
import { useCommandsActivity } from './api/useCommandsActivity';
|
||||
|
||||
export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const mapInit = useMapInit();
|
||||
@@ -63,127 +63,123 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const { pingAdded, pingCancelled } = useCommandPings();
|
||||
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return {
|
||||
command(type, data) {
|
||||
switch (type) {
|
||||
case Commands.init: // USED
|
||||
mapInit(data as CommandInit);
|
||||
break;
|
||||
case Commands.addSystems: // USED
|
||||
addSystems(data as CommandAddSystems);
|
||||
break;
|
||||
case Commands.updateSystems: // USED
|
||||
updateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems: // USED
|
||||
removeSystems(data as CommandRemoveSystems);
|
||||
break;
|
||||
case Commands.addConnections: // USED
|
||||
addConnections(data as CommandAddConnections);
|
||||
break;
|
||||
case Commands.removeConnections: // USED
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
break;
|
||||
case Commands.updateConnection: // USED
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
break;
|
||||
case Commands.charactersUpdated: // USED
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
break;
|
||||
case Commands.characterAdded: // USED
|
||||
characterAdded(data as CommandCharacterAdded);
|
||||
break;
|
||||
case Commands.characterRemoved: // USED
|
||||
characterRemoved(data as CommandCharacterRemoved);
|
||||
break;
|
||||
case Commands.characterUpdated: // USED
|
||||
characterUpdated(data as CommandCharacterUpdated);
|
||||
break;
|
||||
case Commands.presentCharacters: // USED
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.mapUpdated: // USED
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
break;
|
||||
case Commands.routes:
|
||||
mapRoutes(data as CommandRoutes);
|
||||
break;
|
||||
case Commands.userRoutes:
|
||||
mapUserRoutes(data as CommandRoutes);
|
||||
break;
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
command(type, data) {
|
||||
switch (type) {
|
||||
case Commands.init: // USED
|
||||
mapInit(data as CommandInit);
|
||||
break;
|
||||
case Commands.addSystems: // USED
|
||||
addSystems(data as CommandAddSystems);
|
||||
break;
|
||||
case Commands.updateSystems: // USED
|
||||
updateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems: // USED
|
||||
removeSystems(data as CommandRemoveSystems);
|
||||
break;
|
||||
case Commands.addConnections: // USED
|
||||
addConnections(data as CommandAddConnections);
|
||||
break;
|
||||
case Commands.removeConnections: // USED
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
break;
|
||||
case Commands.updateConnection: // USED
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
break;
|
||||
case Commands.charactersUpdated: // USED
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
break;
|
||||
case Commands.characterAdded: // USED
|
||||
characterAdded(data as CommandCharacterAdded);
|
||||
break;
|
||||
case Commands.characterRemoved: // USED
|
||||
characterRemoved(data as CommandCharacterRemoved);
|
||||
break;
|
||||
case Commands.characterUpdated: // USED
|
||||
characterUpdated(data as CommandCharacterUpdated);
|
||||
break;
|
||||
case Commands.presentCharacters: // USED
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.mapUpdated: // USED
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
break;
|
||||
case Commands.routes:
|
||||
mapRoutes(data as CommandRoutes);
|
||||
break;
|
||||
case Commands.userRoutes:
|
||||
mapUserRoutes(data as CommandRoutes);
|
||||
break;
|
||||
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
break;
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
break;
|
||||
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
break;
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
break;
|
||||
|
||||
case Commands.centerSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.centerSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.selectSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.selectSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.killsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.killsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.detailedKillsUpdated:
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
case Commands.detailedKillsUpdated:
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
|
||||
case Commands.characterActivityData:
|
||||
characterActivityData(data as CommandCharacterActivityData);
|
||||
break;
|
||||
case Commands.characterActivityData:
|
||||
characterActivityData(data as CommandCharacterActivityData);
|
||||
break;
|
||||
|
||||
case Commands.trackingCharactersData:
|
||||
trackingCharactersData(data as CommandTrackingCharactersData);
|
||||
break;
|
||||
case Commands.trackingCharactersData:
|
||||
trackingCharactersData(data as CommandTrackingCharactersData);
|
||||
break;
|
||||
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
|
||||
case Commands.userSettingsUpdated:
|
||||
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
||||
break;
|
||||
case Commands.userSettingsUpdated:
|
||||
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
||||
break;
|
||||
|
||||
case Commands.systemCommentAdded:
|
||||
addComment(data as CommandCommentAdd);
|
||||
break;
|
||||
case Commands.systemCommentAdded:
|
||||
addComment(data as CommandCommentAdd);
|
||||
break;
|
||||
|
||||
case Commands.systemCommentRemoved:
|
||||
removeComment(data as CommandCommentRemoved);
|
||||
break;
|
||||
case Commands.systemCommentRemoved:
|
||||
removeComment(data as CommandCommentRemoved);
|
||||
break;
|
||||
|
||||
case Commands.pingAdded:
|
||||
pingAdded(data as CommandPingAdded);
|
||||
break;
|
||||
case Commands.pingAdded:
|
||||
pingAdded(data as CommandPingAdded);
|
||||
break;
|
||||
|
||||
case Commands.pingCancelled:
|
||||
pingCancelled(data as CommandPingCancelled);
|
||||
break;
|
||||
case Commands.pingCancelled:
|
||||
pingCancelled(data as CommandPingCancelled);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
}
|
||||
|
||||
emitMapEvent({ name: type, data });
|
||||
},
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
emitMapEvent({ name: type, data });
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEventBuffer } from '@/hooks/Mapper/hooks';
|
||||
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
|
||||
|
||||
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { RefObject, useCallback, useEffect, useRef } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
|
||||
|
||||
// const inIndex = 0;
|
||||
// const prevEventTime = +new Date();
|
||||
@@ -10,10 +11,28 @@ const LAST_VERSION_KEY = 'wandererLastVersion';
|
||||
// @ts-ignore
|
||||
export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRef: RefObject<any>) => {
|
||||
const visible = usePageVisibility();
|
||||
|
||||
const wasHiddenOnce = useRef(false);
|
||||
const visibleRef = useRef(visible);
|
||||
visibleRef.current = visible;
|
||||
|
||||
// @ts-ignore
|
||||
const handleBufferedEvent = useCallback(({ type, body }) => {
|
||||
if (!visibleRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
handlerRefs.forEach(ref => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current?.command(type, body);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { handleEvent: handleMapEvent } = useEventBuffer<any>(handleBufferedEvent);
|
||||
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// const [record, setRecord] = useLocalStorageState<boolean>('record', {
|
||||
// defaultValue: false,
|
||||
@@ -54,52 +73,6 @@ export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRe
|
||||
[hooksRef.current],
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const eventsBufferRef = useRef<{ type; body }[]>([]);
|
||||
|
||||
const eventTick = useCallback(
|
||||
debounce(() => {
|
||||
if (eventsBufferRef.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, body } = eventsBufferRef.current.shift()!;
|
||||
handlerRefs.forEach(ref => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current?.command(type, body);
|
||||
});
|
||||
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
|
||||
|
||||
if (eventsBufferRef.current.length > 0) {
|
||||
eventTick();
|
||||
}
|
||||
}, 10),
|
||||
[],
|
||||
);
|
||||
const eventTickRef = useRef(eventTick);
|
||||
eventTickRef.current = eventTick;
|
||||
|
||||
// @ts-ignore
|
||||
const handleMapEvent = useCallback(({ type, body }) => {
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// const currentTime = +new Date();
|
||||
// const timeDiff = currentTime - prevEventTime;
|
||||
// prevEventTime = currentTime;
|
||||
// console.log('JOipP', `IN [${inIndex++}] [${timeDiff}] ${getFormattedTime()}`, { type, body });
|
||||
|
||||
if (!eventTickRef.current || !visibleRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventsBufferRef.current.push({ type, body });
|
||||
eventTickRef.current();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible && !wasHiddenOnce.current) {
|
||||
wasHiddenOnce.current = true;
|
||||
|
||||
@@ -79,8 +79,7 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
accept [
|
||||
:map_id,
|
||||
:character_id,
|
||||
:tracked,
|
||||
:followed
|
||||
:tracked
|
||||
]
|
||||
|
||||
argument :map_id, :uuid, allow_nil?: false
|
||||
|
||||
@@ -54,6 +54,7 @@ defmodule WandererApp.Application do
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
WandererAppWeb.PresenceGracePeriodManager,
|
||||
WandererAppWeb.Presence,
|
||||
WandererAppWeb.Endpoint
|
||||
]
|
||||
|
||||
@@ -129,21 +129,23 @@ defmodule WandererApp.CachedInfo do
|
||||
|
||||
def get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
|
||||
# Create normalized cache key (smaller ID first for bidirectional lookup)
|
||||
{id1, id2} = if from_solar_system_id < to_solar_system_id do
|
||||
{from_solar_system_id, to_solar_system_id}
|
||||
else
|
||||
{to_solar_system_id, from_solar_system_id}
|
||||
end
|
||||
|
||||
{id1, id2} =
|
||||
if from_solar_system_id < to_solar_system_id do
|
||||
{from_solar_system_id, to_solar_system_id}
|
||||
else
|
||||
{to_solar_system_id, from_solar_system_id}
|
||||
end
|
||||
|
||||
cache_key = "jump_#{id1}_#{id2}"
|
||||
|
||||
|
||||
case WandererApp.Cache.lookup(cache_key) do
|
||||
{:ok, nil} ->
|
||||
# Build jump index if not exists
|
||||
build_jump_index()
|
||||
WandererApp.Cache.lookup(cache_key)
|
||||
|
||||
result -> result
|
||||
|
||||
result ->
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
@@ -152,17 +154,19 @@ defmodule WandererApp.CachedInfo do
|
||||
{:ok, jumps} ->
|
||||
jumps
|
||||
|> Enum.each(fn jump ->
|
||||
{id1, id2} = if jump.from_solar_system_id < jump.to_solar_system_id do
|
||||
{jump.from_solar_system_id, jump.to_solar_system_id}
|
||||
else
|
||||
{jump.to_solar_system_id, jump.from_solar_system_id}
|
||||
end
|
||||
|
||||
{id1, id2} =
|
||||
if jump.from_solar_system_id < jump.to_solar_system_id do
|
||||
{jump.from_solar_system_id, jump.to_solar_system_id}
|
||||
else
|
||||
{jump.to_solar_system_id, jump.from_solar_system_id}
|
||||
end
|
||||
|
||||
cache_key = "jump_#{id1}_#{id2}"
|
||||
WandererApp.Cache.put(cache_key, jump)
|
||||
end)
|
||||
|
||||
_ -> :error
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|
||||
@check_start_queue_interval :timer.seconds(1)
|
||||
@garbage_collection_interval :timer.minutes(15)
|
||||
@untrack_characters_interval :timer.minutes(1)
|
||||
@untrack_characters_interval :timer.minutes(5)
|
||||
@inactive_character_timeout :timer.minutes(10)
|
||||
@untrack_character_timeout :timer.minutes(10)
|
||||
|
||||
@@ -54,6 +54,8 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
true
|
||||
)
|
||||
|
||||
Logger.debug(fn -> "Add character to track_characters_queue: #{inspect(character_id)}" end)
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"track_characters_queue",
|
||||
[character_id],
|
||||
@@ -69,29 +71,25 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
def stop_tracking(state, character_id) do
|
||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||
true <- Enum.member?(characters, character_id),
|
||||
{:ok, %{start_time: start_time}} <-
|
||||
WandererApp.Character.get_character_state(character_id, false) do
|
||||
false <- WandererApp.Cache.has_key?("#{character_id}:track_requested") do
|
||||
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:last_active_time")
|
||||
WandererApp.Character.delete_character_state(character_id)
|
||||
|
||||
tracked_characters =
|
||||
characters |> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
|
||||
|
||||
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{
|
||||
duration: duration
|
||||
})
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"tracked_characters",
|
||||
[],
|
||||
fn tracked_characters ->
|
||||
tracked_characters
|
||||
|> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
end
|
||||
)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
@@ -129,7 +127,8 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
"character_untrack_queue",
|
||||
[{map_id, character_id}],
|
||||
fn untrack_queue ->
|
||||
[{map_id, character_id} | untrack_queue] |> Enum.uniq()
|
||||
[{map_id, character_id} | untrack_queue]
|
||||
|> Enum.uniq_by(fn {map_id, character_id} -> map_id <> character_id end)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
@@ -20,7 +20,7 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
)
|
||||
when not is_nil(caller_pid) do
|
||||
with {:ok, character} <-
|
||||
WandererApp.Character.get_by_eve_id(character_eve_id),
|
||||
WandererApp.Character.get_by_eve_id("#{character_eve_id}"),
|
||||
{:ok, %{tracked: is_tracked}} <-
|
||||
do_update_character_tracking(character, map_id, track, caller_pid) do
|
||||
# Determine which event to send based on tracking mode and previous state
|
||||
@@ -55,15 +55,19 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
Builds tracking data for all characters with access to a map.
|
||||
"""
|
||||
def build_tracking_data(map_id, current_user_id) do
|
||||
with {:ok, map} <- WandererApp.MapRepo.get(map_id, [:acls]),
|
||||
{:ok, character_settings} <-
|
||||
WandererApp.Character.Activity.get_map_character_settings(map_id),
|
||||
with {:ok, map} <-
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
),
|
||||
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
|
||||
{:ok, %{characters: characters_with_access}} <-
|
||||
WandererApp.Maps.load_characters(map, character_settings, current_user_id) do
|
||||
WandererApp.Maps.load_characters(map, current_user_id) do
|
||||
# Map characters to tracking data
|
||||
{:ok, characters_data} =
|
||||
build_character_tracking_data(characters_with_access, character_settings)
|
||||
build_character_tracking_data(characters_with_access)
|
||||
|
||||
{:ok, main_character} =
|
||||
get_main_character(user_settings, characters_with_access, characters_with_access)
|
||||
@@ -98,21 +102,19 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
end
|
||||
|
||||
# Helper to build tracking data for each character
|
||||
defp build_character_tracking_data(characters, character_settings) do
|
||||
defp build_character_tracking_data(characters) do
|
||||
{:ok,
|
||||
Enum.map(characters, fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
|
||||
%{
|
||||
character: char |> WandererAppWeb.MapEventHandler.map_ui_character_stat(),
|
||||
tracked: (setting && setting.tracked) || false
|
||||
tracked: char.tracked
|
||||
}
|
||||
end)}
|
||||
end
|
||||
|
||||
# Private implementation of update character tracking
|
||||
defp do_update_character_tracking(character, map_id, track, caller_pid) do
|
||||
WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character.id)
|
||||
WandererApp.MapCharacterSettingsRepo.get(map_id, character.id)
|
||||
|> case do
|
||||
# Untracking flow
|
||||
{:ok, %{tracked: true} = existing_settings} ->
|
||||
|
||||
@@ -88,11 +88,18 @@ defmodule WandererApp.Map.Server do
|
||||
|> map_pid!
|
||||
|> GenServer.cast({&Impl.remove_character/2, [character_id]})
|
||||
|
||||
def untrack_characters(map_id, character_ids) when is_binary(map_id),
|
||||
do:
|
||||
map_id
|
||||
|> map_pid!
|
||||
|> GenServer.cast({&Impl.untrack_characters/2, [character_ids]})
|
||||
def untrack_characters(map_id, character_ids) when is_binary(map_id) do
|
||||
map_id
|
||||
|> map_pid()
|
||||
|> case do
|
||||
pid when is_pid(pid) ->
|
||||
GenServer.cast(pid, {&Impl.untrack_characters/2, [character_ids]})
|
||||
|
||||
_ ->
|
||||
WandererApp.Cache.insert("map_#{map_id}:started", false)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def add_system(map_id, system_info, user_id, character_id) when is_binary(map_id),
|
||||
do:
|
||||
|
||||
@@ -16,7 +16,13 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
systems
|
||||
|> Enum.flat_map(fn sys ->
|
||||
with {:ok, sigs} <- MapSystemSignature.by_system_id(sys.id) do
|
||||
sigs
|
||||
# Add solar_system_id to each signature and remove system_id
|
||||
Enum.map(sigs, fn sig ->
|
||||
sig
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, sys.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
end)
|
||||
else
|
||||
err ->
|
||||
Logger.error("[list_signatures] error: #{inspect(err)}")
|
||||
@@ -32,28 +38,70 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
def create_signature(
|
||||
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
|
||||
_conn,
|
||||
%{"solar_system_id" => _solar_system_id} = params
|
||||
) do
|
||||
attrs = Map.put(params, "character_eve_id", char_id)
|
||||
%{"solar_system_id" => solar_system_id} = params
|
||||
)
|
||||
when is_integer(solar_system_id) do
|
||||
# Convert solar_system_id to system_id for internal use
|
||||
with {:ok, system} <- MapSystem.by_map_id_and_solar_system_id(map_id, solar_system_id) do
|
||||
attrs =
|
||||
params
|
||||
|> Map.put("character_eve_id", char_id)
|
||||
|> Map.put("system_id", system.id)
|
||||
|> Map.delete("solar_system_id")
|
||||
|
||||
case Server.update_signatures(map_id, %{
|
||||
added_signatures: [attrs],
|
||||
updated_signatures: [],
|
||||
removed_signatures: [],
|
||||
solar_system_id: params["solar_system_id"],
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
}) do
|
||||
:ok ->
|
||||
{:ok, attrs}
|
||||
case Server.update_signatures(map_id, %{
|
||||
added_signatures: [attrs],
|
||||
updated_signatures: [],
|
||||
removed_signatures: [],
|
||||
solar_system_id: solar_system_id,
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
}) do
|
||||
:ok ->
|
||||
# Try to fetch the created signature to return with proper fields
|
||||
with {:ok, sigs} <-
|
||||
MapSystemSignature.by_system_id_and_eve_ids(system.id, [attrs["eve_id"]]),
|
||||
sig when not is_nil(sig) <- List.first(sigs) do
|
||||
result =
|
||||
sig
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
err ->
|
||||
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
{:ok, result}
|
||||
else
|
||||
_ ->
|
||||
# Fallback: return attrs with solar_system_id added
|
||||
attrs_result =
|
||||
attrs
|
||||
|> Map.put(:solar_system_id, solar_system_id)
|
||||
|> Map.drop(["system_id"])
|
||||
|
||||
{:ok, attrs_result}
|
||||
end
|
||||
|
||||
err ->
|
||||
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
Logger.error(
|
||||
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
|
||||
)
|
||||
|
||||
{:error, :system_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def create_signature(
|
||||
%{assigns: %{map_id: _map_id, owner_character_id: _char_id, owner_user_id: _user_id}} =
|
||||
_conn,
|
||||
%{"solar_system_id" => _invalid} = _params
|
||||
),
|
||||
do: {:error, :missing_params}
|
||||
|
||||
def create_signature(_conn, _params), do: {:error, :missing_params}
|
||||
|
||||
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
@@ -90,7 +138,18 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
delete_connection_with_sigs: false
|
||||
})
|
||||
|
||||
{:ok, attrs}
|
||||
# Fetch the updated signature to return with proper fields
|
||||
with {:ok, updated_sig} <- MapSystemSignature.by_id(sig_id) do
|
||||
result =
|
||||
updated_sig
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
{:ok, result}
|
||||
else
|
||||
_ -> {:ok, attrs}
|
||||
end
|
||||
else
|
||||
err ->
|
||||
Logger.error("[update_signature] Unexpected error: #{inspect(err)}")
|
||||
|
||||
@@ -59,6 +59,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
map_update = %{acls: map.acls, scope: map.scope}
|
||||
|
||||
WandererApp.Map.update_map(map_id, map_update)
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
broadcast_acl_updates({:ok, result}, map_id)
|
||||
|
||||
@@ -66,7 +67,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
end
|
||||
|
||||
def handle_acl_updated(map_id, acl_id) do
|
||||
{:ok, map} =
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
@@ -74,8 +75,9 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
]
|
||||
)
|
||||
|
||||
if map.acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
|
||||
WandererApp.Map.update_map(map_id, %{acls: map.acls})
|
||||
if acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
|
||||
WandererApp.Map.update_map(map_id, %{acls: acls})
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
:ok =
|
||||
acl_id
|
||||
@@ -85,7 +87,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
end
|
||||
|
||||
def handle_acl_deleted(map_id, _acl_id) do
|
||||
{:ok, map} =
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
@@ -93,7 +95,8 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
]
|
||||
)
|
||||
|
||||
WandererApp.Map.update_map(map_id, %{acls: map.acls})
|
||||
WandererApp.Map.update_map(map_id, %{acls: acls})
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
character_ids =
|
||||
map_id
|
||||
|
||||
@@ -59,7 +59,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
|
||||
def update_tracked_characters(map_id) do
|
||||
Task.start_link(fn ->
|
||||
{:ok, map_tracked_character_ids} =
|
||||
{:ok, all_map_tracked_character_ids} =
|
||||
map_id
|
||||
|> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_all()
|
||||
|> case do
|
||||
@@ -67,30 +67,19 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
|
||||
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
|
||||
map_active_tracked_characters =
|
||||
map_tracked_character_ids
|
||||
|> Enum.filter(fn character -> character in tracked_characters end)
|
||||
|
||||
{:ok, old_map_tracked_characters} =
|
||||
{:ok, actual_map_tracked_characters} =
|
||||
WandererApp.Cache.lookup("maps:#{map_id}:tracked_characters", [])
|
||||
|
||||
characters_to_remove = old_map_tracked_characters -- map_active_tracked_characters
|
||||
characters_to_remove = actual_map_tracked_characters -- all_map_tracked_character_ids
|
||||
|
||||
{:ok, invalidate_character_ids} =
|
||||
WandererApp.Cache.lookup(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[]
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
(invalidate_character_ids ++ characters_to_remove) |> Enum.uniq()
|
||||
characters_to_remove,
|
||||
fn ids ->
|
||||
(ids ++ characters_to_remove) |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
|
||||
|
||||
:ok
|
||||
end)
|
||||
end
|
||||
@@ -98,7 +87,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
def untrack_characters(map_id, character_ids) do
|
||||
character_ids
|
||||
|> Enum.each(fn character_id ->
|
||||
is_character_map_active?(map_id, character_id)
|
||||
character_map_active = is_character_map_active?(map_id, character_id)
|
||||
|
||||
character_map_active
|
||||
|> untrack_character(map_id, character_id)
|
||||
end)
|
||||
end
|
||||
@@ -126,15 +117,18 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
|
||||
def cleanup_characters(map_id, owner_id) do
|
||||
{:ok, invalidate_character_ids} =
|
||||
WandererApp.Cache.lookup(
|
||||
WandererApp.Cache.get_and_remove(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[]
|
||||
)
|
||||
|
||||
acls =
|
||||
map_id
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:acls, [])
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
)
|
||||
|
||||
invalidate_character_ids
|
||||
|> Task.async_stream(
|
||||
@@ -186,11 +180,6 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
{:error, reason} ->
|
||||
Logger.error("Error in cleanup_characters: #{inspect(reason)}")
|
||||
end)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[]
|
||||
)
|
||||
end
|
||||
|
||||
defp remove_and_untrack_characters(map_id, character_ids) do
|
||||
@@ -227,8 +216,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
{:ok, presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
|
||||
|
||||
WandererApp.Cache.lookup!("maps:#{map_id}:tracked_characters", [])
|
||||
|> Enum.filter(fn character_id -> character_id in presence_character_ids end)
|
||||
presence_character_ids
|
||||
|> Enum.map(fn character_id ->
|
||||
Task.start_link(fn ->
|
||||
character_updates =
|
||||
|
||||
@@ -25,7 +25,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
]
|
||||
|
||||
@systems_cleanup_timeout :timer.minutes(30)
|
||||
@characters_cleanup_timeout :timer.minutes(1)
|
||||
@characters_cleanup_timeout :timer.minutes(5)
|
||||
@connections_cleanup_timeout :timer.minutes(2)
|
||||
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
@@ -100,7 +100,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
Process.send_after(self(), :update_presence, @update_presence_timeout)
|
||||
Process.send_after(self(), :cleanup_connections, 5_000)
|
||||
Process.send_after(self(), :cleanup_systems, 10_000)
|
||||
Process.send_after(self(), :cleanup_characters, :timer.minutes(5))
|
||||
Process.send_after(self(), :cleanup_characters, @characters_cleanup_timeout)
|
||||
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:started", true)
|
||||
@@ -127,6 +127,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
Logger.debug(fn -> "Stopping map server for #{map_id}" end)
|
||||
|
||||
WandererApp.Cache.delete("map_#{map_id}:started")
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
:telemetry.execute([:wanderer_app, :map, :stopped], %{count: 1})
|
||||
|
||||
@@ -278,7 +279,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
end
|
||||
|
||||
def handle_event({:acl_deleted, %{acl_id: acl_id}}, %{map_id: map_id} = state) do
|
||||
AclsImpl.handle_acl_updated(map_id, acl_id)
|
||||
AclsImpl.handle_acl_deleted(map_id, acl_id)
|
||||
|
||||
state
|
||||
end
|
||||
@@ -580,18 +581,27 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
{:ok, presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
|
||||
|
||||
characters_ids =
|
||||
map_id
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:characters, [])
|
||||
{:ok, old_presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:old_presence_character_ids", [])
|
||||
|
||||
new_present_character_ids =
|
||||
presence_character_ids
|
||||
|> Enum.filter(fn character_id ->
|
||||
not Enum.member?(old_presence_character_ids, character_id)
|
||||
end)
|
||||
|
||||
not_present_character_ids =
|
||||
characters_ids
|
||||
old_presence_character_ids
|
||||
|> Enum.filter(fn character_id ->
|
||||
not Enum.member?(presence_character_ids, character_id)
|
||||
end)
|
||||
|
||||
CharactersImpl.track_characters(map_id, presence_character_ids)
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:old_presence_character_ids",
|
||||
presence_character_ids
|
||||
)
|
||||
|
||||
CharactersImpl.track_characters(map_id, new_present_character_ids)
|
||||
CharactersImpl.untrack_characters(map_id, not_present_character_ids)
|
||||
|
||||
broadcast!(
|
||||
|
||||
@@ -94,13 +94,22 @@ defmodule WandererApp.Maps do
|
||||
end
|
||||
end
|
||||
|
||||
def load_characters(map, character_settings, user_id) do
|
||||
def load_characters(map, user_id) do
|
||||
{:ok, user_characters} =
|
||||
WandererApp.Api.Character.active_by_user(%{user_id: user_id})
|
||||
|
||||
characters =
|
||||
map_available_characters =
|
||||
map
|
||||
|> get_map_available_characters(user_characters)
|
||||
|
||||
{:ok, character_settings} =
|
||||
WandererApp.MapCharacterSettingsRepo.get_by_map_filtered(
|
||||
map.id,
|
||||
map_available_characters |> Enum.map(& &1.id)
|
||||
)
|
||||
|
||||
characters =
|
||||
map_available_characters
|
||||
|> Enum.map(fn c ->
|
||||
map_character(c, character_settings |> Enum.find(&(&1.character_id == c.id)))
|
||||
end)
|
||||
@@ -176,48 +185,57 @@ defmodule WandererApp.Maps do
|
||||
tracked: tracked
|
||||
}
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "map_characters-#{map_id}",
|
||||
opts: [ttl: :timer.seconds(2)]
|
||||
)
|
||||
defp _get_map_characters(%{id: map_id} = map) do
|
||||
map_acls =
|
||||
map.acls
|
||||
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
|
||||
defp get_map_characters(%{id: map_id} = map) do
|
||||
WandererApp.Cache.lookup!("map_characters-#{map_id}")
|
||||
|> case do
|
||||
nil ->
|
||||
map_acls =
|
||||
map.acls
|
||||
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
|
||||
|
||||
map_acl_owner_ids =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.owner_id end)
|
||||
map_acl_owner_ids =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.owner_id end)
|
||||
|
||||
map_members =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.members end)
|
||||
|> List.flatten()
|
||||
|> Enum.filter(fn member -> member.role != :blocked end)
|
||||
map_members =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.members end)
|
||||
|> List.flatten()
|
||||
|> Enum.filter(fn member -> member.role != :blocked end)
|
||||
|
||||
map_member_eve_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
|
||||
|> Enum.map(fn member -> member.eve_character_id end)
|
||||
map_member_eve_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
|
||||
|> Enum.map(fn member -> member.eve_character_id end)
|
||||
|
||||
map_member_corporation_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
|
||||
|> Enum.map(fn member -> member.eve_corporation_id end)
|
||||
map_member_corporation_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
|
||||
|> Enum.map(fn member -> member.eve_corporation_id end)
|
||||
|
||||
map_member_alliance_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
|
||||
|> Enum.map(fn member -> member.eve_alliance_id end)
|
||||
map_member_alliance_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
|
||||
|> Enum.map(fn member -> member.eve_alliance_id end)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
map_acl_owner_ids: map_acl_owner_ids,
|
||||
map_member_eve_ids: map_member_eve_ids,
|
||||
map_member_corporation_ids: map_member_corporation_ids,
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}}
|
||||
map_characters =
|
||||
%{
|
||||
map_acl_owner_ids: map_acl_owner_ids,
|
||||
map_member_eve_ids: map_member_eve_ids,
|
||||
map_member_corporation_ids: map_member_corporation_ids,
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_characters-#{map_id}",
|
||||
map_characters
|
||||
)
|
||||
|
||||
{:ok, map_characters}
|
||||
|
||||
map_characters ->
|
||||
{:ok, map_characters}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_map_available_characters(map, user_characters) do
|
||||
@@ -227,7 +245,7 @@ defmodule WandererApp.Maps do
|
||||
map_member_eve_ids: map_member_eve_ids,
|
||||
map_member_corporation_ids: map_member_corporation_ids,
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}} = _get_map_characters(map)
|
||||
}} = get_map_characters(map)
|
||||
|
||||
user_characters
|
||||
|> Enum.filter(fn c ->
|
||||
|
||||
@@ -53,20 +53,8 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
def get_tracked_by_map_all(map_id),
|
||||
do: WandererApp.Api.MapCharacterSettings.tracked_by_map_all(%{map_id: map_id})
|
||||
|
||||
def get_by_map(map_id, character_id) do
|
||||
case get_by_map_filtered(map_id, [character_id]) do
|
||||
{:ok, [setting | _]} ->
|
||||
{:ok, setting}
|
||||
|
||||
{:ok, []} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def track(settings) do
|
||||
{:ok, _} = get(settings.map_id, settings.character_id)
|
||||
# Only update the tracked field, preserving other fields
|
||||
WandererApp.Api.MapCharacterSettings.track(%{
|
||||
map_id: settings.map_id,
|
||||
@@ -75,6 +63,7 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def untrack(settings) do
|
||||
{:ok, _} = get(settings.map_id, settings.character_id)
|
||||
# Only update the tracked field, preserving other fields
|
||||
WandererApp.Api.MapCharacterSettings.untrack(%{
|
||||
map_id: settings.map_id,
|
||||
@@ -83,22 +72,16 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def track!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.track(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case track(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to track: #{inspect(error)}"
|
||||
error -> raise "Failed to track: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
def untrack!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.untrack(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case untrack(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to untrack: #{inspect(error)}"
|
||||
error -> raise "Failed to untrack: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -117,22 +100,16 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def follow!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.follow(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case follow(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to follow: #{inspect(error)}"
|
||||
error -> raise "Failed to follow: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
def unfollow!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.unfollow(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case unfollow(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to unfollow: #{inspect(error)}"
|
||||
error -> raise "Failed to unfollow: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -596,10 +596,24 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
acl -> acl.id
|
||||
end)
|
||||
|
||||
updated_acls = current_acl_ids ++ [new_acl_id]
|
||||
updated_acls =
|
||||
if new_acl_id in current_acl_ids do
|
||||
current_acl_ids
|
||||
else
|
||||
current_acl_ids ++ [new_acl_id]
|
||||
end
|
||||
|
||||
case WandererApp.Api.Map.update_acls(loaded_map, %{acls: updated_acls}) do
|
||||
{:ok, updated_map} ->
|
||||
# Only broadcast if we actually added a new ACL
|
||||
unless new_acl_id in current_acl_ids do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"maps:#{loaded_map.id}",
|
||||
{:map_acl_updated, [new_acl_id], []}
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, updated_map}
|
||||
|
||||
{:error, error} ->
|
||||
|
||||
@@ -192,6 +192,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_added
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(new_member)})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -199,6 +201,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member added event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(new_member)})
|
||||
end
|
||||
|
||||
@@ -300,6 +305,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_updated
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(updated_membership)})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -307,6 +314,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member updated event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(updated_membership)})
|
||||
end
|
||||
|
||||
@@ -385,6 +395,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_removed
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{ok: true})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -392,6 +404,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member removed event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{ok: true})
|
||||
end
|
||||
|
||||
@@ -417,6 +432,14 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
# Private Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp broadcast_acl_updated(acl_id) do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{acl_id}",
|
||||
{:acl_updated, %{acl_id: acl_id}}
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp member_to_json(member) do
|
||||
base = %{
|
||||
|
||||
@@ -15,7 +15,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %OpenApiSpex.Schema{type: :string, format: :uuid},
|
||||
system_id: %OpenApiSpex.Schema{type: :string, format: :uuid},
|
||||
solar_system_id: %OpenApiSpex.Schema{type: :integer},
|
||||
eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
character_eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
name: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
@@ -31,13 +31,13 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
},
|
||||
required: [
|
||||
:id,
|
||||
:system_id,
|
||||
:solar_system_id,
|
||||
:eve_id,
|
||||
:character_eve_id
|
||||
],
|
||||
example: %{
|
||||
id: "sig-uuid-1",
|
||||
system_id: "sys-uuid-1",
|
||||
solar_system_id: 30_000_142,
|
||||
eve_id: "ABC-123",
|
||||
character_eve_id: "123456789",
|
||||
name: "Wormhole K162",
|
||||
@@ -122,7 +122,15 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
{:ok, signature} ->
|
||||
case WandererApp.Api.MapSystem.by_id(signature.system_id) do
|
||||
{:ok, system} when system.map_id == map_id ->
|
||||
json(conn, %{data: signature})
|
||||
# Add solar_system_id and remove system_id
|
||||
# Convert to a plain map to avoid encoder issues
|
||||
signature_data =
|
||||
signature
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
json(conn, %{data: signature_data})
|
||||
|
||||
_ ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Signature not found"})
|
||||
|
||||
@@ -13,6 +13,7 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
alias Plug.Crypto
|
||||
alias WandererApp.Api.User
|
||||
alias WandererApp.SecurityAudit
|
||||
alias WandererApp.Audit.RequestContext
|
||||
@@ -140,43 +141,60 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
defp authenticate_bearer_token(conn) do
|
||||
case get_req_header(conn, "authorization") do
|
||||
["Bearer " <> token] ->
|
||||
validate_api_token(token)
|
||||
validate_api_token(conn, token)
|
||||
|
||||
_ ->
|
||||
{:error, "Missing or invalid authorization header"}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_api_token(token) do
|
||||
# Look up the map by its public API key
|
||||
case find_map_by_api_key(token) do
|
||||
{:ok, map} when not is_nil(map) ->
|
||||
# Get the actual owner of the map
|
||||
case User.by_id(map.owner_id, load: :characters) do
|
||||
{:ok, user} ->
|
||||
# Return the map owner as the authenticated user
|
||||
{:ok, user, map}
|
||||
defp validate_api_token(conn, token) do
|
||||
# Check for map identifier in path params
|
||||
# According to PR feedback, routes supply params["map_identifier"]
|
||||
case conn.params["map_identifier"] do
|
||||
nil ->
|
||||
# No map identifier in path - this might be a general API endpoint
|
||||
# For now, we'll return an error since we need to validate against a specific map
|
||||
{:error, "Authentication failed", :no_map_context}
|
||||
|
||||
identifier ->
|
||||
# Resolve the identifier (could be UUID or slug)
|
||||
case resolve_map_identifier(identifier) do
|
||||
{:ok, map} ->
|
||||
# Validate the token matches this specific map's API key
|
||||
if is_binary(map.public_api_key) &&
|
||||
Crypto.secure_compare(map.public_api_key, token) do
|
||||
# Get the map owner
|
||||
case User.by_id(map.owner_id, load: :characters) do
|
||||
{:ok, user} ->
|
||||
{:ok, user, map}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Authentication failed", :map_owner_not_found}
|
||||
end
|
||||
else
|
||||
{:error, "Authentication failed", :invalid_token_for_map}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
# Return generic error with specific reason for internal logging
|
||||
{:error, "Authentication failed", :map_owner_not_found}
|
||||
{:error, "Authentication failed", :map_not_found}
|
||||
end
|
||||
|
||||
_ ->
|
||||
# Return generic error with specific reason for internal logging
|
||||
{:error, "Authentication failed", :invalid_api_key}
|
||||
end
|
||||
end
|
||||
|
||||
defp find_map_by_api_key(api_key) do
|
||||
# Import necessary modules
|
||||
import Ash.Query
|
||||
# Helper to resolve map by ID or slug
|
||||
defp resolve_map_identifier(identifier) do
|
||||
alias WandererApp.Api.Map
|
||||
|
||||
# Query for map with matching public API key
|
||||
Map
|
||||
|> filter(public_api_key == ^api_key)
|
||||
|> Ash.read_one()
|
||||
# Try as UUID first
|
||||
case Map.by_id(identifier) do
|
||||
{:ok, map} ->
|
||||
{:ok, map}
|
||||
|
||||
_ ->
|
||||
# Try as slug
|
||||
Map.get_map_by_slug(identifier)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_user_role(user) do
|
||||
|
||||
@@ -38,8 +38,6 @@ defmodule WandererAppWeb.UserAuth do
|
||||
{:halt, redirect_require_login(socket)}
|
||||
|
||||
%User{characters: characters} ->
|
||||
:ok = track_characters(characters)
|
||||
|
||||
{:cont, new_socket}
|
||||
end
|
||||
|
||||
|
||||
@@ -75,13 +75,12 @@ defmodule WandererAppWeb.CharactersLive do
|
||||
def handle_event("delete", %{"character_id" => character_id}, socket) do
|
||||
WandererApp.Character.TrackerManager.stop_tracking(character_id)
|
||||
|
||||
{:ok, map_user_settings} =
|
||||
{:ok, map_character_settings} =
|
||||
WandererApp.Api.MapCharacterSettings.tracked_by_character(%{character_id: character_id})
|
||||
|
||||
map_user_settings
|
||||
map_character_settings
|
||||
|> Enum.each(fn settings ->
|
||||
settings
|
||||
|> WandererApp.Api.MapCharacterSettings.untrack()
|
||||
{:ok, _} = WandererApp.MapCharacterSettingsRepo.untrack(settings)
|
||||
end)
|
||||
|
||||
{:ok, updated_character} =
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"user_id" => user_id} = _session, socket) when not is_nil(user_id) do
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, maps} = WandererApp.Maps.get_available_maps(socket.assigns.current_user)
|
||||
|
||||
{:ok,
|
||||
@@ -14,7 +14,6 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
characters: [],
|
||||
selected_map: nil,
|
||||
selected_map_slug: nil,
|
||||
user_id: user_id,
|
||||
maps: maps |> Enum.sort_by(& &1.name, :asc)
|
||||
)}
|
||||
end
|
||||
@@ -37,24 +36,22 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
|> assign(:page_title, "Characters Tracking")
|
||||
end
|
||||
|
||||
defp apply_action(socket, :characters, %{"slug" => map_slug} = _params) do
|
||||
selected_map = socket.assigns.maps |> Enum.find(&(&1.slug == map_slug))
|
||||
|
||||
{:ok, character_settings} =
|
||||
WandererApp.Character.Activity.get_map_character_settings(selected_map.id)
|
||||
|
||||
user_id = socket.assigns.user_id
|
||||
defp apply_action(
|
||||
%{assigns: %{current_user: current_user, maps: maps}} = socket,
|
||||
:characters,
|
||||
%{"slug" => map_slug} = _params
|
||||
) do
|
||||
selected_map = maps |> Enum.find(&(&1.slug == map_slug))
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :characters_tracking)
|
||||
|> assign(:page_title, "Characters Tracking")
|
||||
|> assign(
|
||||
selected_map: selected_map,
|
||||
selected_map_slug: map_slug,
|
||||
character_settings: character_settings
|
||||
selected_map_slug: map_slug
|
||||
)
|
||||
|> assign_async(:characters, fn ->
|
||||
WandererApp.Maps.load_characters(selected_map, character_settings, user_id)
|
||||
WandererApp.Maps.load_characters(selected_map, current_user.id)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -71,55 +68,36 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_track", %{"character_id" => character_id}, socket) do
|
||||
def handle_event(
|
||||
"toggle_track",
|
||||
%{"character_id" => character_id},
|
||||
%{assigns: %{current_user: current_user}} = socket
|
||||
) do
|
||||
selected_map = socket.assigns.selected_map
|
||||
character_settings = socket.assigns.character_settings
|
||||
|
||||
case character_settings |> Enum.find(&(&1.character_id == character_id)) do
|
||||
nil ->
|
||||
WandererApp.MapCharacterSettingsRepo.create(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id,
|
||||
tracked: true
|
||||
})
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
character_setting ->
|
||||
case character_setting.tracked do
|
||||
true ->
|
||||
character_setting
|
||||
|> WandererApp.MapCharacterSettingsRepo.untrack!()
|
||||
|
||||
WandererApp.Map.Server.untrack_characters(selected_map.id, [
|
||||
character_setting.character_id
|
||||
])
|
||||
|
||||
_ ->
|
||||
character_setting
|
||||
|> WandererApp.MapCharacterSettingsRepo.track!()
|
||||
end
|
||||
end
|
||||
|
||||
%{result: characters} = socket.assigns.characters
|
||||
|
||||
{:ok, character_settings} =
|
||||
WandererApp.Character.Activity.get_map_character_settings(selected_map.id)
|
||||
case characters |> Enum.find(&(&1.id == character_id)) do
|
||||
%{tracked: false} ->
|
||||
WandererApp.MapCharacterSettingsRepo.track(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id
|
||||
})
|
||||
|
||||
characters =
|
||||
characters
|
||||
|> Enum.map(fn c ->
|
||||
WandererApp.Maps.map_character(
|
||||
c,
|
||||
character_settings |> Enum.find(&(&1.character_id == c.id))
|
||||
)
|
||||
end)
|
||||
%{tracked: true} ->
|
||||
WandererApp.MapCharacterSettingsRepo.untrack(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id
|
||||
})
|
||||
|
||||
WandererApp.Map.Server.untrack_characters(selected_map.id, [
|
||||
character_id
|
||||
])
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(character_settings: character_settings)
|
||||
|> assign_async(:characters, fn ->
|
||||
{:ok, %{characters: characters}}
|
||||
WandererApp.Maps.load_characters(selected_map, current_user.id)
|
||||
end)}
|
||||
end
|
||||
|
||||
|
||||
@@ -244,38 +244,41 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
{:ok, user_settings} =
|
||||
WandererApp.MapUserSettingsRepo.create_or_update(map_id, current_user_id, settings)
|
||||
|
||||
{:ok, map_user_settings} =
|
||||
user_settings
|
||||
|> WandererApp.Api.MapUserSettings.update_main_character(%{
|
||||
main_character_eve_id: "#{character_eve_id}"
|
||||
})
|
||||
case Ash.update(user_settings, %{main_character_eve_id: "#{character_eve_id}"},
|
||||
action: :update_main_character
|
||||
) do
|
||||
{:ok, map_user_settings} ->
|
||||
{:ok, tracking_data} =
|
||||
WandererApp.Character.TrackingUtils.build_tracking_data(map_id, current_user_id)
|
||||
|
||||
{:ok, tracking_data} =
|
||||
WandererApp.Character.TrackingUtils.build_tracking_data(map_id, current_user_id)
|
||||
{main_character_id, main_character_eve_id} =
|
||||
WandererApp.Character.TrackingUtils.get_main_character(
|
||||
map_user_settings,
|
||||
current_user_characters,
|
||||
current_user_characters
|
||||
)
|
||||
|> case do
|
||||
{:ok, main_character} when not is_nil(main_character) ->
|
||||
{main_character.id, main_character.eve_id}
|
||||
|
||||
{main_character_id, main_character_eve_id} =
|
||||
WandererApp.Character.TrackingUtils.get_main_character(
|
||||
map_user_settings,
|
||||
current_user_characters,
|
||||
current_user_characters
|
||||
)
|
||||
|> case do
|
||||
{:ok, main_character} when not is_nil(main_character) ->
|
||||
{main_character.id, main_character.eve_id}
|
||||
_ ->
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{nil, nil}
|
||||
end
|
||||
Process.send_after(self(), %{event: :refresh_user_characters}, 50)
|
||||
|
||||
Process.send_after(self(), %{event: :refresh_user_characters}, 50)
|
||||
{:reply, %{data: tracking_data},
|
||||
socket
|
||||
|> assign(
|
||||
map_user_settings: map_user_settings,
|
||||
main_character_id: main_character_id,
|
||||
main_character_eve_id: main_character_eve_id
|
||||
)}
|
||||
|
||||
{:reply, %{data: tracking_data},
|
||||
socket
|
||||
|> assign(
|
||||
map_user_settings: map_user_settings,
|
||||
main_character_id: main_character_id,
|
||||
main_character_eve_id: main_character_eve_id
|
||||
)}
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to update main character: #{inspect(reason)}")
|
||||
{:reply, %{error: "Failed to update main character"}, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
@@ -333,21 +336,18 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
def needs_tracking_setup?(
|
||||
only_tracked_characters,
|
||||
characters,
|
||||
character_settings,
|
||||
user_permissions
|
||||
) do
|
||||
tracked_count =
|
||||
characters
|
||||
|> Enum.count(fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
setting && setting.tracked
|
||||
char.tracked
|
||||
end)
|
||||
|
||||
untracked_count =
|
||||
characters
|
||||
|> Enum.count(fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
setting == nil || !setting.tracked
|
||||
!char.tracked
|
||||
end)
|
||||
|
||||
user_permissions.track_character &&
|
||||
|
||||
@@ -422,14 +422,11 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
current_user_characters |> Enum.map(& &1.id)
|
||||
),
|
||||
{:ok, map_user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
|
||||
{:ok, character_settings} <-
|
||||
WandererApp.Character.Activity.get_map_character_settings(map_id),
|
||||
{:ok, %{characters: available_map_characters}} =
|
||||
WandererApp.Maps.load_characters(map, character_settings, current_user_id) do
|
||||
WandererApp.Maps.load_characters(map, current_user_id) do
|
||||
tracked_data =
|
||||
get_tracked_data(
|
||||
available_map_characters,
|
||||
character_settings,
|
||||
user_permissions,
|
||||
only_tracked_characters
|
||||
)
|
||||
@@ -473,15 +470,13 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
|
||||
defp get_tracked_data(
|
||||
available_map_characters,
|
||||
character_settings,
|
||||
user_permissions,
|
||||
only_tracked_characters
|
||||
) do
|
||||
tracked_characters =
|
||||
available_map_characters
|
||||
|> Enum.filter(fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
setting != nil && setting.tracked == true
|
||||
char.tracked
|
||||
end)
|
||||
|
||||
all_tracked? =
|
||||
@@ -492,7 +487,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
MapCharactersEventHandler.needs_tracking_setup?(
|
||||
only_tracked_characters,
|
||||
available_map_characters,
|
||||
character_settings,
|
||||
user_permissions
|
||||
)
|
||||
|
||||
@@ -709,6 +703,18 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
|
||||
Process.send_after(self(), %{event: :load_map_pings}, 200)
|
||||
|
||||
Process.send_after(
|
||||
self(),
|
||||
%{
|
||||
event: :maybe_select_system,
|
||||
payload: %{
|
||||
character_id: nil,
|
||||
solar_system_id: nil
|
||||
}
|
||||
},
|
||||
200
|
||||
)
|
||||
|
||||
if needs_tracking_setup do
|
||||
Process.send_after(self(), %{event: :show_tracking}, 10)
|
||||
|
||||
|
||||
@@ -44,16 +44,24 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|
||||
current_user: current_user,
|
||||
tracked_characters: tracked_characters,
|
||||
map_id: map_id,
|
||||
map_user_settings: map_user_settings
|
||||
map_user_settings: map_user_settings,
|
||||
main_character_eve_id: main_character_eve_id,
|
||||
following_character_eve_id: following_character_eve_id
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
character =
|
||||
tracked_characters
|
||||
|> Enum.find(fn tracked_character -> tracked_character.id == character_id end)
|
||||
if is_nil(character_id) do
|
||||
tracked_characters
|
||||
|> Enum.find(fn tracked_character ->
|
||||
tracked_character.eve_id == (following_character_eve_id || main_character_eve_id)
|
||||
end)
|
||||
else
|
||||
tracked_characters
|
||||
|> Enum.find(fn tracked_character -> tracked_character.id == character_id end)
|
||||
end
|
||||
|
||||
is_user_character =
|
||||
not is_nil(character)
|
||||
is_user_character = not is_nil(character)
|
||||
|
||||
is_select_on_spash =
|
||||
map_user_settings
|
||||
@@ -61,10 +69,9 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|
||||
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
|
||||
|
||||
is_following =
|
||||
case WandererApp.MapUserSettingsRepo.get(map_id, current_user.id) do
|
||||
{:ok, %{following_character_eve_id: following_character_eve_id}}
|
||||
when not is_nil(following_character_eve_id) ->
|
||||
is_user_character && following_character_eve_id == character.eve_id
|
||||
case is_user_character && not is_nil(following_character_eve_id) do
|
||||
true ->
|
||||
following_character_eve_id == character.eve_id
|
||||
|
||||
_ ->
|
||||
false
|
||||
@@ -75,26 +82,19 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|
||||
if not must_select? do
|
||||
socket
|
||||
else
|
||||
# Check if we already selected this exact system for this char:
|
||||
last_selected =
|
||||
WandererApp.Cache.lookup!(
|
||||
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
|
||||
nil
|
||||
)
|
||||
# Always select the system when auto-select is enabled (following or select_on_spash).
|
||||
# The frontend will handle deselecting other systems
|
||||
#
|
||||
select_solar_system_id =
|
||||
if not is_nil(solar_system_id) do
|
||||
"#{solar_system_id}"
|
||||
else
|
||||
{:ok, character} = WandererApp.Character.get_map_character(map_id, character.id)
|
||||
"#{character.solar_system_id}"
|
||||
end
|
||||
|
||||
if last_selected == solar_system_id do
|
||||
# same system => skip
|
||||
socket
|
||||
else
|
||||
# new system => update cache + push event
|
||||
WandererApp.Cache.put(
|
||||
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
|
||||
solar_system_id
|
||||
)
|
||||
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("select_system", solar_system_id)
|
||||
end
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("select_system", select_solar_system_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -24,24 +24,8 @@ defmodule WandererAppWeb.Presence do
|
||||
%{character_id: character_id, tracked: any_tracked, from: from}
|
||||
end)
|
||||
|
||||
presence_tracked_character_ids =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{tracked: tracked} -> tracked end)
|
||||
|> Enum.map(fn %{character_id: character_id} ->
|
||||
character_id
|
||||
end)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_character_ids",
|
||||
presence_tracked_character_ids
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_data",
|
||||
presence_data
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
# Delegate all cache operations to the PresenceGracePeriodManager
|
||||
WandererAppWeb.PresenceGracePeriodManager.process_presence_change(map_id, presence_data)
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
233
lib/wanderer_app_web/presence_grace_period_manager.ex
Normal file
233
lib/wanderer_app_web/presence_grace_period_manager.ex
Normal file
@@ -0,0 +1,233 @@
|
||||
defmodule WandererAppWeb.PresenceGracePeriodManager do
|
||||
@moduledoc """
|
||||
Manages grace period for character presence tracking.
|
||||
|
||||
This module prevents rapid start/stop cycles of character tracking
|
||||
by introducing a 5-minute grace period before stopping tracking
|
||||
for characters that leave presence.
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
# 30 minutes
|
||||
@grace_period_ms :timer.minutes(30)
|
||||
@check_remove_queue_interval :timer.seconds(30)
|
||||
|
||||
defstruct pending_removals: %{}, timers: %{}, to_remove: []
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Process presence changes with grace period logic.
|
||||
|
||||
Updates the cache with the final list of character IDs that should be tracked,
|
||||
accounting for the grace period.
|
||||
"""
|
||||
def process_presence_change(map_id, presence_data) do
|
||||
GenServer.cast(__MODULE__, {:process_presence_change, map_id, presence_data})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
Logger.info("#{__MODULE__} started")
|
||||
Process.send_after(self(), :check_remove_queue, @check_remove_queue_interval)
|
||||
|
||||
{:ok, %__MODULE__{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:process_presence_change, map_id, presence_data}, state) do
|
||||
# Extract currently tracked character IDs from presence data
|
||||
current_tracked_character_ids =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{tracked: tracked} -> tracked end)
|
||||
|> Enum.map(fn %{character_id: character_id} -> character_id end)
|
||||
|
||||
# Get previous tracked character IDs from cache
|
||||
previous_tracked_character_ids = get_previous_character_ids(map_id)
|
||||
|
||||
current_set = MapSet.new(current_tracked_character_ids)
|
||||
previous_set = MapSet.new(previous_tracked_character_ids)
|
||||
|
||||
# Characters that just joined (not in previous, but in current)
|
||||
newly_joined = MapSet.difference(current_set, previous_set)
|
||||
|
||||
# Characters that just left (in previous, but not in current)
|
||||
newly_left = MapSet.difference(previous_set, current_set)
|
||||
|
||||
# Process newly joined characters - cancel any pending removals
|
||||
state =
|
||||
state
|
||||
|> cancel_pending_removals(map_id, current_set)
|
||||
|> schedule_removals(map_id, newly_left)
|
||||
|
||||
# Process newly left characters - schedule them for removal after grace period
|
||||
# Calculate the final character IDs (current + still pending removal)
|
||||
pending_for_map = get_pending_removals_for_map(state, map_id)
|
||||
|
||||
final_character_ids = MapSet.union(current_set, pending_for_map) |> MapSet.to_list()
|
||||
|
||||
# Update cache with final character IDs (includes grace period logic)
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_character_ids", final_character_ids)
|
||||
|
||||
# Only update presence_data if the character IDs actually changed
|
||||
if final_character_ids != previous_tracked_character_ids do
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_data", presence_data)
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:grace_period_expired, map_id, character_id}, state) do
|
||||
Logger.debug(fn -> "Grace period expired for character #{character_id} on map #{map_id}" end)
|
||||
|
||||
# Remove from pending removals and timers
|
||||
state =
|
||||
state
|
||||
|> remove_pending_removal(map_id, character_id)
|
||||
|> remove_after_grace_period(map_id, character_id)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:check_remove_queue, state) do
|
||||
Process.send_after(self(), :check_remove_queue, @check_remove_queue_interval)
|
||||
|
||||
remove_from_cache_after_grace_period(state)
|
||||
{:noreply, %{state | to_remove: []}}
|
||||
end
|
||||
|
||||
defp cancel_pending_removals(state, map_id, character_ids) do
|
||||
Enum.reduce(character_ids, state, fn character_id, acc_state ->
|
||||
case get_timer_ref(acc_state, map_id, character_id) do
|
||||
nil ->
|
||||
acc_state
|
||||
|
||||
timer_ref ->
|
||||
Logger.debug(fn ->
|
||||
"Cancelling grace period for character #{character_id} on map #{map_id} (rejoined)"
|
||||
end)
|
||||
|
||||
Process.cancel_timer(timer_ref)
|
||||
remove_pending_removal(acc_state, map_id, character_id)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp schedule_removals(state, map_id, character_ids) do
|
||||
Enum.reduce(character_ids, state, fn character_id, acc_state ->
|
||||
# Only schedule if not already pending
|
||||
case get_timer_ref(acc_state, map_id, character_id) do
|
||||
nil ->
|
||||
Logger.debug(fn ->
|
||||
"Scheduling grace period for character #{character_id} on map #{map_id}"
|
||||
end)
|
||||
|
||||
timer_ref =
|
||||
Process.send_after(
|
||||
self(),
|
||||
{:grace_period_expired, map_id, character_id},
|
||||
@grace_period_ms
|
||||
)
|
||||
|
||||
add_pending_removal(acc_state, map_id, character_id, timer_ref)
|
||||
|
||||
_ ->
|
||||
acc_state
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp add_pending_removal(state, map_id, character_id, timer_ref) do
|
||||
pending_key = {map_id, character_id}
|
||||
|
||||
%{
|
||||
state
|
||||
| pending_removals: Map.put(state.pending_removals, pending_key, true),
|
||||
timers: Map.put(state.timers, pending_key, timer_ref)
|
||||
}
|
||||
end
|
||||
|
||||
defp remove_pending_removal(state, map_id, character_id) do
|
||||
pending_key = {map_id, character_id}
|
||||
|
||||
%{
|
||||
state
|
||||
| pending_removals: Map.delete(state.pending_removals, pending_key),
|
||||
timers: Map.delete(state.timers, pending_key)
|
||||
}
|
||||
end
|
||||
|
||||
defp get_timer_ref(state, map_id, character_id) do
|
||||
Map.get(state.timers, {map_id, character_id})
|
||||
end
|
||||
|
||||
defp get_previous_character_ids(map_id) do
|
||||
case WandererApp.Cache.get("map_#{map_id}:presence_character_ids") do
|
||||
nil -> []
|
||||
character_ids -> character_ids
|
||||
end
|
||||
end
|
||||
|
||||
defp get_pending_removals_for_map(state, map_id) do
|
||||
state.pending_removals
|
||||
|> Enum.filter(fn {{pending_map_id, _character_id}, _} -> pending_map_id == map_id end)
|
||||
|> Enum.map(fn {{_map_id, character_id}, _} -> character_id end)
|
||||
|> MapSet.new()
|
||||
end
|
||||
|
||||
defp remove_after_grace_period(%{to_remove: to_remove} = state, map_id, character_id_to_remove) do
|
||||
%{
|
||||
state
|
||||
| to_remove:
|
||||
(to_remove ++ [{map_id, character_id_to_remove}])
|
||||
|> Enum.uniq_by(fn {map_id, character_id} -> map_id <> character_id end)
|
||||
}
|
||||
end
|
||||
|
||||
defp remove_from_cache_after_grace_period(%{to_remove: to_remove} = state) do
|
||||
# Get current presence data to recalculate without the expired character
|
||||
to_remove
|
||||
|> Enum.each(fn {map_id, character_id_to_remove} ->
|
||||
case WandererApp.Cache.get("map_#{map_id}:presence_data") do
|
||||
nil ->
|
||||
:ok
|
||||
|
||||
presence_data ->
|
||||
# Recalculate tracked character IDs from current presence data
|
||||
updated_presence_data =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{character_id: character_id} ->
|
||||
character_id != character_id_to_remove
|
||||
end)
|
||||
|
||||
presence_tracked_character_ids =
|
||||
updated_presence_data
|
||||
|> Enum.filter(fn %{tracked: tracked} ->
|
||||
tracked
|
||||
end)
|
||||
|> Enum.map(fn %{character_id: character_id} -> character_id end)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_data", updated_presence_data)
|
||||
# Update both caches
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_character_ids",
|
||||
presence_tracked_character_ids
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"Updated cache after grace period for map #{map_id}, tracked characters: #{inspect(presence_tracked_character_ids)}"
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.77.1"
|
||||
@version "1.77.9"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
@@ -100,7 +100,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "creates a new signature with valid parameters", %{conn: conn, map: map} do
|
||||
signature_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_142,
|
||||
"eve_id" => "ABC-123",
|
||||
"character_eve_id" => "123456789",
|
||||
"name" => "Test Signature",
|
||||
@@ -132,7 +132,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "handles signature creation with minimal required fields", %{conn: conn, map: map} do
|
||||
minimal_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_143,
|
||||
"eve_id" => "XYZ-456",
|
||||
"character_eve_id" => "987654321"
|
||||
}
|
||||
@@ -152,7 +152,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "handles signature creation with all optional fields", %{conn: conn, map: map} do
|
||||
complete_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_144,
|
||||
"eve_id" => "DEF-789",
|
||||
"character_eve_id" => "456789123",
|
||||
"name" => "Complete Signature",
|
||||
@@ -181,7 +181,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
map = Factory.insert(:map)
|
||||
|
||||
signature_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_145,
|
||||
"eve_id" => "ABC-123",
|
||||
"character_eve_id" => "123456789"
|
||||
}
|
||||
@@ -392,11 +392,11 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "validates signature creation with invalid data types", %{conn: conn, map: map} do
|
||||
invalid_params = [
|
||||
%{"system_id" => "not-a-uuid", "eve_id" => "ABC", "character_eve_id" => "123"},
|
||||
%{"system_id" => Ecto.UUID.generate(), "eve_id" => 123, "character_eve_id" => "123"},
|
||||
%{"system_id" => Ecto.UUID.generate(), "eve_id" => "ABC", "character_eve_id" => 123},
|
||||
%{"solar_system_id" => "not-an-integer", "eve_id" => "ABC", "character_eve_id" => "123"},
|
||||
%{"solar_system_id" => 30_000_142, "eve_id" => 123, "character_eve_id" => "123"},
|
||||
%{"solar_system_id" => 30_000_142, "eve_id" => "ABC", "character_eve_id" => 123},
|
||||
%{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_142,
|
||||
"eve_id" => "ABC",
|
||||
"character_eve_id" => "123",
|
||||
"linked_system_id" => "not-an-integer"
|
||||
@@ -426,7 +426,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
long_string = String.duplicate("a", 1000)
|
||||
|
||||
long_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_146,
|
||||
"eve_id" => "LONG-123",
|
||||
"character_eve_id" => "123456789",
|
||||
"name" => long_string,
|
||||
@@ -448,7 +448,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "handles special characters in signature data", %{conn: conn, map: map} do
|
||||
special_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_147,
|
||||
"eve_id" => "ABC-123",
|
||||
"character_eve_id" => "123456789",
|
||||
"name" => "Special chars: àáâãäåæçèéêë",
|
||||
@@ -470,7 +470,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "handles empty string values", %{conn: conn, map: map} do
|
||||
empty_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_148,
|
||||
"eve_id" => "",
|
||||
"character_eve_id" => "",
|
||||
"name" => "",
|
||||
@@ -537,7 +537,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
if length(data) > 0 do
|
||||
signature = List.first(data)
|
||||
assert Map.has_key?(signature, "id")
|
||||
assert Map.has_key?(signature, "system_id")
|
||||
assert Map.has_key?(signature, "solar_system_id")
|
||||
assert Map.has_key?(signature, "eve_id")
|
||||
assert Map.has_key?(signature, "character_eve_id")
|
||||
end
|
||||
@@ -564,7 +564,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "created signature response structure", %{conn: conn, map: map} do
|
||||
signature_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_149,
|
||||
"eve_id" => "TEST-001",
|
||||
"character_eve_id" => "123456789",
|
||||
"name" => "Test Signature"
|
||||
@@ -582,7 +582,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
case response do
|
||||
%{"data" => data} ->
|
||||
# Should have signature structure
|
||||
assert Map.has_key?(data, "id") or Map.has_key?(data, "system_id")
|
||||
assert Map.has_key?(data, "id") or Map.has_key?(data, "solar_system_id")
|
||||
|
||||
%{"error" => _error} ->
|
||||
# Error response is also valid
|
||||
|
||||
@@ -8,7 +8,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
describe "parameter validation" do
|
||||
test "validates missing connection assigns for create_signature" do
|
||||
conn = %{assigns: %{}}
|
||||
params = %{"solar_system_id" => "30000142"}
|
||||
params = %{"solar_system_id" => 30_000_142}
|
||||
|
||||
result = Signatures.create_signature(conn, params)
|
||||
assert {:error, :missing_params} = result
|
||||
@@ -209,7 +209,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
}
|
||||
|
||||
# Test with minimal required parameters
|
||||
params = %{"solar_system_id" => "30000142"}
|
||||
params = %{"solar_system_id" => 30_000_142}
|
||||
|
||||
MapTestHelpers.expect_map_server_error(fn ->
|
||||
result = Signatures.create_signature(conn, params)
|
||||
@@ -416,7 +416,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
nil
|
||||
]
|
||||
|
||||
params = %{"solar_system_id" => "30000142"}
|
||||
params = %{"solar_system_id" => 30_000_142}
|
||||
|
||||
Enum.each(malformed_conns, fn conn ->
|
||||
# This should either crash (expected) or return error
|
||||
@@ -462,17 +462,16 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
tasks =
|
||||
Enum.map(1..3, fn i ->
|
||||
Task.async(fn ->
|
||||
MapTestHelpers.expect_map_server_error(fn ->
|
||||
params = %{"solar_system_id" => "3000014#{i}"}
|
||||
Signatures.create_signature(conn, params)
|
||||
end)
|
||||
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
|
||||
end)
|
||||
end)
|
||||
|
||||
# All tasks should complete without crashing
|
||||
Enum.each(tasks, fn task ->
|
||||
assert Task.await(task) == :ok
|
||||
end)
|
||||
Enum.each(tasks, &Task.await/1)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -669,7 +668,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
}
|
||||
]
|
||||
|
||||
params = %{"solar_system_id" => "30000142"}
|
||||
params = %{"solar_system_id" => 30_000_142}
|
||||
|
||||
Enum.each(assign_variations, fn assigns ->
|
||||
conn = %{assigns: assigns}
|
||||
|
||||
Reference in New Issue
Block a user