Compare commits

..

66 Commits

Author SHA1 Message Date
Dmitry Popov
27b5694885 chore: release version v1.66.15 2025-06-08 11:03:13 +02:00
Dmitry Popov
4093f28cee chore: release version v1.66.15 2025-06-08 10:45:41 +02:00
Dmitry Popov
08aaf2f2dd chore: release version v1.66.15 2025-06-08 10:19:25 +02:00
Dmitry Popov
df955ff8b0 chore: release version v1.66.15 2025-06-07 13:57:25 +02:00
CI
4dc74022b4 chore: release version v1.66.15 2025-06-07 09:01:44 +00:00
Dmitry Popov
c2b7d07208 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 10:58:35 +02:00
Dmitry Popov
21e76a6f05 fix(Core): Added back arm docker image build 2025-06-07 10:58:24 +02:00
CI
89c0d6fad6 chore: release version v1.66.14 2025-06-07 08:48:05 +00:00
Dmitry Popov
b74d15b1ee Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 10:47:31 +02:00
Dmitry Popov
10313438bf fix(Core): fixed online updates 2025-06-07 10:47:28 +02:00
CI
a764217948 chore: release version v1.66.13 2025-06-07 07:49:31 +00:00
Dmitry Popov
d81d6fb937 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 09:48:56 +02:00
Dmitry Popov
4c0f7ab7f9 fix(Core): fixed location tracking issues 2025-06-07 09:48:53 +02:00
CI
1c48945a96 chore: release version v1.66.12
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-06 23:58:04 +00:00
Dmitry Popov
850901f62f Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 01:57:37 +02:00
Dmitry Popov
4822854e30 chore: release version v1.66.10 2025-06-07 01:57:32 +02:00
CI
f580538331 chore: release version v1.66.11 2025-06-06 23:47:37 +00:00
Dmitry Popov
0d70c555e6 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 01:47:07 +02:00
Dmitry Popov
c5f6cf0080 fix(Core): fixed refresh character tokens 2025-06-07 01:47:03 +02:00
CI
6ff7b3bc9a chore: release version v1.66.10
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-06 22:37:51 +00:00
Dmitry Popov
346c2c65b0 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 00:37:23 +02:00
Dmitry Popov
a6445fd500 chore: release version v1.66.8 2025-06-07 00:37:20 +02:00
CI
358e43e508 chore: release version v1.66.9 2025-06-06 22:23:05 +00:00
Dmitry Popov
20c5ba6b63 chore: release version v1.66.8 2025-06-07 00:22:35 +02:00
CI
661658a6e8 chore: release version v1.66.8 2025-06-06 21:32:10 +00:00
Dmitry Popov
0a6f224ed3 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-06 23:31:41 +02:00
Dmitry Popov
e7bb29693f fix: fixed disable detailed kills env check 2025-06-06 23:31:38 +02:00
CI
32dfd50461 chore: release version v1.66.7 2025-06-06 20:34:16 +00:00
Dmitry Popov
bfec385dce Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-06 22:33:49 +02:00
Dmitry Popov
04278f99d7 fix: fixed disable detailed kills env check 2025-06-06 22:33:46 +02:00
CI
9d3db19dc1 chore: release version v1.66.6 2025-06-06 20:03:06 +00:00
Dmitry Popov
3953e33f37 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-06 22:02:34 +02:00
Dmitry Popov
611fdd56d0 fix: respect error limits for ESI APIs 2025-06-06 22:02:31 +02:00
CI
36a4fd0f35 chore: release version v1.66.5 2025-06-06 17:13:49 +00:00
Dmitry Popov
488984a988 fix: respect error limits for ESI APIs 2025-06-06 19:13:07 +02:00
CI
1b183d6e58 chore: release version v1.66.4
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-06 12:52:02 +00:00
Dmitry Popov
3dcb6d30b5 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-06 14:51:27 +02:00
Dmitry Popov
1d11788f89 fix: respect error limits for ESI APIs 2025-06-06 14:51:24 +02:00
CI
d3822128ab chore: release version v1.66.3
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-05 22:58:55 +00:00
Dmitry Popov
6ac7836505 chore: release version v1.66.2 2025-06-06 00:58:24 +02:00
CI
5796fccae4 chore: release version v1.66.2 2025-06-05 18:53:11 +00:00
Dmitry Popov
22ab6e544c chore: release version v1.66.1 2025-06-05 20:43:28 +02:00
Dmitry Popov
5e735ab8bd chore: release version v1.66.1 2025-06-05 20:27:05 +02:00
Dmitry Popov
450139402d chore: release version v1.66.1 2025-06-05 20:26:49 +02:00
CI
1e0d4f1fde chore: release version v1.66.1 2025-06-05 15:28:12 +00:00
Dmitry Popov
7e9b1af3a3 Merge pull request #420 from guarzo/guarzo/sigfx
fix: remove bugs with signature deletion
2025-06-05 19:27:46 +04:00
Guarzo
30b90cd4be fix: remove bugs with signature deletion 2025-06-05 10:15:52 -04:00
CI
f28affa222 chore: release version v1.66.0
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-05 09:23:21 +00:00
Dmitry Popov
2ae7b187b8 Merge pull request #418 from wanderer-industries/develop
Develop
2025-06-05 13:22:54 +04:00
Dmitry Popov
ad037be1f4 Merge branch 'main' into develop 2025-06-05 11:09:19 +02:00
CI
9e31542c5f chore: release version v1.65.24
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-04 18:57:47 +00:00
Dmitry Popov
aae6ed0cb3 fix(Core): Fixed errors duration check to reduce requests amount to ESI 2025-06-04 20:57:17 +02:00
Dmitry Popov
7ea2ecb8e9 Merge pull request #417 from guarzo/guarzo/deleting
feat: show deleted signatures during undo timer
2025-06-04 22:30:59 +04:00
guarzo
852fc28896 Merge branch 'develop' into guarzo/deleting 2025-06-04 13:04:12 -04:00
Guarzo
fcdab79802 fix: remove callbacks from effect dependencies 2025-06-04 13:04:00 -04:00
CI
bb0e06589f chore: release version v1.65.23 2025-06-04 15:53:50 +00:00
Dmitry Popov
d65b72c4dd fix(Core): Added back arm docker image build 2025-06-04 17:53:21 +02:00
CI
24a3d5b3de chore: release version v1.65.22 2025-06-04 15:44:34 +00:00
Dmitry Popov
2814b46941 fix(Core): Fix character tracking issues 2025-06-04 17:43:53 +02:00
Guarzo
6ffc25448d feat: show deleted signatures during undo timer 2025-06-03 21:57:04 -04:00
CI
d9a82f7c9f chore: release version v1.65.21
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-01 19:00:52 +00:00
Dmitry Popov
9a8106947e Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-01 21:00:23 +02:00
Dmitry Popov
e21ca79ea1 fix(Core): Fix connection pool errors 2025-06-01 21:00:14 +02:00
CI
dd579caeac chore: release version v1.65.20 2025-06-01 17:56:46 +00:00
Dmitry Popov
ddf8a4b9fb Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-01 19:56:18 +02:00
Dmitry Popov
3c04f4194e fix(Core): fix waypoint set timeout errors 2025-06-01 19:56:14 +02:00
33 changed files with 1668 additions and 758 deletions

View File

@@ -2,6 +2,179 @@
<!-- changelog -->
## [v1.66.15](https://github.com/wanderer-industries/wanderer/compare/v1.66.14...v1.66.15) (2025-06-07)
### Bug Fixes:
* Core: Added back arm docker image build
## [v1.66.14](https://github.com/wanderer-industries/wanderer/compare/v1.66.13...v1.66.14) (2025-06-07)
### Bug Fixes:
* Core: fixed online updates
## [v1.66.13](https://github.com/wanderer-industries/wanderer/compare/v1.66.12...v1.66.13) (2025-06-07)
### Bug Fixes:
* Core: fixed location tracking issues
## [v1.66.12](https://github.com/wanderer-industries/wanderer/compare/v1.66.11...v1.66.12) (2025-06-06)
## [v1.66.11](https://github.com/wanderer-industries/wanderer/compare/v1.66.10...v1.66.11) (2025-06-06)
### Bug Fixes:
* Core: fixed refresh character tokens
## [v1.66.10](https://github.com/wanderer-industries/wanderer/compare/v1.66.9...v1.66.10) (2025-06-06)
## [v1.66.9](https://github.com/wanderer-industries/wanderer/compare/v1.66.8...v1.66.9) (2025-06-06)
## [v1.66.8](https://github.com/wanderer-industries/wanderer/compare/v1.66.7...v1.66.8) (2025-06-06)
### Bug Fixes:
* fixed disable detailed kills env check
## [v1.66.7](https://github.com/wanderer-industries/wanderer/compare/v1.66.6...v1.66.7) (2025-06-06)
### Bug Fixes:
* fixed disable detailed kills env check
## [v1.66.6](https://github.com/wanderer-industries/wanderer/compare/v1.66.5...v1.66.6) (2025-06-06)
### Bug Fixes:
* respect error limits for ESI APIs
## [v1.66.5](https://github.com/wanderer-industries/wanderer/compare/v1.66.4...v1.66.5) (2025-06-06)
### Bug Fixes:
* respect error limits for ESI APIs
## [v1.66.4](https://github.com/wanderer-industries/wanderer/compare/v1.66.3...v1.66.4) (2025-06-06)
### Bug Fixes:
* respect error limits for ESI APIs
## [v1.66.3](https://github.com/wanderer-industries/wanderer/compare/v1.66.2...v1.66.3) (2025-06-05)
## [v1.66.2](https://github.com/wanderer-industries/wanderer/compare/v1.66.1...v1.66.2) (2025-06-05)
## [v1.66.1](https://github.com/wanderer-industries/wanderer/compare/v1.66.0...v1.66.1) (2025-06-05)
### Bug Fixes:
* remove bugs with signature deletion
## [v1.66.0](https://github.com/wanderer-industries/wanderer/compare/v1.65.24...v1.66.0) (2025-06-05)
### Features:
* show deleted signatures during undo timer
### Bug Fixes:
* remove callbacks from effect dependencies
## [v1.65.24](https://github.com/wanderer-industries/wanderer/compare/v1.65.23...v1.65.24) (2025-06-04)
### Bug Fixes:
* Core: Fixed errors duration check to reduce requests amount to ESI
## [v1.65.23](https://github.com/wanderer-industries/wanderer/compare/v1.65.22...v1.65.23) (2025-06-04)
### Bug Fixes:
* Core: Added back arm docker image build
## [v1.65.22](https://github.com/wanderer-industries/wanderer/compare/v1.65.21...v1.65.22) (2025-06-04)
### Bug Fixes:
* Core: Fix character tracking issues
## [v1.65.21](https://github.com/wanderer-industries/wanderer/compare/v1.65.20...v1.65.21) (2025-06-01)
### Bug Fixes:
* Core: Fix connection pool errors
## [v1.65.20](https://github.com/wanderer-industries/wanderer/compare/v1.65.19...v1.65.20) (2025-06-01)
### Bug Fixes:
* Core: fix waypoint set timeout errors
## [v1.65.19](https://github.com/wanderer-industries/wanderer/compare/v1.65.18...v1.65.19) (2025-06-01)

View File

@@ -1,14 +1,13 @@
import { useCallback } from 'react';
import clsx from 'clsx';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import classes from './Characters.module.scss';
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { useCallback } from 'react';
import classes from './Characters.module.scss';
interface CharactersProps {
data: CharacterTypeRaw[];
}
@@ -17,13 +16,22 @@ export const Characters = ({ data }: CharactersProps) => {
const [parent] = useAutoAnimate();
const {
outCommand,
data: { mainCharacterEveId, followingCharacterEveId },
} = useMapRootState();
const handleSelect = useCallback((character: CharacterTypeRaw) => {
const handleSelect = useCallback(async (character: CharacterTypeRaw) => {
if (!character) {
return;
}
await outCommand({
type: OutCommand.startTracking,
data: { character_eve_id: character.eve_id },
});
emitMapEvent({
name: Commands.centerSystem,
data: character?.location?.solar_system_id?.toString(),
data: character.location?.solar_system_id?.toString(),
});
}, []);
@@ -37,14 +45,26 @@ export const Characters = ({ data }: CharactersProps) => {
className={clsx(
'overflow-hidden relative',
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
'transition-colors duration-250',
'transition-colors duration-250 hover:bg-stone-300/90',
{
['border-stone-800/90']: !character.online,
['border-lime-600/70']: character.online,
},
)}
title={character.name}
title={character.tracking_paused ? `${character.name} - Tracking Paused (click to resume)` : character.name}
>
{character.tracking_paused && (
<>
<span
className={clsx(
'absolute top-[2px] left-[2px] w-[9px] h-[9px]',
'text-yellow-500 text-[9px] rounded-[1px] z-10 hover:hidden',
'pi',
PrimeIcons.PAUSE,
)}
/>
</>
)}
{mainCharacterEveId === character.eve_id && (
<span
className={clsx(
@@ -55,6 +75,7 @@ export const Characters = ({ data }: CharactersProps) => {
)}
/>
)}
{followingCharacterEveId === character.eve_id && (
<span
className={clsx(

View File

@@ -1,5 +1,4 @@
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useCallback, useRef } from 'react';
import {
CommandCharacterAdded,
CommandCharacterRemoved,
@@ -7,6 +6,7 @@ import {
CommandCharacterUpdated,
CommandPresentCharacters,
} from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
export const useCommandsCharacters = () => {
const { update } = useMapState();

View File

@@ -1,8 +1,8 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { convertConnection2Edge, convertSystem2Node } from '../../helpers';
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useCallback, useRef } from 'react';
import { useReactFlow } from 'reactflow';
import { convertConnection2Edge, convertSystem2Node } from '../../helpers';
export const useMapInit = () => {
const rf = useReactFlow();

View File

@@ -12,10 +12,10 @@ import {
SIGNATURE_SETTING_STORE_KEY,
SIGNATURE_WINDOW_ID,
SignatureSettingsType,
SIGNATURES_DELETION_TIMING,
SIGNATURE_DELETION_TIMEOUTS,
getDeletionTimeoutMs,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
/**
* Custom hook for managing pending signature deletions and undo countdown.
@@ -27,16 +27,32 @@ function useSignatureUndo(
) {
const [countdown, setCountdown] = useState<number>(0);
const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
const [deletedSignatures, setDeletedSignatures] = useState<ExtendedSystemSignature[]>([]);
const intervalRef = useRef<number | null>(null);
const addDeleted = useCallback((ids: string[]) => {
const addDeleted = useCallback((signatures: ExtendedSystemSignature[]) => {
const newIds = signatures.map(sig => sig.eve_id);
setPendingIds(prev => {
const next = new Set(prev);
ids.forEach(id => next.add(id));
newIds.forEach(id => next.add(id));
return next;
});
setDeletedSignatures(prev => [...prev, ...signatures]);
}, []);
// Clear deleted signatures when system changes
useEffect(() => {
if (systemId) {
setDeletedSignatures([]);
setPendingIds(new Set());
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
}, [systemId]);
// kick off or clear countdown whenever pendingIds changes
useEffect(() => {
// clear any existing timer
@@ -47,13 +63,13 @@ function useSignatureUndo(
if (pendingIds.size === 0) {
setCountdown(0);
setDeletedSignatures([]);
return;
}
// determine timeout from settings
const timingKey = Number(settings[SETTINGS_KEYS.DELETION_TIMING] ?? SIGNATURES_DELETION_TIMING.DEFAULT);
const timeoutMs =
Number(SIGNATURE_DELETION_TIMEOUTS[timingKey as keyof typeof SIGNATURE_DELETION_TIMEOUTS]) || 10000;
const timeoutMs = getDeletionTimeoutMs(settings);
setCountdown(Math.ceil(timeoutMs / 1000));
// start new interval
@@ -63,6 +79,7 @@ function useSignatureUndo(
clearInterval(intervalRef.current!);
intervalRef.current = null;
setPendingIds(new Set());
setDeletedSignatures([]);
return 0;
}
return prev - 1;
@@ -75,7 +92,7 @@ function useSignatureUndo(
intervalRef.current = null;
}
};
}, [pendingIds, settings[SETTINGS_KEYS.DELETION_TIMING]]);
}, [pendingIds, settings]);
// undo handler
const handleUndo = useCallback(async () => {
@@ -85,6 +102,7 @@ function useSignatureUndo(
data: { system_id: systemId, eve_ids: Array.from(pendingIds) },
});
setPendingIds(new Set());
setDeletedSignatures([]);
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
@@ -95,6 +113,7 @@ function useSignatureUndo(
return {
pendingIds,
countdown,
deletedSignatures,
addDeleted,
handleUndo,
};
@@ -118,7 +137,11 @@ export const SystemSignatures = () => {
const [systemId] = selectedSystems;
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
const { pendingIds, countdown, addDeleted, handleUndo } = useSignatureUndo(systemId, currentSettings, outCommand);
const { pendingIds, countdown, deletedSignatures, addDeleted, handleUndo } = useSignatureUndo(
systemId,
currentSettings,
outCommand,
);
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
if (pendingIds.size > 0 && countdown > 0) {
@@ -175,6 +198,7 @@ export const SystemSignatures = () => {
<SystemSignaturesContent
systemId={systemId}
settings={currentSettings}
deletedSignatures={deletedSignatures}
onLazyDeleteChange={handleLazyDeleteToggle}
onCountChange={handleCountChange}
onSignatureDeleted={addDeleted}

View File

@@ -58,7 +58,8 @@ interface SystemSignaturesContentProps {
onLazyDeleteChange?: (value: boolean) => void;
onCountChange?: (count: number) => void;
filterSignature?: (signature: SystemSignature) => boolean;
onSignatureDeleted?: (deletedIds: string[]) => void;
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
deletedSignatures?: ExtendedSystemSignature[];
}
export const SystemSignaturesContent = ({
@@ -71,6 +72,7 @@ export const SystemSignaturesContent = ({
onCountChange,
filterSignature,
onSignatureDeleted,
deletedSignatures = [],
}: SystemSignaturesContentProps) => {
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
@@ -126,10 +128,8 @@ export const SystemSignaturesContent = ({
event.preventDefault();
event.stopPropagation();
if (onSignatureDeleted && selectedSignatures.length > 0) {
const deletedIds = selectedSignatures.map(s => s.eve_id);
onSignatureDeleted(deletedIds);
}
// Delete key should always immediately delete, never show pending deletions
handleDeleteSelected();
});
@@ -158,9 +158,16 @@ export const SystemSignaturesContent = ({
const handleSelectSignatures = useCallback(
(e: { value: SystemSignature[] }) => {
selectable ? onSelect?.(e.value[0]) : setSelectedSignatures(e.value as ExtendedSystemSignature[]);
// Filter out deleted signatures from selection
const selectableSignatures = e.value.filter(
sig => !deletedSignatures.some(deleted => deleted.eve_id === sig.eve_id),
);
selectable
? onSelect?.(selectableSignatures[0])
: setSelectedSignatures(selectableSignatures as ExtendedSystemSignature[]);
},
[onSelect, selectable, setSelectedSignatures],
[onSelect, selectable, setSelectedSignatures, deletedSignatures],
);
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
@@ -174,7 +181,11 @@ export const SystemSignaturesContent = ({
);
const filteredSignatures = useMemo<ExtendedSystemSignature[]>(() => {
return signatures.filter(sig => {
// Get the set of deleted signature IDs for quick lookup
const deletedIds = new Set(deletedSignatures.map(sig => sig.eve_id));
// Common filter function
const shouldShowSignature = (sig: ExtendedSystemSignature): boolean => {
if (filterSignature && !filterSignature(sig)) {
return false;
}
@@ -203,9 +214,27 @@ export const SystemSignaturesContent = ({
return true;
}
return settings[sig.kind];
return settings[sig.kind] as boolean;
};
// Filter active signatures, excluding any that are in the deleted list
const activeSignatures = signatures.filter(sig => {
// Skip if this signature is in the deleted list
if (deletedIds.has(sig.eve_id)) {
return false;
}
return shouldShowSignature(sig);
});
}, [signatures, hideLinkedSignatures, settings, filterSignature]);
// Add deleted signatures with pending deletion flag, applying the same filters
const deletedWithPendingFlag = deletedSignatures.filter(shouldShowSignature).map(sig => ({
...sig,
pendingDeletion: true,
}));
return [...activeSignatures, ...deletedWithPendingFlag];
}, [signatures, hideLinkedSignatures, settings, filterSignature, deletedSignatures]);
const onRowMouseEnter = useCallback((e: DataTableRowMouseEvent) => {
setHoveredSignature(e.data as SystemSignature);
@@ -249,7 +278,8 @@ export const SystemSignaturesContent = ({
{hasUnsupportedLanguage && (
<div className="w-full flex justify-center items-center text-amber-500 text-xs p-1 bg-amber-950/20 border-b border-amber-800/30">
<i className={PrimeIcons.EXCLAMATION_TRIANGLE + ' mr-1'} />
Non-English signatures detected. Some signatures may not display correctly. Double-click to edit signature details.
Non-English signatures detected. Some signatures may not display correctly. Double-click to edit signature
details.
</div>
)}
<DataTable

View File

@@ -148,7 +148,8 @@ export enum SIGNATURES_DELETION_TIMING {
EXTENDED,
}
export type SignatureDeletionTimingType = { [key in SIGNATURES_DELETION_TIMING]?: unknown };
// Now use a stricter type: every timing key maps to a number
export type SignatureDeletionTimingType = Record<SIGNATURES_DELETION_TIMING, number>;
export const SIGNATURE_SETTINGS = {
filterFlags: [
@@ -219,12 +220,28 @@ export const SETTINGS_VALUES: SignatureSettingsType = {
[SETTINGS_KEYS.COMBAT_SITE]: true,
};
// Now this map is strongly typed as “number” for each timing enum
export const SIGNATURE_DELETION_TIMEOUTS: SignatureDeletionTimingType = {
[SIGNATURES_DELETION_TIMING.DEFAULT]: 10_000,
[SIGNATURES_DELETION_TIMING.IMMEDIATE]: 0,
[SIGNATURES_DELETION_TIMING.DEFAULT]: 10_000,
[SIGNATURES_DELETION_TIMING.EXTENDED]: 30_000,
};
/**
* Helper function to extract the deletion timeout in milliseconds from settings
*/
export function getDeletionTimeoutMs(settings: SignatureSettingsType): number {
const raw = settings[SETTINGS_KEYS.DELETION_TIMING];
const timing =
raw && typeof raw === 'object' && 'value' in raw
? (raw as { value: SIGNATURES_DELETION_TIMING }).value
: (raw as SIGNATURES_DELETION_TIMING | undefined);
const validTiming = typeof timing === 'number' ? timing : SIGNATURES_DELETION_TIMING.DEFAULT;
return SIGNATURE_DELETION_TIMEOUTS[validTiming];
}
// Replace the flat structure with a nested structure by language
export const LANGUAGE_TYPE_MAPPINGS = {
EN: {

View File

@@ -5,7 +5,10 @@ import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useEffect, useState } from 'react';
import useRefState from 'react-usestateref';
import { SETTINGS_KEYS } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import {
SETTINGS_KEYS,
getDeletionTimeoutMs,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getActualSigs } from '../helpers';
import { UseSystemSignaturesDataProps } from './types';
@@ -20,7 +23,7 @@ export const useSystemSignaturesData = ({
onLazyDeleteChange,
onSignatureDeleted,
}: Omit<UseSystemSignaturesDataProps, 'deletionTiming'> & {
onSignatureDeleted?: (deletedIds: string[]) => void;
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
}) => {
const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
@@ -74,9 +77,16 @@ export const useSystemSignaturesData = ({
if (removed.length > 0) {
await processRemovedSignatures(removed, added, updated);
if (onSignatureDeleted) {
const deletedIds = removed.map(sig => sig.eve_id);
onSignatureDeleted(deletedIds);
// Only show pending deletions if:
// 1. Lazy deletion is enabled AND
// 2. Deletion timing is not immediate (> 0)
if (onSignatureDeleted && lazyDeleteValue) {
const timeoutMs = getDeletionTimeoutMs(settings);
if (timeoutMs > 0) {
onSignatureDeleted(removed);
}
}
}
@@ -102,11 +112,18 @@ export const useSystemSignaturesData = ({
const handleDeleteSelected = useCallback(async () => {
if (!selectedSignatures.length) return;
const selectedIds = selectedSignatures.map(s => s.eve_id);
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
// IMPORTANT: Send deletion to server BEFORE updating local state
// Otherwise signaturesRef.current will be updated and getActualSigs won't detect removals
await handleUpdateSignatures(finalList, false, true);
// Update local state after server call
setSignatures(finalList);
setSelectedSignatures([]);
}, [handleUpdateSignatures, selectedSignatures, signatures]);
}, [handleUpdateSignatures, selectedSignatures, signatures, setSignatures]);
const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures);

View File

@@ -1,14 +1,13 @@
import { Characters } from '../characters/Characters';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo } from 'react';
import clsx from 'clsx';
import { sortOnlineFunc } from '@/hooks/Mapper/components/hooks/useGetOwnOnlineCharacters.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
import { Button } from 'primereact/button';
import clsx from 'clsx';
import { useMemo } from 'react';
import { Characters } from '../characters/Characters';
const Topbar = ({ children }: WithChildren) => {
const {
data: { characters, userCharacters, pings },
data: { characters, userCharacters },
} = useMapRootState();
const charsToShow = useMemo(() => {

View File

@@ -33,6 +33,7 @@ export type CharacterTypeRaw = {
corporation_id: number;
corporation_name: string;
corporation_ticker: string;
tracking_paused: boolean;
};
export interface TrackingCharacter {

View File

@@ -1,10 +1,10 @@
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types/system.ts';
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Mapper/types/character.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
import { CommentType, PingData, SystemSignature, UserPermissions } from '@/hooks/Mapper/types';
import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Mapper/types/character.ts';
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types/system.ts';
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
export enum Commands {
init = 'init',
@@ -260,6 +260,7 @@ export enum OutCommand {
updateMainCharacter = 'updateMainCharacter',
addPing = 'add_ping',
cancelPing = 'cancel_ping',
startTracking = 'startTracking',
// Only UI commands
openSettings = 'open_settings',

View File

@@ -53,16 +53,6 @@ public_api_disabled =
|> get_var_from_path_or_env("WANDERER_PUBLIC_API_DISABLED", "false")
|> String.to_existing_atom()
character_api_disabled =
config_dir
|> get_var_from_path_or_env("WANDERER_CHARACTER_API_DISABLED", "true")
|> String.to_existing_atom()
zkill_preload_disabled =
config_dir
|> get_var_from_path_or_env("WANDERER_ZKILL_PRELOAD_DISABLED", "false")
|> String.to_existing_atom()
map_subscriptions_enabled =
config_dir
|> get_var_from_path_or_env("WANDERER_MAP_SUBSCRIPTIONS_ENABLED", "false")
@@ -128,8 +118,10 @@ config :wanderer_app,
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
public_api_disabled: public_api_disabled,
character_api_disabled: character_api_disabled,
zkill_preload_disabled: zkill_preload_disabled,
character_api_disabled:
System.get_env("WANDERER_CHARACTER_API_DISABLED", "true") |> String.to_existing_atom(),
zkill_preload_disabled:
System.get_env("WANDERER_ZKILL_PRELOAD_DISABLED", "false") |> String.to_existing_atom(),
map_subscriptions_enabled: map_subscriptions_enabled,
map_connection_auto_expire_hours: map_connection_auto_expire_hours,
map_connection_auto_eol_hours: map_connection_auto_eol_hours,

View File

@@ -20,9 +20,9 @@ defmodule WandererApp.Application do
pools: %{
default: [
# number of connections per pool
size: 25,
size: 50,
# number of pools (so total 50 connections)
count: 2
count: 4
]
}
},

View File

@@ -54,18 +54,26 @@ defmodule WandererApp.Character do
end
end
def get_map_character(map_id, character_id) do
def get_map_character(map_id, character_id, opts \\ []) do
case get_character(character_id) do
{:ok, character} ->
# If we are forcing the character to not be present, we merge the character state with map settings
character_is_present =
if opts |> Keyword.get(:not_present, false) do
false
else
WandererApp.Character.TrackerManager.Impl.character_is_present(map_id, character_id)
end
{:ok,
character
|> maybe_merge_map_character_settings(
map_id,
WandererApp.Character.TrackerManager.Impl.character_is_present(map_id, character_id)
character_is_present
)}
_ ->
{:ok, nil}
error ->
error
end
end
@@ -183,7 +191,7 @@ defmodule WandererApp.Character do
{:ok, result} ->
{:ok, result |> prepare_search_results()}
{:error, error} ->
error ->
Logger.warning("#{__MODULE__} failed search: #{inspect(error)}")
{:ok, []}
end
@@ -243,13 +251,22 @@ defmodule WandererApp.Character do
end
end
defp maybe_merge_map_character_settings(character, map_id, true), do: character
defp maybe_merge_map_character_settings(%{id: character_id} = character, map_id, true) do
{:ok, tracking_paused} =
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)
character
|> Map.merge(%{tracking_paused: tracking_paused})
end
defp maybe_merge_map_character_settings(
%{id: character_id} = character,
map_id,
_character_is_present
) do
{:ok, tracking_paused} =
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)
WandererApp.MapCharacterSettingsRepo.get(map_id, character_id)
|> case do
{:ok, settings} when not is_nil(settings) ->
@@ -262,6 +279,7 @@ defmodule WandererApp.Character do
|> Map.put(:online, false)
|> Map.merge(@default_character_tracking_data)
end
|> Map.merge(%{tracking_paused: tracking_paused})
end
defp prepare_search_results(result) do

View File

@@ -33,8 +33,16 @@ defmodule WandererApp.Character.Tracker do
status: binary()
}
@online_error_timeout :timer.minutes(3)
@forbidden_ttl :timer.minutes(1)
@pause_tracking_timeout :timer.minutes(60 * 24)
@offline_timeout :timer.minutes(5)
@online_error_timeout :timer.minutes(2)
@ship_error_timeout :timer.minutes(2)
@location_error_timeout :timer.minutes(2)
@online_forbidden_ttl :timer.seconds(7)
@online_limit_ttl :timer.seconds(7)
@forbidden_ttl :timer.seconds(5)
@limit_ttl :timer.seconds(5)
@location_limit_ttl :timer.seconds(1)
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
def new(), do: __struct__()
@@ -49,6 +57,90 @@ defmodule WandererApp.Character.Tracker do
|> new()
end
def check_offline(character_id) do
WandererApp.Cache.lookup!("character:#{character_id}:last_online_time")
|> case do
nil ->
pause_tracking(character_id)
:ok
last_online_time ->
duration = DateTime.diff(DateTime.utc_now(), last_online_time, :millisecond)
if duration >= @offline_timeout do
pause_tracking(character_id)
:ok
else
:skip
end
end
end
def check_online_errors(character_id),
do: check_tracking_errors(character_id, "online", @online_error_timeout)
def check_ship_errors(character_id),
do: check_tracking_errors(character_id, "ship", @ship_error_timeout)
def check_location_errors(character_id),
do: check_tracking_errors(character_id, "location", @location_error_timeout)
defp check_tracking_errors(character_id, type, timeout) do
WandererApp.Cache.lookup!("character:#{character_id}:#{type}_error_time")
|> case do
nil ->
:skip
error_time ->
duration = DateTime.diff(DateTime.utc_now(), error_time, :millisecond)
if duration >= timeout do
pause_tracking(character_id)
:ok
else
:skip
end
end
end
defp pause_tracking(character_id) do
if not WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused") do
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
Logger.warning("[CharacterTracker] paused for #{character_id}")
WandererApp.Cache.put(
"character:#{character_id}:tracking_paused",
true,
ttl: @pause_tracking_timeout
)
{:ok, %{solar_system_id: solar_system_id}} =
WandererApp.Character.get_character(character_id)
{:ok, %{active_maps: active_maps}} =
WandererApp.Character.get_character_state(character_id)
active_maps
|> Enum.each(fn map_id ->
WandererApp.Cache.put(
"map:#{map_id}:character:#{character_id}:start_solar_system_id",
solar_system_id
)
end)
end
end
def update_settings(character_id, track_settings) do
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
@@ -61,8 +153,142 @@ defmodule WandererApp.Character.Tracker do
|> maybe_start_ship_tracking(track_settings)}
end
def update_online(character_id) when is_binary(character_id),
do:
character_id
|> WandererApp.Character.get_character_state!()
|> update_online()
def update_online(%{track_online: true, character_id: character_id} = character_state) do
case WandererApp.Character.get_character(character_id) do
{:ok, %{eve_id: eve_id, access_token: access_token}}
when not is_nil(access_token) ->
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
true ->
{:error, :skipped}
_ ->
case WandererApp.Esi.get_character_online(eve_id,
access_token: access_token,
character_id: character_id
) do
{:ok, online} ->
online = get_online(online)
if online.online == true do
WandererApp.Cache.insert(
"character:#{character_id}:last_online_time",
DateTime.utc_now()
)
else
WandererApp.Cache.delete("character:#{character_id}:last_online_time")
end
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
WandererApp.Cache.delete("character:#{character_id}:info_forbidden")
WandererApp.Cache.delete("character:#{character_id}:ship_forbidden")
WandererApp.Cache.delete("character:#{character_id}:location_forbidden")
WandererApp.Cache.delete("character:#{character_id}:wallet_forbidden")
WandererApp.Character.update_character(character_id, online)
update = %{
character_state
| is_online: online.online,
track_ship: online.online,
track_location: online.online
}
WandererApp.Character.update_character_state(character_id, update)
:ok
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
Logger.warning("#{__MODULE__} failed to update_online: #{inspect(error)}")
WandererApp.Cache.put(
"character:#{character_id}:online_forbidden",
true,
ttl: @online_forbidden_ttl
)
if is_nil(
WandererApp.Cache.lookup!("character:#{character_id}:online_error_time")
) do
WandererApp.Cache.insert(
"character:#{character_id}:online_error_time",
DateTime.utc_now()
)
end
{:error, :skipped}
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
Logger.warning(
"#{__MODULE__} failed to update_online: #{inspect(:error_limited)}"
)
WandererApp.Cache.put(
"character:#{character_id}:online_forbidden",
true,
ttl: reset_timeout
)
{:error, :skipped}
{:error, error} ->
Logger.error("#{__MODULE__} failed to update_online: #{inspect(error)}")
WandererApp.Cache.put(
"character:#{character_id}:online_forbidden",
true,
ttl: @online_forbidden_ttl
)
if is_nil(
WandererApp.Cache.lookup!("character:#{character_id}:online_error_time")
) do
WandererApp.Cache.insert(
"character:#{character_id}:online_error_time",
DateTime.utc_now()
)
end
{:error, :skipped}
_ ->
{:error, :skipped}
end
end
_ ->
{:error, :skipped}
end
end
def update_online(_), do: {:error, :skipped}
defp get_reset_timeout(_headers, _default_timeout \\ @limit_ttl)
defp get_reset_timeout(
%{"x-esi-error-limit-remain" => ["0"], "x-esi-error-limit-reset" => [reset_seconds]},
_default_timeout
)
when is_binary(reset_seconds),
do: :timer.seconds((reset_seconds |> String.to_integer()) + 1)
defp get_reset_timeout(_headers, default_timeout), do: default_timeout
def update_info(character_id) do
WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden")
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
true ->
{:error, :skipped}
@@ -79,8 +305,8 @@ defmodule WandererApp.Character.Tracker do
:ok
{:error, :forbidden} ->
Logger.warning("#{__MODULE__} failed to get_character_info: forbidden")
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
Logger.warning("#{__MODULE__} failed to get_character_info: #{inspect(error)}")
WandererApp.Cache.put(
"character:#{character_id}:info_forbidden",
@@ -88,20 +314,44 @@ defmodule WandererApp.Character.Tracker do
ttl: @forbidden_ttl
)
{:error, :forbidden}
{:error, error}
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
Logger.warning(
"#{__MODULE__} failed to get_character_info: #{inspect(:error_limited)}"
)
WandererApp.Cache.put(
"character:#{character_id}:info_forbidden",
true,
ttl: reset_timeout
)
{:error, :error_limited}
{:error, error} ->
WandererApp.Cache.put(
"character:#{character_id}:info_forbidden",
true,
ttl: @forbidden_ttl
)
Logger.error("#{__MODULE__} failed to get_character_info: #{inspect(error)}")
{:error, error}
_ ->
{:error, :skipped}
end
end
end
def update_ship(character_id) when is_binary(character_id) do
character_id
|> WandererApp.Character.get_character_state!()
|> update_ship()
end
def update_ship(character_id) when is_binary(character_id),
do:
character_id
|> WandererApp.Character.get_character_state!()
|> update_ship()
def update_ship(
%{character_id: character_id, track_ship: true, is_online: true} = character_state
@@ -110,7 +360,9 @@ defmodule WandererApp.Character.Tracker do
|> WandererApp.Character.get_character()
|> case do
{:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) ->
WandererApp.Cache.has_key?("character:#{character_id}:ship_forbidden")
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:ship_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
true ->
{:error, :skipped}
@@ -118,16 +370,15 @@ defmodule WandererApp.Character.Tracker do
_ ->
case WandererApp.Esi.get_character_ship(eve_id,
access_token: access_token,
character_id: character_id,
refresh_token?: true
character_id: character_id
) do
{:ok, ship} ->
{:ok, ship} when is_non_struct_map(ship) ->
character_state |> maybe_update_ship(ship)
:ok
{:error, :forbidden} ->
Logger.warning("#{__MODULE__} failed to update_ship: forbidden")
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
Logger.warning("#{__MODULE__} failed to update_ship: #{inspect(error)}")
WandererApp.Cache.put(
"character:#{character_id}:ship_forbidden",
@@ -135,7 +386,27 @@ defmodule WandererApp.Character.Tracker do
ttl: @forbidden_ttl
)
{:error, :forbidden}
if is_nil(WandererApp.Cache.lookup!("character:#{character_id}:ship_error_time")) do
WandererApp.Cache.insert(
"character:#{character_id}:ship_error_time",
DateTime.utc_now()
)
end
{:error, error}
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
Logger.warning("#{__MODULE__} failed to update_ship: #{inspect(:error_limited)}")
WandererApp.Cache.put(
"character:#{character_id}:ship_forbidden",
true,
ttl: reset_timeout
)
{:error, :error_limited}
{:error, error} ->
Logger.error("#{__MODULE__} failed to update_ship: #{inspect(error)}")
@@ -146,7 +417,32 @@ defmodule WandererApp.Character.Tracker do
ttl: @forbidden_ttl
)
if is_nil(WandererApp.Cache.lookup!("character:#{character_id}:ship_error_time")) do
WandererApp.Cache.insert(
"character:#{character_id}:ship_error_time",
DateTime.utc_now()
)
end
{:error, error}
_ ->
Logger.error("#{__MODULE__} failed to update_ship: wrong response")
WandererApp.Cache.put(
"character:#{character_id}:ship_forbidden",
true,
ttl: @forbidden_ttl
)
if is_nil(WandererApp.Cache.lookup!("character:#{character_id}:ship_error_time")) do
WandererApp.Cache.insert(
"character:#{character_id}:ship_error_time",
DateTime.utc_now()
)
end
{:error, :skipped}
end
end
@@ -157,18 +453,19 @@ defmodule WandererApp.Character.Tracker do
def update_ship(_), do: {:error, :skipped}
def update_location(character_id) when is_binary(character_id) do
character_id
|> WandererApp.Character.get_character_state!()
|> update_location()
end
def update_location(character_id) when is_binary(character_id),
do:
character_id
|> WandererApp.Character.get_character_state!()
|> update_location()
def update_location(
%{track_location: true, is_online: true, character_id: character_id} = character_state
) do
case WandererApp.Character.get_character(character_id) do
{:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) ->
WandererApp.Cache.has_key?("character:#{character_id}:location_forbidden")
(WandererApp.Cache.has_key?("character:#{character_id}:location_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
true ->
{:error, :skipped}
@@ -176,36 +473,70 @@ defmodule WandererApp.Character.Tracker do
_ ->
case WandererApp.Esi.get_character_location(eve_id,
access_token: access_token,
character_id: character_id,
refresh_token?: true
character_id: character_id
) do
{:ok, location} ->
{:ok, location} when is_non_struct_map(location) ->
character_state
|> maybe_update_location(location)
:ok
{:error, :forbidden} ->
Logger.warning("#{__MODULE__} failed to update_location: forbidden")
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
Logger.warning("#{__MODULE__} failed to update_location: #{inspect(error)}")
if is_nil(
WandererApp.Cache.lookup!("character:#{character_id}:location_error_time")
) do
WandererApp.Cache.insert(
"character:#{character_id}:location_error_time",
DateTime.utc_now()
)
end
{:error, :skipped}
{:error, :error_limited, headers} ->
Logger.warning(
"#{__MODULE__} failed to update_location: #{inspect(:error_limited)}"
)
reset_timeout = get_reset_timeout(headers, @location_limit_ttl)
WandererApp.Cache.put(
"character:#{character_id}:location_forbidden",
true,
ttl: @forbidden_ttl
ttl: reset_timeout
)
{:error, :forbidden}
{:error, :error_limited}
{:error, error} ->
Logger.error("#{__MODULE__} failed to update_location: #{inspect(error)}")
WandererApp.Cache.put(
"character:#{character_id}:location_forbidden",
true,
ttl: @forbidden_ttl
)
if is_nil(
WandererApp.Cache.lookup!("character:#{character_id}:location_error_time")
) do
WandererApp.Cache.insert(
"character:#{character_id}:location_error_time",
DateTime.utc_now()
)
end
{:error, error}
{:error, :skipped}
_ ->
Logger.error("#{__MODULE__} failed to update_location: wrong response")
if is_nil(
WandererApp.Cache.lookup!("character:#{character_id}:location_error_time")
) do
WandererApp.Cache.insert(
"character:#{character_id}:location_error_time",
DateTime.utc_now()
)
end
{:error, :skipped}
end
_ ->
@@ -219,146 +550,6 @@ defmodule WandererApp.Character.Tracker do
def update_location(_), do: {:error, :skipped}
def update_online(character_id) when is_binary(character_id) do
character_id
|> WandererApp.Character.get_character_state!()
|> update_online()
end
def update_online(%{track_online: true, character_id: character_id} = character_state) do
case WandererApp.Character.get_character(character_id) do
{:ok, %{eve_id: eve_id, access_token: access_token}}
when not is_nil(access_token) ->
WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden")
|> case do
true ->
{:error, :skipped}
_ ->
case WandererApp.Esi.get_character_online(eve_id,
access_token: access_token,
character_id: character_id,
refresh_token?: true
) do
{:ok, online} ->
online = get_online(online)
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
WandererApp.Character.update_character(character_id, online)
if not online.online do
WandererApp.Cache.delete("character:#{character_id}:location_started")
WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
end
update = %{
character_state
| is_online: online.online,
track_ship: online.online,
track_location: online.online
}
WandererApp.Character.update_character_state(character_id, update)
:ok
{:error, :forbidden} ->
Logger.warning("#{__MODULE__} failed to update_online: forbidden")
if not WandererApp.Cache.lookup!(
"character:#{character_id}:online_forbidden",
false
) do
WandererApp.Cache.put(
"character:#{character_id}:online_forbidden",
true,
ttl: @forbidden_ttl
)
if is_nil(
WandererApp.Cache.lookup("character:#{character_id}:online_error_time")
) do
WandererApp.Cache.insert(
"character:#{character_id}:online_error_time",
DateTime.utc_now()
)
end
end
:ok
{:error, :error_limited} ->
Logger.warning("#{__MODULE__} failed to update_online: :error_limited")
if not WandererApp.Cache.lookup!(
"character:#{character_id}:online_forbidden",
false
) do
WandererApp.Cache.put(
"character:#{character_id}:online_forbidden",
true,
ttl: @forbidden_ttl
)
if is_nil(
WandererApp.Cache.lookup("character:#{character_id}:online_error_time")
) do
WandererApp.Cache.insert(
"character:#{character_id}:online_error_time",
DateTime.utc_now()
)
end
end
:ok
{:error, error} ->
Logger.error("#{__MODULE__} failed to update_online: #{inspect(error)}")
if is_nil(WandererApp.Cache.lookup("character:#{character_id}:online_error_time")) do
WandererApp.Cache.insert(
"character:#{character_id}:online_error_time",
DateTime.utc_now()
)
end
:ok
end
end
_ ->
{:error, :skipped}
end
end
def update_online(_), do: {:error, :skipped}
def check_online_errors(character_id) do
WandererApp.Cache.lookup!("character:#{character_id}:online_error_time")
|> case do
nil ->
:skip
error_time ->
duration = DateTime.diff(DateTime.utc_now(), error_time, :second)
if duration >= @online_error_timeout do
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
:ok
else
:skip
end
end
end
def update_wallet(character_id) do
character_id
|> WandererApp.Character.get_character()
@@ -369,7 +560,9 @@ defmodule WandererApp.Character.Tracker do
|> WandererApp.Character.can_track_wallet?()
|> case do
true ->
WandererApp.Cache.has_key?("character:#{character_id}:wallet_forbidden")
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:wallet_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
true ->
{:error, :skipped}
@@ -378,8 +571,7 @@ defmodule WandererApp.Character.Tracker do
case WandererApp.Esi.get_character_wallet(eve_id,
params: %{datasource: "tranquility"},
access_token: access_token,
character_id: character_id,
refresh_token?: true
character_id: character_id
) do
{:ok, result} ->
{:ok, state} = WandererApp.Character.get_character_state(character_id)
@@ -387,8 +579,8 @@ defmodule WandererApp.Character.Tracker do
:ok
{:error, :forbidden} ->
Logger.warning("#{__MODULE__} failed to _update_wallet: forbidden")
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
Logger.warning("#{__MODULE__} failed to update_wallet: #{inspect(error)}")
WandererApp.Cache.put(
"character:#{character_id}:wallet_forbidden",
@@ -396,11 +588,44 @@ defmodule WandererApp.Character.Tracker do
ttl: @forbidden_ttl
)
{:error, :forbidden}
{:error, :skipped}
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
Logger.warning(
"#{__MODULE__} failed to update_wallet: #{inspect(:error_limited)}"
)
WandererApp.Cache.put(
"character:#{character_id}:wallet_forbidden",
true,
ttl: reset_timeout
)
{:error, :skipped}
{:error, error} ->
Logger.error("#{__MODULE__} failed to _update_wallet: #{inspect(error)}")
{:error, error}
WandererApp.Cache.put(
"character:#{character_id}:wallet_forbidden",
true,
ttl: @forbidden_ttl
)
{:error, :skipped}
error ->
Logger.error("#{__MODULE__} failed to _update_wallet: #{inspect(error)}")
WandererApp.Cache.put(
"character:#{character_id}:wallet_forbidden",
true,
ttl: @forbidden_ttl
)
{:error, :skipped}
end
end
@@ -414,83 +639,99 @@ defmodule WandererApp.Character.Tracker do
end
defp update_alliance(%{character_id: character_id} = state, alliance_id) do
alliance_id
|> WandererApp.Esi.get_alliance_info()
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
{:ok, %{"name" => alliance_name, "ticker" => alliance_ticker}} ->
{:ok, character} = WandererApp.Character.get_character(character_id)
character_update = %{
alliance_id: alliance_id,
alliance_name: alliance_name,
alliance_ticker: alliance_ticker
}
{:ok, _character} =
WandererApp.Api.Character.update_alliance(character, character_update)
WandererApp.Character.update_character(character_id, character_update)
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character_id}:alliance",
{:character_alliance, {character_id, character_update}}
)
true ->
state
_error ->
Logger.error("Failed to get alliance info for #{alliance_id}")
state
_ ->
alliance_id
|> WandererApp.Esi.get_alliance_info()
|> case do
{:ok, %{"name" => alliance_name, "ticker" => alliance_ticker}} ->
{:ok, character} = WandererApp.Character.get_character(character_id)
character_update = %{
alliance_id: alliance_id,
alliance_name: alliance_name,
alliance_ticker: alliance_ticker
}
{:ok, _character} =
WandererApp.Api.Character.update_alliance(character, character_update)
WandererApp.Character.update_character(character_id, character_update)
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character_id}:alliance",
{:character_alliance, {character_id, character_update}}
)
state
_error ->
Logger.error("Failed to get alliance info for #{alliance_id}")
state
end
end
end
defp update_corporation(%{character_id: character_id} = state, corporation_id) do
corporation_id
|> WandererApp.Esi.get_corporation_info()
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
alliance_id = Map.get(corporation_info, "alliance_id")
true ->
state
{:ok, character} =
WandererApp.Character.get_character(character_id)
_ ->
corporation_id
|> WandererApp.Esi.get_corporation_info()
|> case do
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
alliance_id = Map.get(corporation_info, "alliance_id")
character_update = %{
corporation_id: corporation_id,
corporation_name: corporation_name,
corporation_ticker: corporation_ticker,
alliance_id: alliance_id
}
{:ok, character} =
WandererApp.Character.get_character(character_id)
{:ok, _character} =
WandererApp.Api.Character.update_corporation(character, character_update)
WandererApp.Character.update_character(character_id, character_update)
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character_id}:corporation",
{:character_corporation,
{character_id,
%{
character_update = %{
corporation_id: corporation_id,
corporation_name: corporation_name,
corporation_ticker: corporation_ticker
}}}
)
corporation_ticker: corporation_ticker,
alliance_id: alliance_id
}
state
|> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id})
|> maybe_update_alliance()
{:ok, _character} =
WandererApp.Api.Character.update_corporation(character, character_update)
error ->
Logger.warning(
"Failed to get corporation info for character #{character_id}: #{inspect(error)}",
character_id: character_id,
corporation_id: corporation_id
)
WandererApp.Character.update_character(character_id, character_update)
state
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character_id}:corporation",
{:character_corporation,
{character_id,
%{
corporation_id: corporation_id,
corporation_name: corporation_name,
corporation_ticker: corporation_ticker
}}}
)
state
|> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id})
|> maybe_update_alliance()
error ->
Logger.warning(
"Failed to get corporation info for character #{character_id}: #{inspect(error)}",
character_id: character_id,
corporation_id: corporation_id
)
state
end
end
end
@@ -500,7 +741,8 @@ defmodule WandererApp.Character.Tracker do
} =
state,
ship
) do
)
when is_non_struct_map(ship) do
ship_type_id = Map.get(ship, "ship_type_id")
ship_name = Map.get(ship, "ship_name")
@@ -524,6 +766,12 @@ defmodule WandererApp.Character.Tracker do
state
end
defp maybe_update_ship(
state,
_ship
),
do: state
defp maybe_update_location(
%{
character_id: character_id
@@ -533,32 +781,12 @@ defmodule WandererApp.Character.Tracker do
) do
location = get_location(location)
if not is_location_started?(character_id) do
WandererApp.Cache.lookup!("character:#{character_id}:start_solar_system_id", nil)
|> case do
nil ->
WandererApp.Cache.put(
"character:#{character_id}:start_solar_system_id",
location.solar_system_id
)
start_solar_system_id ->
if location.solar_system_id != start_solar_system_id do
WandererApp.Cache.put(
"character:#{character_id}:location_started",
true
)
end
end
end
{:ok,
%{solar_system_id: solar_system_id, structure_id: structure_id, station_id: station_id} =
character} =
WandererApp.Character.get_character(character_id)
(not is_location_started?(character_id) ||
is_location_updated?(location, solar_system_id, structure_id, station_id))
is_location_updated?(location, solar_system_id, structure_id, station_id)
|> case do
true ->
{:ok, _character} = WandererApp.Api.Character.update_location(character, location)
@@ -573,13 +801,6 @@ defmodule WandererApp.Character.Tracker do
state
end
defp is_location_started?(character_id),
do:
WandererApp.Cache.lookup!(
"character:#{character_id}:location_started",
false
)
defp is_location_updated?(
%{
solar_system_id: new_solar_system_id,
@@ -717,16 +938,31 @@ defmodule WandererApp.Character.Tracker do
defp maybe_update_active_maps(
%{character_id: character_id, active_maps: active_maps} =
state,
%{map_id: map_id, track: true} = _track_settings
%{map_id: map_id, track: true} = track_settings
) do
WandererApp.Cache.put(
"character:#{character_id}:map:#{map_id}:tracking_start_time",
DateTime.utc_now()
)
if not Enum.member?(active_maps, map_id) do
WandererApp.Cache.put(
"character:#{character_id}:map:#{map_id}:tracking_start_time",
DateTime.utc_now()
)
WandererApp.Cache.take("character:#{character_id}:last_active_time")
WandererApp.Cache.put(
"map:#{map_id}:character:#{character_id}:start_solar_system_id",
track_settings |> Map.get(:solar_system_id)
)
%{state | active_maps: [map_id | active_maps] |> Enum.uniq()}
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
WandererApp.Cache.take("character:#{character_id}:last_active_time")
%{state | active_maps: [map_id | active_maps]}
else
WandererApp.Cache.take("character:#{character_id}:last_active_time")
state
end
end
defp maybe_update_active_maps(

View File

@@ -77,8 +77,6 @@ defmodule WandererApp.Character.TrackerManager.Impl do
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
WandererApp.Cache.delete("character:#{character_id}:last_active_time")
WandererApp.Cache.delete("character:#{character_id}:location_started")
WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
WandererApp.Character.delete_character_state(character_id)
tracked_characters =
@@ -189,7 +187,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end,
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task,
timeout: :timer.seconds(15)
timeout: :timer.seconds(60)
)
|> Enum.map(fn result ->
case result do
@@ -214,6 +212,10 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|> Task.async_stream(
fn {map_id, character_id} ->
if not character_is_present(map_id, character_id) do
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
{:ok, character_state} =
WandererApp.Character.Tracker.update_settings(character_id, %{
map_id: map_id,

View File

@@ -16,7 +16,7 @@ defmodule WandererApp.Character.TrackerPool do
@registry :tracker_pool_registry
@unique_registry :unique_tracker_pool_registry
@update_location_interval :timer.seconds(2)
@update_location_interval :timer.seconds(1)
@update_online_interval :timer.seconds(5)
@check_online_errors_interval :timer.seconds(30)
@update_ship_interval :timer.seconds(2)
@@ -24,6 +24,8 @@ defmodule WandererApp.Character.TrackerPool do
@update_wallet_interval :timer.minutes(1)
@inactive_character_timeout :timer.minutes(5)
@pause_tracking_timeout :timer.minutes(60 * 24)
@logger Application.compile_env(:wanderer_app, :logger)
def new(), do: __struct__()
@@ -49,7 +51,15 @@ defmodule WandererApp.Character.TrackerPool do
# end)
tracked_ids
|> Enum.each(fn id -> Cachex.put(@cache, id, uuid) end)
|> Enum.each(fn id ->
# WandererApp.Cache.put(
# "character:#{id}:tracking_paused",
# true,
# ttl: @pause_tracking_timeout
# )
Cachex.put(@cache, id, uuid)
end)
state =
%{
@@ -75,10 +85,15 @@ defmodule WandererApp.Character.TrackerPool do
[tracked_id | r_tracked_ids]
end)
# WandererApp.Cache.put(
# "character:#{tracked_id}:tracking_paused",
# true,
# ttl: @pause_tracking_timeout
# )
# Cachex.get_and_update(@cache, :tracked_characters, fn ids ->
# {:commit, ids ++ [tracked_id]}
# end)
Cachex.put(@cache, tracked_id, uuid)
{:noreply, %{state | characters: [tracked_id | characters]}}
@@ -96,7 +111,7 @@ defmodule WandererApp.Character.TrackerPool do
# Cachex.get_and_update(@cache, :tracked_characters, fn ids ->
# {:commit, ids |> Enum.reject(fn id -> id == tracked_id end)}
# end)
#
Cachex.del(@cache, tracked_id)
{:noreply, %{state | characters: characters |> Enum.reject(fn id -> id == tracked_id end)}}
@@ -155,12 +170,20 @@ defmodule WandererApp.Character.TrackerPool do
) do
Process.send_after(self(), :update_online, @update_online_interval)
characters
|> Enum.map(fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_online, [
character_id
])
end)
try do
characters
|> Enum.map(fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_online, [
character_id
])
end)
rescue
e ->
Logger.error("""
[Tracker Pool] update_online => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
end
{:noreply, state}
end
@@ -174,14 +197,22 @@ defmodule WandererApp.Character.TrackerPool do
) do
Process.send_after(self(), :update_online, @update_online_interval)
characters
|> Enum.each(fn character_id ->
WandererApp.Character.update_character(character_id, %{online: false})
try do
characters
|> Enum.each(fn character_id ->
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
end)
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
end)
rescue
e ->
Logger.error("""
[Tracker Pool] update_online => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
end
{:noreply, state}
end
@@ -195,21 +226,33 @@ defmodule WandererApp.Character.TrackerPool do
) do
Process.send_after(self(), :check_online_errors, @check_online_errors_interval)
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :check_online_errors, [
character_id
])
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in check_online_errors: #{inspect(reason)}")
end)
try do
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(
WandererApp.Character.Tracker,
:check_online_errors,
[
character_id
]
)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in check_online_errors: #{inspect(reason)}")
end)
rescue
e ->
Logger.error("""
[Tracker Pool] check_online_errors => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
end
{:noreply, state}
end
@@ -224,12 +267,20 @@ defmodule WandererApp.Character.TrackerPool do
) do
Process.send_after(self(), :update_location, @update_location_interval)
characters
|> Enum.map(fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_location, [
character_id
])
end)
try do
characters
|> Enum.map(fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_location, [
character_id
])
end)
rescue
e ->
Logger.error("""
[Tracker Pool] update_location => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
end
{:noreply, state}
end
@@ -253,12 +304,20 @@ defmodule WandererApp.Character.TrackerPool do
) do
Process.send_after(self(), :update_ship, @update_ship_interval)
characters
|> Enum.map(fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_ship, [
character_id
])
end)
try do
characters
|> Enum.map(fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_ship, [
character_id
])
end)
rescue
e ->
Logger.error("""
[Tracker Pool] update_ship => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
end
{:noreply, state}
end
@@ -282,21 +341,29 @@ defmodule WandererApp.Character.TrackerPool do
) do
Process.send_after(self(), :update_info, @update_info_interval)
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_info, [
character_id
])
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in update_info: #{inspect(reason)}")
end)
try do
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_info, [
character_id
])
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> Logger.error("Error in update_info: #{inspect(reason)}")
end)
rescue
e ->
Logger.error("""
[Tracker Pool] update_info => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
end
{:noreply, state}
end
@@ -320,21 +387,29 @@ defmodule WandererApp.Character.TrackerPool do
) do
Process.send_after(self(), :update_wallet, @update_wallet_interval)
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_wallet, [
character_id
])
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in update_wallet: #{inspect(reason)}")
end)
try do
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_wallet, [
character_id
])
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> Logger.error("Error in update_wallet: #{inspect(reason)}")
end)
rescue
e ->
Logger.error("""
[Tracker Pool] update_wallet => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
end
{:noreply, state}
end

View File

@@ -7,7 +7,7 @@ defmodule WandererApp.Character.TrackerPoolDynamicSupervisor do
@cache :tracked_characters
@registry :tracker_pool_registry
@unique_registry :unique_tracker_pool_registry
@tracker_pool_limit 100
@tracker_pool_limit 50
@name __MODULE__

View File

@@ -145,13 +145,15 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
end
defp get_wallet_journal(
%{corporation_id: corporation_id, access_token: access_token} = _character,
%{id: character_id, corporation_id: corporation_id, access_token: access_token} =
_character,
division
)
when not is_nil(access_token) do
case WandererApp.Esi.get_corporation_wallet_journal(corporation_id, division,
params: %{datasource: "tranquility"},
access_token: access_token
access_token: access_token,
character_id: character_id
) do
{:ok, result} ->
{:corporation_wallet_journal, result}
@@ -160,7 +162,7 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
Logger.warning("#{__MODULE__} failed to get_wallet_journal: forbidden")
{:error, :forbidden}
{:error, :error_limited} ->
{:error, :error_limited, _headers} ->
Logger.warning("#{__MODULE__} failed to get_wallet_journal: error_limited")
{:error, :error_limited}
@@ -173,12 +175,14 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
defp get_wallet_journal(_character, _division), do: {:error, :skipped}
defp update_corp_wallets(
%{corporation_id: corporation_id, access_token: access_token} = _character
%{id: character_id, corporation_id: corporation_id, access_token: access_token} =
_character
)
when not is_nil(access_token) do
case WandererApp.Esi.get_corporation_wallets(corporation_id,
params: %{datasource: "tranquility"},
access_token: access_token
access_token: access_token,
character_id: character_id
) do
{:ok, result} ->
{:corporation_wallets, result}
@@ -187,7 +191,7 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
Logger.warning("#{__MODULE__} failed to update_corp_wallets: forbidden")
{:error, :forbidden}
{:error, :error_limited} ->
{:error, :error_limited, _headers} ->
Logger.warning("#{__MODULE__} failed to update_corp_wallets: error_limited")
{:error, :error_limited}

View File

@@ -11,6 +11,8 @@ defmodule WandererApp.Esi.ApiClient do
@base_url "https://esi.evetech.net/latest"
@wanderrer_user_agent "(wanderer-industries@proton.me; +https://github.com/wanderer-industries/wanderer)"
@req_esi Req.new(base_url: @base_url, finch: WandererApp.Finch)
@get_link_pairs_advanced_params [
:include_mass_crit,
:include_eol,
@@ -35,8 +37,8 @@ defmodule WandererApp.Esi.ApiClient do
@default_avoid_systems [@zarzakh_system]
@cache_opts [cache: true]
@retry_opts [max_retries: 0, retry_log_level: :warning]
@timeout_opts [pool_timeout: 15_000, receive_timeout: :timer.seconds(30)]
@retry_opts [retry: false, retry_log_level: :warning]
@timeout_opts [pool_timeout: 15_000, receive_timeout: :timer.minutes(1)]
@api_retry_count 1
@logger Application.compile_env(:wanderer_app, :logger)
@@ -47,7 +49,7 @@ defmodule WandererApp.Esi.ApiClient do
do:
post_esi(
"/ui/autopilot/waypoint",
opts
get_auth_opts(opts)
|> Keyword.merge(
params: %{
add_to_beginning: add_to_beginning,
@@ -60,8 +62,8 @@ defmodule WandererApp.Esi.ApiClient do
def post_characters_affiliation(character_eve_ids, _opts)
when is_list(character_eve_ids),
do:
post(
"#{@base_url}/characters/affiliation/",
post_esi(
"/characters/affiliation/",
json: character_eve_ids,
params: %{
datasource: "tranquility"
@@ -218,8 +220,6 @@ defmodule WandererApp.Esi.ApiClient do
{:ok, result}
{:error, _error} ->
@logger.error("Error getting custom routes for #{inspect(origin)}: #{inspect(hubs)}")
@logger.error(
"Error getting custom routes for #{inspect(origin)}: #{inspect(params)}"
)
@@ -275,10 +275,7 @@ defmodule WandererApp.Esi.ApiClient do
"success" => true
}
{:error, :not_found} ->
%{"origin" => origin, "destination" => destination, "systems" => [], "success" => false}
{:error, error} ->
error ->
Logger.warning("Error getting routes: #{inspect(error)}")
%{"origin" => origin, "destination" => destination, "systems" => [], "success" => false}
end
@@ -293,6 +290,7 @@ defmodule WandererApp.Esi.ApiClient do
case _get_alliance_info(eve_id, "", opts) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
error -> error
end
end
@@ -314,6 +312,7 @@ defmodule WandererApp.Esi.ApiClient do
case _get_corporation_info(eve_id, "", opts) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
error -> error
end
end
@@ -330,6 +329,7 @@ defmodule WandererApp.Esi.ApiClient do
) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
error -> error
end
end
@@ -463,10 +463,10 @@ defmodule WandererApp.Esi.ApiClient do
get(
path,
auth_opts,
opts
opts |> with_refresh_token()
)
else
get_retry(path, auth_opts, opts)
get_retry(path, auth_opts, opts |> with_refresh_token())
end
end
@@ -485,7 +485,7 @@ defmodule WandererApp.Esi.ApiClient do
"/corporations/#{corporation_eve_id}/#{info_path}",
[params: opts[:params] || []] ++
(opts |> get_auth_opts()),
opts ++ @cache_opts
(opts |> with_refresh_token()) ++ @cache_opts
)
defp with_user_agent_opts(opts) do
@@ -495,17 +495,14 @@ defmodule WandererApp.Esi.ApiClient do
)
end
defp with_refresh_token(opts) do
opts |> Keyword.merge(refresh_token?: true)
end
defp with_cache_opts(opts) do
opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
end
defp post_esi(path, opts),
do:
post(
"#{@base_url}#{path}",
[params: opts[:params] || []] ++ (opts |> get_auth_opts())
)
defp get(path, api_opts \\ [], opts \\ []) do
case Cachex.get(:api_cache, path) do
{:ok, cached_data} when not is_nil(cached_data) ->
@@ -519,8 +516,9 @@ defmodule WandererApp.Esi.ApiClient do
defp do_get_request(path, api_opts \\ [], opts \\ []) do
try do
case Req.get(
"#{@base_url}#{path}",
@req_esi,
api_opts
|> Keyword.merge(url: path)
|> with_user_agent_opts()
|> with_cache_opts()
|> Keyword.merge(@retry_opts)
@@ -537,11 +535,12 @@ defmodule WandererApp.Esi.ApiClient do
{:ok, %{status: 404}} ->
{:error, :not_found}
{:ok, %{status: 403} = _error} ->
get_retry(path, api_opts, opts)
{:ok, %{status: 420, headers: headers} = _error} ->
Logger.warning("#{path} error_limited error: #{inspect(headers)}")
{:error, :error_limited, headers}
{:ok, %{status: 420} = _error} ->
get_retry(path, api_opts, opts, :error_limited)
{:ok, %{status: status} = _error} when status in [401, 403] ->
get_retry(path, api_opts, opts)
{:ok, %{status: status}} ->
{:error, "Unexpected status: #{status}"}
@@ -551,7 +550,7 @@ defmodule WandererApp.Esi.ApiClient do
end
rescue
e ->
@logger.error(Exception.message(e))
Logger.error(Exception.message(e))
{:error, "Request failed"}
end
@@ -593,8 +592,48 @@ defmodule WandererApp.Esi.ApiClient do
{:ok, %{status: 403}} ->
{:error, :forbidden}
{:ok, %{status: 420}} ->
{:error, :error_limited}
{:ok, %{status: 420, headers: headers} = _error} ->
Logger.warning("#{url} error_limited error: #{inspect(headers)}")
{:error, :error_limited, headers}
{:ok, %{status: status}} ->
{:error, "Unexpected status: #{status}"}
{:error, reason} ->
{:error, reason}
end
rescue
e ->
@logger.error(Exception.message(e))
{:error, "Request failed"}
end
end
defp post_esi(url, opts) do
try do
req_opts =
(opts |> with_user_agent_opts() |> Keyword.merge(@retry_opts)) ++
[params: opts[:params] || []]
Req.new(
[base_url: @base_url, finch: WandererApp.Finch] ++
req_opts
)
|> Req.post(url: url)
|> case do
{:ok, %{status: status, body: body}} when status in [200, 201] ->
{:ok, body}
{:ok, %{status: 504}} ->
{:error, :timeout}
{:ok, %{status: 403}} ->
{:error, :forbidden}
{:ok, %{status: 420, headers: headers} = _error} ->
Logger.warning("#{url} error_limited error: #{inspect(headers)}")
{:error, :error_limited, headers}
{:ok, %{status: status}} ->
{:error, "Unexpected status: #{status}"}

View File

@@ -96,7 +96,9 @@ defmodule WandererApp.Map do
map_id
|> get_map!()
|> Map.get(:characters, [])
|> Enum.map(fn character_id -> WandererApp.Character.get_map_character!(map_id, character_id) end)
|> Enum.map(fn character_id ->
WandererApp.Character.get_map_character!(map_id, character_id)
end)
def list_systems(map_id),
do: {:ok, map_id |> get_map!() |> Map.get(:systems, Map.new()) |> Map.values()}
@@ -155,56 +157,61 @@ defmodule WandererApp.Map do
case not (characters |> Enum.member?(character_id)) do
true ->
{:ok,
%{
alliance_id: alliance_id,
corporation_id: corporation_id,
solar_system_id: solar_system_id,
structure_id: structure_id,
station_id: station_id,
ship: ship_type_id,
ship_name: ship_name
}} = WandererApp.Character.get_character(character_id)
WandererApp.Character.get_map_character(map_id, character_id)
|> case do
{:ok,
%{
alliance_id: alliance_id,
corporation_id: corporation_id,
solar_system_id: solar_system_id,
structure_id: structure_id,
station_id: station_id,
ship: ship_type_id,
ship_name: ship_name
}} ->
map_id
|> update_map(%{characters: [character_id | characters]})
map_id
|> update_map(%{characters: [character_id | characters]})
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:alliance_id",
alliance_id
)
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:alliance_id",
alliance_id
)
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:corporation_id",
corporation_id
)
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:corporation_id",
corporation_id
)
# WandererApp.Cache.insert(
# "map:#{map_id}:character:#{character_id}:solar_system_id",
# solar_system_id
# )
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:solar_system_id",
solar_system_id
)
# WandererApp.Cache.insert(
# "map:#{map_id}:character:#{character_id}:structure_id",
# structure_id
# )
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:structure_id",
structure_id
)
# WandererApp.Cache.insert(
# "map:#{map_id}:character:#{character_id}:station_id",
# station_id
# )
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:station_id",
station_id
)
# WandererApp.Cache.insert(
# "map:#{map_id}:character:#{character_id}:ship_type_id",
# ship_type_id
# )
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:ship_type_id",
ship_type_id
)
# WandererApp.Cache.insert(
# "map:#{map_id}:character:#{character_id}:ship_name",
# ship_name
# )
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:ship_name",
ship_name
)
:ok
:ok
error ->
error
end
_ ->
{:error, :already_exists}

View File

@@ -14,13 +14,16 @@ defmodule WandererApp.Map.Server.CharactersImpl do
map_id: map_id,
tracked: track_character
}),
{:ok, character} <- WandererApp.Character.get_map_character(map_id, character_id) do
{:ok, character} <- WandererApp.Character.get_character(character_id) do
Impl.broadcast!(map_id, :character_added, character)
:telemetry.execute([:wanderer_app, :map, :character, :added], %{count: 1})
:ok
else
{:error, :not_found} ->
:ok
_error ->
{:ok, character} = WandererApp.Character.get_map_character(map_id, character_id)
{:ok, character} = WandererApp.Character.get_character(character_id)
Impl.broadcast!(map_id, :character_added, character)
:ok
end
@@ -207,15 +210,21 @@ defmodule WandererApp.Map.Server.CharactersImpl do
end
def update_characters(%{map_id: map_id} = state) do
{:ok, presence_character_ids} =
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
WandererApp.Cache.lookup!("maps:#{map_id}:tracked_characters", [])
|> Enum.filter(fn character_id -> character_id in presence_character_ids end)
|> Enum.map(fn character_id ->
Task.start_link(fn ->
character_updates =
maybe_update_online(map_id, character_id) ++
maybe_update_location(map_id, character_id) ++
maybe_update_ship(map_id, character_id) ++
maybe_update_alliance(map_id, character_id) ++
maybe_update_corporation(map_id, character_id)
maybe_update_tracking_status(map_id, character_id)
maybe_update_location(map_id, character_id) ++
maybe_update_ship(map_id, character_id) ++
maybe_update_alliance(map_id, character_id) ++
maybe_update_corporation(map_id, character_id)
character_updates
|> Enum.filter(fn update -> update != :skip end)
@@ -238,6 +247,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
{:character_online, _info} ->
:broadcast
{:character_tracking, _info} ->
:broadcast
{:character_alliance, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
@@ -292,38 +304,47 @@ defmodule WandererApp.Map.Server.CharactersImpl do
old_location,
%{map: map, map_id: map_id, rtree_name: rtree_name, map_opts: map_opts} = _state
) do
start_solar_system_id =
WandererApp.Cache.take("map:#{map_id}:character:#{character_id}:start_solar_system_id")
case is_nil(old_location.solar_system_id) and
is_nil(start_solar_system_id) and
ConnectionsImpl.can_add_location(map.scope, location.solar_system_id) do
true ->
:ok = SystemsImpl.maybe_add_system(map_id, location, nil, rtree_name, map_opts)
_ ->
ConnectionsImpl.is_connection_valid(
map.scope,
old_location.solar_system_id,
location.solar_system_id
)
|> case do
true ->
:ok =
SystemsImpl.maybe_add_system(map_id, location, old_location, rtree_name, map_opts)
:ok =
SystemsImpl.maybe_add_system(map_id, old_location, location, rtree_name, map_opts)
if is_character_in_space?(location) do
if is_nil(start_solar_system_id) || start_solar_system_id == old_location.solar_system_id do
ConnectionsImpl.is_connection_valid(
map.scope,
old_location.solar_system_id,
location.solar_system_id
)
|> case do
true ->
:ok =
ConnectionsImpl.maybe_add_connection(
map_id,
location,
old_location,
character_id,
false
)
end
SystemsImpl.maybe_add_system(map_id, location, old_location, rtree_name, map_opts)
_ ->
:ok
:ok =
SystemsImpl.maybe_add_system(map_id, old_location, location, rtree_name, map_opts)
if is_character_in_space?(location) do
:ok =
ConnectionsImpl.maybe_add_connection(
map_id,
location,
old_location,
character_id,
false
)
end
_ ->
:ok
end
else
# skip adding connection or system if character just started tracking on the map
:ok
end
end
end
@@ -333,7 +354,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
end
defp track_character(map_id, character_id) do
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, character} =
WandererApp.Character.get_map_character(map_id, character_id, not_present: true)
add_character(%{map_id: map_id}, character, true)
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
@@ -341,7 +364,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
track: true,
track_online: true,
track_location: true,
track_ship: true
track_ship: true,
solar_system_id: character.solar_system_id
})
end
@@ -369,6 +393,33 @@ defmodule WandererApp.Map.Server.CharactersImpl do
end
end
defp maybe_update_tracking_status(map_id, character_id) do
with {:ok, old_tracking_paused} <-
WandererApp.Cache.lookup(
"map:#{map_id}:character:#{character_id}:tracking_paused",
false
),
{:ok, tracking_paused} <-
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false) do
case old_tracking_paused != tracking_paused do
true ->
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:tracking_paused",
tracking_paused
)
[{:character_tracking, %{tracking_paused: tracking_paused}}]
_ ->
[:skip]
end
else
error ->
Logger.error("Failed to update character_tracking: #{inspect(error, pretty: true)}")
[:skip]
end
end
defp maybe_update_ship(map_id, character_id) do
with {:ok, old_ship_type_id} <-
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:ship_type_id"),

View File

@@ -18,34 +18,38 @@ defmodule WandererApp.Maps do
]
def find_routes(map_id, hubs, origin, routes_settings, false) do
{:ok, routes} =
WandererApp.Esi.find_routes(
map_id,
origin,
hubs,
routes_settings
)
WandererApp.Esi.find_routes(
map_id,
origin,
hubs,
routes_settings
)
|> case do
{:ok, routes} ->
systems_static_data =
routes
|> Enum.map(fn route_info -> route_info.systems end)
|> List.flatten()
|> Enum.uniq()
|> Task.async_stream(
fn system_id ->
case WandererApp.CachedInfo.get_system_static_info(system_id) do
{:ok, nil} ->
nil
systems_static_data =
routes
|> Enum.map(fn route_info -> route_info.systems end)
|> List.flatten()
|> Enum.uniq()
|> Task.async_stream(
fn system_id ->
case WandererApp.CachedInfo.get_system_static_info(system_id) do
{:ok, nil} ->
nil
{:ok, system} ->
system |> Map.take(@minimum_route_attrs)
end
end,
max_concurrency: 10
)
|> Enum.map(fn {:ok, val} -> val end)
{:ok, system} ->
system |> Map.take(@minimum_route_attrs)
end
end,
max_concurrency: 10
)
|> Enum.map(fn {:ok, val} -> val end)
{:ok, %{routes: routes, systems_static_data: systems_static_data}}
{:ok, %{routes: routes, systems_static_data: systems_static_data}}
error ->
{:ok, %{routes: [], systems_static_data: []}}
end
end
def find_routes(map_id, hubs, origin, routes_settings, true) do

View File

@@ -28,8 +28,6 @@ defmodule WandererApp.Server.ServerStatusTracker do
@logger Application.compile_env(:wanderer_app, :logger)
def get_status(), do: GenServer.call(@name, :get_status)
def start_link(opts \\ []), do: GenServer.start(__MODULE__, opts, name: @name)
@impl true
@@ -42,9 +40,6 @@ defmodule WandererApp.Server.ServerStatusTracker do
@impl true
def terminate(_reason, _state), do: :ok
@impl true
def handle_call(:get_status, _from, state), do: {:reply, {:ok, state}, state}
@impl true
def handle_call(:stop, _, state), do: {:stop, :normal, :ok, state}
@@ -66,7 +61,7 @@ defmodule WandererApp.Server.ServerStatusTracker do
} = state
) do
Process.send_after(self(), :refresh_status, @refresh_interval)
Task.async(fn -> _get_server_status(retries) end)
Task.async(fn -> get_server_status(retries) end)
{:noreply, state}
end
@@ -104,7 +99,7 @@ defmodule WandererApp.Server.ServerStatusTracker do
def handle_info(_action, state),
do: {:noreply, state}
defp _get_server_status(retries) do
defp get_server_status(retries) do
case WandererApp.Esi.get_server_status() do
{:ok, result} ->
{:status, _get_status(result)}
@@ -113,7 +108,7 @@ defmodule WandererApp.Server.ServerStatusTracker do
if retries > 0 do
:retry
else
@logger.warning("#{__MODULE__} failed to refresh server status: :timeout")
Logger.warning("#{__MODULE__} failed to refresh server status: :timeout")
{:status, @initial_state}
end
@@ -121,9 +116,12 @@ defmodule WandererApp.Server.ServerStatusTracker do
if retries > 0 do
:retry
else
@logger.warning("#{__MODULE__} failed to refresh server status: #{inspect(error)}")
Logger.warning("#{__MODULE__} failed to refresh server status: #{inspect(error)}")
{:status, @initial_state}
end
_ ->
{:error, :unknown}
end
end

View File

@@ -15,26 +15,39 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
@doc """
Fetch killmails for multiple systems, returning a map of system_id => kills.
"""
def fetch_kills_for_systems(system_ids, since_hours, state, _opts \\ []) when is_list(system_ids) do
try do
{final_map, final_state} =
Enum.reduce(system_ids, {%{}, state}, fn sid, {acc_map, acc_st} ->
case fetch_kills_for_system(sid, since_hours, acc_st) do
{:ok, kills, new_st} ->
{Map.put(acc_map, sid, kills), new_st}
def fetch_kills_for_systems(system_ids, since_hours, state, _opts \\ [])
when is_list(system_ids) do
zkill_preload_disabled = WandererApp.Env.zkill_preload_disabled?()
{:error, reason, new_st} ->
Logger.debug(fn -> "[Fetcher] system=#{sid} => error=#{inspect(reason)}" end)
{Map.put(acc_map, sid, {:error, reason}), new_st}
end
if not zkill_preload_disabled do
try do
{final_map, final_state} =
Enum.reduce(system_ids, {%{}, state}, fn sid, {acc_map, acc_st} ->
case fetch_kills_for_system(sid, since_hours, acc_st) do
{:ok, kills, new_st} ->
{Map.put(acc_map, sid, kills), new_st}
{:error, reason, new_st} ->
Logger.debug(fn -> "[Fetcher] system=#{sid} => error=#{inspect(reason)}" end)
{Map.put(acc_map, sid, {:error, reason}), new_st}
end
end)
Logger.debug(fn ->
"[Fetcher] fetch_kills_for_systems => done, final_map_size=#{map_size(final_map)} calls=#{final_state.calls_count}"
end)
Logger.debug(fn -> "[Fetcher] fetch_kills_for_systems => done, final_map_size=#{map_size(final_map)} calls=#{final_state.calls_count}" end)
{:ok, final_map}
rescue
e ->
Logger.error("[Fetcher] EXCEPTION in fetch_kills_for_systems => #{Exception.message(e)}")
{:error, e}
{:ok, final_map}
rescue
e ->
Logger.error(
"[Fetcher] EXCEPTION in fetch_kills_for_systems => #{Exception.message(e)}"
)
{:error, e}
end
else
{:error, :kills_disabled}
end
end
@@ -58,18 +71,25 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
if not force? and KillsCache.recently_fetched?(system_id) do
cached_kills = KillsCache.fetch_cached_kills(system_id)
final = maybe_take(cached_kills, limit)
Logger.debug(fn -> "#{log_prefix}, recently_fetched?=true => returning #{length(final)} cached kills" end)
Logger.debug(fn ->
"#{log_prefix}, recently_fetched?=true => returning #{length(final)} cached kills"
end)
{:ok, final, state}
else
Logger.debug(fn -> "#{log_prefix}, hours=#{since_hours}, limit=#{inspect(limit)}, force=#{force?}" end)
Logger.debug(fn ->
"#{log_prefix}, hours=#{since_hours}, limit=#{inspect(limit)}, force=#{force?}"
end)
cutoff_dt = hours_ago(since_hours)
result =
retry with: exponential_backoff(300)
|> randomize()
|> cap(5_000)
|> expiry(120_000) do
retry with:
exponential_backoff(300)
|> randomize()
|> cap(5_000)
|> expiry(120_000) do
case do_multi_page_fetch(system_id, cutoff_dt, 1, 0, limit, state) do
{:ok, new_st, total_fetched} ->
# Mark system as fully fetched (to prevent repeated calls).
@@ -118,10 +138,13 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
with {:ok, st1} <- increment_calls_count(state),
{:ok, st2, partials} <- ZkbApi.fetch_and_parse_page(system_id, page, st1) do
Logger.debug(fn -> "[Fetcher] system=#{system_id}, page=#{page}, partials_count=#{length(partials)}" end)
Logger.debug(fn ->
"[Fetcher] system=#{system_id}, page=#{page}, partials_count=#{length(partials)}"
end)
{_count_stored, older_found?, total_now} =
Enum.reduce_while(partials, {0, false, total_so_far}, fn partial, {acc_count, had_older, acc_total} ->
Enum.reduce_while(partials, {0, false, total_so_far}, fn partial,
{acc_count, had_older, acc_total} ->
# If we have a limit and reached it, stop immediately
if reached_limit?(limit, acc_total) do
{:halt, {acc_count, had_older, acc_total}}
@@ -170,7 +193,10 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
end
end
defp parse_partial(%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial, cutoff_dt) do
defp parse_partial(
%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial,
cutoff_dt
) do
# If we've already cached this kill, skip
if KillsCache.get_killmail(kill_id) do
:skip
@@ -191,7 +217,8 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
defp parse_partial(_other, _cutoff_dt), do: :skip
defp fetch_full_killmail(k_id, k_hash) do
retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000), rescue_only: [RuntimeError] do
retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000),
rescue_only: [RuntimeError] do
case WandererApp.Esi.get_killmail(k_id, k_hash) do
{:ok, full_km} ->
{:ok, full_km}
@@ -206,12 +233,25 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.warning("[Fetcher] ESI get_killmail retriable error => kill_id=#{k_id}, reason=#{inspect(reason)}")
Logger.warning(
"[Fetcher] ESI get_killmail retriable error => kill_id=#{k_id}, reason=#{inspect(reason)}"
)
raise "ESI error: #{inspect(reason)}, will retry"
else
Logger.warning("[Fetcher] ESI get_killmail failed => kill_id=#{k_id}, reason=#{inspect(reason)}")
Logger.warning(
"[Fetcher] ESI get_killmail failed => kill_id=#{k_id}, reason=#{inspect(reason)}"
)
{:error, reason}
end
error ->
Logger.warning(
"[Fetcher] ESI get_killmail failed => kill_id=#{k_id}, reason=#{inspect(error)}"
)
error
end
end
end
@@ -223,6 +263,7 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
do: {:ok, %{st | calls_count: c + 1}}
defp reached_limit?(nil, _count_so_far), do: false
defp reached_limit?(limit, count_so_far) when is_integer(limit),
do: count_so_far >= limit

View File

@@ -126,7 +126,9 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
KillsCache.incr_system_kill_count(system_id)
end
else
Logger.warning("[Parser] store_killmail => build_kill_data returned nil for kill_id=#{kill_id}")
Logger.warning(
"[Parser] store_killmail => build_kill_data returned nil for kill_id=#{kill_id}"
)
end
end
@@ -144,7 +146,6 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
"total_value" => total_value,
"npc" => npc
}) do
victim_map = extract_victim_fields(victim)
final_blow_map = extract_final_blow_fields(attackers)
@@ -153,17 +154,14 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
"kill_time" => kill_time_dt,
"solar_system_id" => sys_id,
"zkb" => zkb,
"victim_char_id" => victim_map.char_id,
"victim_corp_id" => victim_map.corp_id,
"victim_alliance_id" => victim_map.alliance_id,
"victim_ship_type_id" => victim_map.ship_type_id,
"final_blow_char_id" => final_blow_map.char_id,
"final_blow_corp_id" => final_blow_map.corp_id,
"final_blow_alliance_id" => final_blow_map.alliance_id,
"final_blow_ship_type_id" => final_blow_map.ship_type_id,
"attacker_count" => attacker_count,
"total_value" => total_value,
"npc" => npc
@@ -179,14 +177,14 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
"alliance_id" => alli,
"ship_type_id" => st_id
}),
do: %{char_id: cid, corp_id: corp, alliance_id: alli, ship_type_id: st_id}
do: %{char_id: cid, corp_id: corp, alliance_id: alli, ship_type_id: st_id}
defp extract_victim_fields(%{
"character_id" => cid,
"corporation_id" => corp,
"ship_type_id" => st_id
}),
do: %{char_id: cid, corp_id: corp, alliance_id: nil, ship_type_id: st_id}
do: %{char_id: cid, corp_id: corp, alliance_id: nil, ship_type_id: st_id}
defp extract_victim_fields(_),
do: %{char_id: nil, corp_id: nil, alliance_id: nil, ship_type_id: nil}
@@ -208,14 +206,14 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
"alliance_id" => alli,
"ship_type_id" => st_id
}),
do: %{char_id: cid, corp_id: corp, alliance_id: alli, ship_type_id: st_id}
do: %{char_id: cid, corp_id: corp, alliance_id: alli, ship_type_id: st_id}
defp extract_attacker_fields(%{
"character_id" => cid,
"corporation_id" => corp,
"ship_type_id" => st_id
}),
do: %{char_id: cid, corp_id: corp, alliance_id: nil, ship_type_id: st_id}
do: %{char_id: cid, corp_id: corp, alliance_id: nil, ship_type_id: st_id}
defp extract_attacker_fields(%{"ship_type_id" => st_id} = attacker) do
%{
@@ -235,52 +233,78 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|> enrich_final_blow()
end
defp enrich_victim(km) do
km
|> maybe_put_character_name("victim_char_id", "victim_char_name")
|> maybe_put_corp_info("victim_corp_id", "victim_corp_ticker", "victim_corp_name")
|> maybe_put_alliance_info("victim_alliance_id", "victim_alliance_ticker", "victim_alliance_name")
|> maybe_put_alliance_info(
"victim_alliance_id",
"victim_alliance_ticker",
"victim_alliance_name"
)
|> maybe_put_ship_name("victim_ship_type_id", "victim_ship_name")
end
defp enrich_final_blow(km) do
km
|> maybe_put_character_name("final_blow_char_id", "final_blow_char_name")
|> maybe_put_corp_info("final_blow_corp_id", "final_blow_corp_ticker", "final_blow_corp_name")
|> maybe_put_alliance_info("final_blow_alliance_id", "final_blow_alliance_ticker", "final_blow_alliance_name")
|> maybe_put_alliance_info(
"final_blow_alliance_id",
"final_blow_alliance_ticker",
"final_blow_alliance_name"
)
|> maybe_put_ship_name("final_blow_ship_type_id", "final_blow_ship_name")
end
defp maybe_put_character_name(km, id_key, name_key) do
case Map.get(km, id_key) do
nil -> km
0 -> km
nil ->
km
0 ->
km
eve_id ->
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
case WandererApp.Esi.get_character_info(eve_id) do
{:ok, %{"name" => char_name}} ->
{:ok, char_name}
result =
retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000),
rescue_only: [RuntimeError] do
case WandererApp.Esi.get_character_info(eve_id) do
{:ok, %{"name" => char_name}} ->
{:ok, char_name}
{:error, :timeout} ->
Logger.debug(fn -> "[Parser] Character info timeout, retrying => id=#{eve_id}" end)
raise "Character info timeout, will retry"
{:error, :timeout} ->
Logger.debug(fn -> "[Parser] Character info timeout, retrying => id=#{eve_id}" end)
{:error, :not_found} ->
Logger.debug(fn -> "[Parser] Character not found => id=#{eve_id}" end)
:skip
raise "Character info timeout, will retry"
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.debug(fn -> "[Parser] Character info retriable error => id=#{eve_id}, reason=#{inspect(reason)}" end)
raise "Character info error: #{inspect(reason)}, will retry"
else
Logger.debug(fn -> "[Parser] Character info failed => id=#{eve_id}, reason=#{inspect(reason)}" end)
{:error, :not_found} ->
Logger.debug(fn -> "[Parser] Character not found => id=#{eve_id}" end)
:skip
end
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.debug(fn ->
"[Parser] Character info retriable error => id=#{eve_id}, reason=#{inspect(reason)}"
end)
raise "Character info error: #{inspect(reason)}, will retry"
else
Logger.debug(fn ->
"[Parser] Character info failed => id=#{eve_id}, reason=#{inspect(reason)}"
end)
:skip
end
error ->
Logger.debug(fn ->
"[Parser] Character info failed => id=#{eve_id}, reason=#{inspect(error)}"
end)
:skip
end
end
end
case result do
{:ok, char_name} -> Map.put(km, name_key, char_name)
@@ -291,109 +315,180 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
defp maybe_put_corp_info(km, id_key, ticker_key, name_key) do
case Map.get(km, id_key) do
nil -> km
0 -> km
nil ->
km
0 ->
km
corp_id ->
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
case WandererApp.Esi.get_corporation_info(corp_id) do
{:ok, %{"ticker" => ticker, "name" => corp_name}} ->
{:ok, {ticker, corp_name}}
result =
retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000),
rescue_only: [RuntimeError] do
case WandererApp.Esi.get_corporation_info(corp_id) do
{:ok, %{"ticker" => ticker, "name" => corp_name}} ->
{:ok, {ticker, corp_name}}
{:error, :timeout} ->
Logger.debug(fn -> "[Parser] Corporation info timeout, retrying => id=#{corp_id}" end)
raise "Corporation info timeout, will retry"
{:error, :timeout} ->
Logger.debug(fn ->
"[Parser] Corporation info timeout, retrying => id=#{corp_id}"
end)
{:error, :not_found} ->
Logger.debug(fn -> "[Parser] Corporation not found => id=#{corp_id}" end)
:skip
raise "Corporation info timeout, will retry"
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.debug(fn -> "[Parser] Corporation info retriable error => id=#{corp_id}, reason=#{inspect(reason)}" end)
raise "Corporation info error: #{inspect(reason)}, will retry"
else
Logger.warning("[Parser] Failed to fetch corp info: ID=#{corp_id}, reason=#{inspect(reason)}")
{:error, :not_found} ->
Logger.debug(fn -> "[Parser] Corporation not found => id=#{corp_id}" end)
:skip
end
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.debug(fn ->
"[Parser] Corporation info retriable error => id=#{corp_id}, reason=#{inspect(reason)}"
end)
raise "Corporation info error: #{inspect(reason)}, will retry"
else
Logger.warning(
"[Parser] Failed to fetch corp info: ID=#{corp_id}, reason=#{inspect(reason)}"
)
:skip
end
error ->
Logger.warning(
"[Parser] Failed to fetch corp info: ID=#{corp_id}, reason=#{inspect(error)}"
)
:skip
end
end
end
case result do
{:ok, {ticker, corp_name}} ->
km
|> Map.put(ticker_key, ticker)
|> Map.put(name_key, corp_name)
_ -> km
_ ->
km
end
end
end
defp maybe_put_alliance_info(km, id_key, ticker_key, name_key) do
case Map.get(km, id_key) do
nil -> km
0 -> km
nil ->
km
0 ->
km
alliance_id ->
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
case WandererApp.Esi.get_alliance_info(alliance_id) do
{:ok, %{"ticker" => alliance_ticker, "name" => alliance_name}} ->
{:ok, {alliance_ticker, alliance_name}}
result =
retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000),
rescue_only: [RuntimeError] do
case WandererApp.Esi.get_alliance_info(alliance_id) do
{:ok, %{"ticker" => alliance_ticker, "name" => alliance_name}} ->
{:ok, {alliance_ticker, alliance_name}}
{:error, :timeout} ->
Logger.debug(fn -> "[Parser] Alliance info timeout, retrying => id=#{alliance_id}" end)
raise "Alliance info timeout, will retry"
{:error, :timeout} ->
Logger.debug(fn ->
"[Parser] Alliance info timeout, retrying => id=#{alliance_id}"
end)
{:error, :not_found} ->
Logger.debug(fn -> "[Parser] Alliance not found => id=#{alliance_id}" end)
:skip
raise "Alliance info timeout, will retry"
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.debug(fn -> "[Parser] Alliance info retriable error => id=#{alliance_id}, reason=#{inspect(reason)}" end)
raise "Alliance info error: #{inspect(reason)}, will retry"
else
Logger.debug(fn -> "[Parser] Alliance info failed => id=#{alliance_id}, reason=#{inspect(reason)}" end)
{:error, :not_found} ->
Logger.debug(fn -> "[Parser] Alliance not found => id=#{alliance_id}" end)
:skip
end
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.debug(fn ->
"[Parser] Alliance info retriable error => id=#{alliance_id}, reason=#{inspect(reason)}"
end)
raise "Alliance info error: #{inspect(reason)}, will retry"
else
Logger.debug(fn ->
"[Parser] Alliance info failed => id=#{alliance_id}, reason=#{inspect(reason)}"
end)
:skip
end
error ->
Logger.debug(fn ->
"[Parser] Alliance info failed => id=#{alliance_id}, reason=#{inspect(error)}"
end)
:skip
end
end
end
case result do
{:ok, {alliance_ticker, alliance_name}} ->
km
|> Map.put(ticker_key, alliance_ticker)
|> Map.put(name_key, alliance_name)
_ -> km
_ ->
km
end
end
end
defp maybe_put_ship_name(km, id_key, name_key) do
case Map.get(km, id_key) do
nil -> km
0 -> km
nil ->
km
0 ->
km
type_id ->
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
case WandererApp.CachedInfo.get_ship_type(type_id) do
{:ok, nil} -> :skip
{:ok, %{name: ship_name}} -> {:ok, ship_name}
{:error, :timeout} ->
Logger.debug(fn -> "[Parser] Ship type timeout, retrying => id=#{type_id}" end)
raise "Ship type timeout, will retry"
{:error, :not_found} ->
Logger.debug(fn -> "[Parser] Ship type not found => id=#{type_id}" end)
:skip
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.debug(fn -> "[Parser] Ship type retriable error => id=#{type_id}, reason=#{inspect(reason)}" end)
raise "Ship type error: #{inspect(reason)}, will retry"
else
Logger.warning("[Parser] Failed to fetch ship type: ID=#{type_id}, reason=#{inspect(reason)}")
result =
retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000),
rescue_only: [RuntimeError] do
case WandererApp.CachedInfo.get_ship_type(type_id) do
{:ok, nil} ->
:skip
end
{:ok, %{name: ship_name}} ->
{:ok, ship_name}
{:error, :timeout} ->
Logger.debug(fn -> "[Parser] Ship type timeout, retrying => id=#{type_id}" end)
raise "Ship type timeout, will retry"
{:error, :not_found} ->
Logger.debug(fn -> "[Parser] Ship type not found => id=#{type_id}" end)
:skip
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.debug(fn ->
"[Parser] Ship type retriable error => id=#{type_id}, reason=#{inspect(reason)}"
end)
raise "Ship type error: #{inspect(reason)}, will retry"
else
Logger.warning(
"[Parser] Failed to fetch ship type: ID=#{type_id}, reason=#{inspect(reason)}"
)
:skip
end
error ->
Logger.warning(
"[Parser] Failed to fetch ship type: ID=#{type_id}, reason=#{inspect(error)}"
)
:skip
end
end
end
case result do
{:ok, ship_name} -> Map.put(km, name_key, ship_name)

View File

@@ -46,7 +46,10 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
# Called on disconnect
def handle_disconnect(code, reason, _old_state) do
Logger.warning("[KillsProvider.Websocket] Disconnected => code=#{code}, reason=#{inspect(reason)} => reconnecting")
Logger.warning(
"[KillsProvider.Websocket] Disconnected => code=#{code}, reason=#{inspect(reason)} => reconnecting"
)
:reconnect
end
@@ -69,41 +72,73 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
end
# The partial from zKillboard has killmail_id + zkb.hash, but no time/victim/attackers
defp parse_and_store_zkb_partial(%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial) do
Logger.debug(fn -> "[KillsProvider.Websocket] parse_and_store_zkb_partial => kill_id=#{kill_id}" end)
defp parse_and_store_zkb_partial(
%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial
) do
Logger.debug(fn ->
"[KillsProvider.Websocket] parse_and_store_zkb_partial => kill_id=#{kill_id}"
end)
result = retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000), rescue_only: [RuntimeError] do
case Esi.get_killmail(kill_id, kill_hash) do
{:ok, full_esi_data} ->
# Merge partial zKB fields (like totalValue) onto ESI data
enriched = Map.merge(full_esi_data, %{"zkb" => partial["zkb"]})
Parser.parse_and_store_killmail(enriched)
:ok
result =
retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000),
rescue_only: [RuntimeError] do
case Esi.get_killmail(kill_id, kill_hash) do
{:ok, full_esi_data} ->
# Merge partial zKB fields (like totalValue) onto ESI data
enriched = Map.merge(full_esi_data, %{"zkb" => partial["zkb"]})
Parser.parse_and_store_killmail(enriched)
:ok
{:error, :timeout} ->
Logger.warning("[KillsProvider.Websocket] ESI get_killmail timeout => kill_id=#{kill_id}, retrying...")
raise "ESI timeout, will retry"
{:error, :timeout} ->
Logger.warning(
"[KillsProvider.Websocket] ESI get_killmail timeout => kill_id=#{kill_id}, retrying..."
)
{:error, :not_found} ->
Logger.warning("[KillsProvider.Websocket] ESI get_killmail not_found => kill_id=#{kill_id}")
:skip
raise "ESI timeout, will retry"
{:error, :not_found} ->
Logger.warning(
"[KillsProvider.Websocket] ESI get_killmail not_found => kill_id=#{kill_id}"
)
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.warning("[KillsProvider.Websocket] ESI get_killmail retriable error => kill_id=#{kill_id}, reason=#{inspect(reason)}")
raise "ESI error: #{inspect(reason)}, will retry"
else
Logger.warning("[KillsProvider.Websocket] ESI get_killmail failed => kill_id=#{kill_id}, reason=#{inspect(reason)}")
:skip
end
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.warning(
"[KillsProvider.Websocket] ESI get_killmail retriable error => kill_id=#{kill_id}, reason=#{inspect(reason)}"
)
raise "ESI error: #{inspect(reason)}, will retry"
else
Logger.warning(
"[KillsProvider.Websocket] ESI get_killmail failed => kill_id=#{kill_id}, reason=#{inspect(reason)}"
)
:skip
end
error ->
Logger.warning(
"[KillsProvider.Websocket] ESI get_killmail failed => kill_id=#{kill_id}, reason=#{inspect(error)}"
)
:skip
end
end
end
case result do
:ok -> :ok
:skip -> :skip
:ok ->
:ok
:skip ->
:skip
{:error, reason} ->
Logger.error("[KillsProvider.Websocket] ESI get_killmail exhausted retries => kill_id=#{kill_id}, reason=#{inspect(reason)}")
Logger.error(
"[KillsProvider.Websocket] ESI get_killmail exhausted retries => kill_id=#{kill_id}, reason=#{inspect(reason)}"
)
:skip
end
end

View File

@@ -104,7 +104,7 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
Creates a new ACL member.
"""
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :create,
operation(:create,
summary: "Create ACL Member",
description: "Creates a new ACL member for a given ACL.",
parameters: [
@@ -127,6 +127,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
@acl_member_create_response_schema
}
]
)
def create(conn, %{"acl_id" => acl_id, "member" => member_params}) do
chosen =
cond do
@@ -160,8 +162,7 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
conn
|> put_status(:bad_request)
|> json(%{
error:
"#{String.capitalize(type)} members cannot have an admin or manager role"
error: "#{String.capitalize(type)} members cannot have an admin or manager role"
})
else
info_fetcher =
@@ -191,7 +192,7 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|> json(%{error: "Creation failed: #{inspect(error)}"})
end
else
{:error, error} ->
error ->
conn
|> put_status(:bad_request)
|> json(%{error: "Entity lookup failed: #{inspect(error)}"})
@@ -206,7 +207,7 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
Updates the role of an ACL member.
"""
@spec update_role(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :update_role,
operation(:update_role,
summary: "Update ACL Member Role",
description: "Updates the role of an ACL member identified by ACL ID and member external ID.",
parameters: [
@@ -235,6 +236,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
@acl_member_update_response_schema
}
]
)
def update_role(conn, %{
"acl_id" => acl_id,
"member_id" => external_id,
@@ -301,7 +304,7 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
Deletes an ACL member.
"""
@spec delete(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :delete,
operation(:delete,
summary: "Delete ACL Member",
description: "Deletes an ACL member identified by ACL ID and member external ID.",
parameters: [
@@ -325,6 +328,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
@acl_member_delete_response_schema
}
]
)
def delete(conn, %{"acl_id" => acl_id, "member_id" => external_id}) do
external_id_str = to_string(external_id)

View File

@@ -32,16 +32,18 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
)
end
def handle_server_event(%{event: :untrack_character, payload: character_id}, %{
assigns: %{
map_id: map_id
}
} = socket) do
def handle_server_event(
%{event: :untrack_character, payload: character_id},
%{
assigns: %{
map_id: map_id
}
} = socket
) do
:ok = WandererApp.Character.TrackingUtils.untrack([%{id: character_id}], map_id, self())
socket
end
def handle_server_event(
%{event: :characters_updated},
%{
@@ -276,6 +278,24 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
)}
end
def handle_ui_event(
"startTracking",
%{"character_eve_id" => character_eve_id},
%{
assigns: %{
map_id: map_id,
current_user: %{id: current_user_id}
}
} = socket
)
when not is_nil(character_eve_id) do
{:ok, character} = WandererApp.Character.get_by_eve_id("#{character_eve_id}")
WandererApp.Cache.delete("character:#{character.id}:tracking_paused")
{:noreply, socket}
end
def handle_ui_event(event, body, socket),
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
@@ -295,6 +315,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|> Map.put(:alliance_ticker, Map.get(character, :alliance_ticker, ""))
|> Map.put_new(:ship, WandererApp.Character.get_ship(character))
|> Map.put_new(:location, get_location(character))
|> Map.put_new(:tracking_paused, character |> Map.get(:tracking_paused, false))
defp get_location(character),
do: %{

View File

@@ -329,15 +329,20 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
%{assigns: %{current_user: current_user, has_tracked_characters?: true}} = socket
) do
character_eve_ids
|> Task.async_stream(fn character_eve_id ->
set_autopilot_waypoint(
current_user,
character_eve_id,
add_to_beginning,
clear_other_waypoints,
destination_id
)
end)
|> Task.async_stream(
fn character_eve_id ->
set_autopilot_waypoint(
current_user,
character_eve_id,
add_to_beginning,
clear_other_waypoints,
destination_id
)
end,
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task,
timeout: :timer.minutes(1)
)
|> Enum.map(fn _result -> :skip end)
{:noreply, socket}

View File

@@ -33,7 +33,8 @@ defmodule WandererAppWeb.MapEventHandler do
"getCharactersTrackingInfo",
"updateCharacterTracking",
"updateFollowingCharacter",
"updateMainCharacter"
"updateMainCharacter",
"startTracking"
]
@map_system_events [

View File

@@ -11,9 +11,7 @@ defmodule WandererAppWeb.ServerStatusLive do
"server_status"
)
{:ok, status} = WandererApp.Server.ServerStatusTracker.get_status()
{:ok, socket |> assign(server_online: not status.vip)}
{:ok, socket |> assign(server_online: false)}
end
@impl true

View File

@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.65.19"
@version "1.66.15"
def project do
[