mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-08 00:35:53 +00:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3670ef40a3 | ||
|
|
16d464fba5 | ||
|
|
0b7e0b9cd0 | ||
|
|
dd5fd114d2 | ||
|
|
6e53879344 | ||
|
|
af2bfd4d59 | ||
|
|
a4a34c8ba7 | ||
|
|
8c609f4fdf | ||
|
|
197f5b583f | ||
|
|
4eb4a03e59 | ||
|
|
3d4e66d438 | ||
|
|
ffbc9f169a | ||
|
|
99650187e9 | ||
|
|
92699317cd | ||
|
|
0e48315803 | ||
|
|
868ec246bd | ||
|
|
0030a688c6 | ||
|
|
3ba8f51a2f | ||
|
|
04576b335c | ||
|
|
ea29aa176f | ||
|
|
9a9b7289ba | ||
|
|
d601790864 | ||
|
|
bf58d3ae93 | ||
|
|
d6c32e2d39 | ||
|
|
bdc4948afb | ||
|
|
331db10029 | ||
|
|
2daf9e34d2 | ||
|
|
558cd9b8b3 | ||
|
|
a0f02d0d2f | ||
|
|
9feb8492aa | ||
|
|
e5aa726899 | ||
|
|
93d1c28ccd | ||
|
|
b5ba9200bc | ||
|
|
699d866670 | ||
|
|
c3071344cb | ||
|
|
9e998dd2b6 | ||
|
|
c9accf6079 | ||
|
|
1b41a51004 | ||
|
|
3338dce900 | ||
|
|
1364779f81 | ||
|
|
b49d3423fc | ||
|
|
cccab2a985 | ||
|
|
1abaa90a7d | ||
|
|
6e1993ca8a | ||
|
|
171c821ac4 | ||
|
|
7ebf9186bf | ||
|
|
57d2f2baef | ||
|
|
0aee13878a | ||
|
|
f93ef0ca76 | ||
|
|
4ec03d8338 | ||
|
|
cb318aa6c6 | ||
|
|
733482cd5c | ||
|
|
3969d1287d | ||
|
|
1aa7854b0d | ||
|
|
7b27d4a1a7 | ||
|
|
24ddb8771f | ||
|
|
7134714245 | ||
|
|
96b320ac26 | ||
|
|
1a27b21efe | ||
|
|
b88e121b30 | ||
|
|
4ba4119c2b | ||
|
|
91d1ca201c | ||
|
|
8bf063a228 | ||
|
|
4f53de39b1 | ||
|
|
8c3804f107 | ||
|
|
1be4ec2b90 | ||
|
|
8f0ed44b11 | ||
|
|
cbadfc4ac4 | ||
|
|
3d88ae4452 | ||
|
|
e57f565812 | ||
|
|
da2605ee03 | ||
|
|
07e2196eb4 | ||
|
|
d3b825529e | ||
|
|
ccf9c0db22 | ||
|
|
6378754c57 |
169
CHANGELOG.md
169
CHANGELOG.md
@@ -2,6 +2,175 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.65.3](https://github.com/wanderer-industries/wanderer/compare/v1.65.2...v1.65.3) (2025-05-23)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Signature: Fixed signature clenup
|
||||
|
||||
## [v1.65.2](https://github.com/wanderer-industries/wanderer/compare/v1.65.1...v1.65.2) (2025-05-23)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Signature: Fixed signature updates
|
||||
|
||||
## [v1.65.1](https://github.com/wanderer-industries/wanderer/compare/v1.65.0...v1.65.1) (2025-05-22)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added unsync map events timeout handling (force page refresh if outdated map events found)
|
||||
|
||||
## [v1.65.0](https://github.com/wanderer-industries/wanderer/compare/v1.64.8...v1.65.0) (2025-05-22)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* default connections from c1 holes to medium size
|
||||
|
||||
* support german and french signatures
|
||||
|
||||
* improve signature undo process
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* remove required id field from character schema
|
||||
|
||||
* update openapi spec response types
|
||||
|
||||
* fix issue with connection generation between k-space
|
||||
|
||||
* Signature: Fixed signatures updates
|
||||
|
||||
* update openapi spec for other apis
|
||||
|
||||
## [v1.64.8](https://github.com/wanderer-industries/wanderer/compare/v1.64.7...v1.64.8) (2025-05-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added unsync map events timeout handling (force page refresh if outdated map events found)
|
||||
|
||||
## [v1.64.7](https://github.com/wanderer-industries/wanderer/compare/v1.64.6...v1.64.7) (2025-05-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed connection EOL time refreshed every 2 minutes
|
||||
|
||||
## [v1.64.6](https://github.com/wanderer-industries/wanderer/compare/v1.64.5...v1.64.6) (2025-05-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added map hubs limits checking & a proper warning message shown
|
||||
|
||||
## [v1.64.5](https://github.com/wanderer-industries/wanderer/compare/v1.64.4...v1.64.5) (2025-05-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added character name update on re-auth
|
||||
|
||||
## [v1.64.4](https://github.com/wanderer-industries/wanderer/compare/v1.64.3...v1.64.4) (2025-05-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added 1 min timeout for ship and location updates on ESI API errors
|
||||
|
||||
## [v1.64.3](https://github.com/wanderer-industries/wanderer/compare/v1.64.2...v1.64.3) (2025-05-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed character tracking initialization logic & removed search caching
|
||||
|
||||
## [v1.64.2](https://github.com/wanderer-industries/wanderer/compare/v1.64.1...v1.64.2) (2025-05-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed tracking of ship & location for offline characters
|
||||
|
||||
## [v1.64.1](https://github.com/wanderer-industries/wanderer/compare/v1.64.0...v1.64.1) (2025-05-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed tracking stopped due to server errors
|
||||
|
||||
## [v1.64.0](https://github.com/wanderer-industries/wanderer/compare/v1.63.0...v1.64.0) (2025-05-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* api: add additional structure/signature methods (#365)
|
||||
|
||||
* api: add additional system/connections methods (#351)
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed EOL connections cleanup
|
||||
|
||||
* Core: Avoid Zarzakh system in routes widget
|
||||
|
||||
* remove repeat errors for token refresh (#375)
|
||||
|
||||
* updated openapi spec for character activity (#374)
|
||||
|
||||
* removed error from characters endpoint, and updated routes (#372)
|
||||
|
||||
* cleanup examples for system and connections (#370)
|
||||
|
||||
* remove error on websocket reconnect (#367)
|
||||
|
||||
## [v1.63.0](https://github.com/wanderer-industries/wanderer/compare/v1.62.4...v1.63.0) (2025-05-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Updated map active characters page
|
||||
|
||||
## [v1.62.4](https://github.com/wanderer-industries/wanderer/compare/v1.62.3...v1.62.4) (2025-05-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed map characters got untracked
|
||||
|
||||
## [v1.62.3](https://github.com/wanderer-industries/wanderer/compare/v1.62.2...v1.62.3) (2025-05-08)
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface SignatureViewProps {
|
||||
export const SignatureView = ({ signature, showCharacterPortrait = false }: SignatureViewProps) => {
|
||||
const isWormhole = signature?.group === SignatureGroup.Wormhole;
|
||||
const hasCharacterInfo = showCharacterPortrait && signature.character_eve_id;
|
||||
const groupDisplay = isWormhole ? SignatureGroup.Wormhole : (signature?.group ?? SignatureGroup.CosmicSignature);
|
||||
const groupDisplay = isWormhole ? SignatureGroup.Wormhole : signature?.group ?? SignatureGroup.CosmicSignature;
|
||||
const characterName = signature.character_name || 'Unknown character';
|
||||
|
||||
return (
|
||||
|
||||
@@ -19,7 +19,7 @@ export type HeaderProps = {
|
||||
lazyDeleteValue: boolean;
|
||||
onLazyDeleteChange: (checked: boolean) => void;
|
||||
pendingCount: number;
|
||||
pendingTimeRemaining?: number; // Time remaining in ms
|
||||
undoCountdown?: number;
|
||||
onUndoClick: () => void;
|
||||
onSettingsClick: () => void;
|
||||
};
|
||||
@@ -29,7 +29,7 @@ export const SystemSignaturesHeader = ({
|
||||
lazyDeleteValue,
|
||||
onLazyDeleteChange,
|
||||
pendingCount,
|
||||
pendingTimeRemaining,
|
||||
undoCountdown,
|
||||
onUndoClick,
|
||||
onSettingsClick,
|
||||
}: HeaderProps) => {
|
||||
@@ -43,13 +43,6 @@ export const SystemSignaturesHeader = ({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isCompact = useMaxWidth(containerRef, COMPACT_MAX_WIDTH);
|
||||
|
||||
// Format time remaining as seconds
|
||||
const formatTimeRemaining = () => {
|
||||
if (!pendingTimeRemaining) return '';
|
||||
const seconds = Math.ceil(pendingTimeRemaining / 1000);
|
||||
return ` (${seconds}s remaining)`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="w-full">
|
||||
<div className="flex justify-between items-center text-xs w-full h-full">
|
||||
@@ -78,7 +71,9 @@ export const SystemSignaturesHeader = ({
|
||||
<WdImgButton
|
||||
className={PrimeIcons.UNDO}
|
||||
style={{ color: 'red' }}
|
||||
tooltip={{ content: `Undo pending changes (${pendingCount})${formatTimeRemaining()}` }}
|
||||
tooltip={{
|
||||
content: `Undo pending deletions (${pendingCount})${undoCountdown && undoCountdown > 0 ? ` — ${undoCountdown}s left` : ''}`,
|
||||
}}
|
||||
onClick={onUndoClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,99 +1,156 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { SystemSignaturesContent } from './SystemSignaturesContent';
|
||||
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
|
||||
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useHotkey } from '@/hooks/Mapper/hooks';
|
||||
import { SystemSignaturesHeader } from './SystemSignatureHeader';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
|
||||
import {
|
||||
SETTINGS_KEYS,
|
||||
SETTINGS_VALUES,
|
||||
SIGNATURE_DELETION_TIMEOUTS,
|
||||
SIGNATURE_SETTING_STORE_KEY,
|
||||
SIGNATURE_WINDOW_ID,
|
||||
SIGNATURES_DELETION_TIMING,
|
||||
SignatureSettingsType,
|
||||
SIGNATURES_DELETION_TIMING,
|
||||
SIGNATURE_DELETION_TIMEOUTS,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { calculateTimeRemaining } from './helpers';
|
||||
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
|
||||
|
||||
export const SystemSignatures = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [sigCount, setSigCount] = useState<number>(0);
|
||||
const [pendingSigs, setPendingSigs] = useState<SystemSignature[]>([]);
|
||||
const [pendingTimeRemaining, setPendingTimeRemaining] = useState<number | undefined>();
|
||||
const undoPendingFnRef = useRef<() => void>(() => {});
|
||||
/**
|
||||
* Custom hook for managing pending signature deletions and undo countdown.
|
||||
*/
|
||||
function useSignatureUndo(
|
||||
systemId: string | undefined,
|
||||
settings: SignatureSettingsType,
|
||||
outCommand: OutCommandHandler,
|
||||
) {
|
||||
const [countdown, setCountdown] = useState<number>(0);
|
||||
const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
|
||||
const {
|
||||
data: { selectedSystems },
|
||||
} = useMapRootState();
|
||||
|
||||
const [currentSettings, setCurrentSettings] = useLocalStorageState(SIGNATURE_SETTING_STORE_KEY, {
|
||||
defaultValue: SETTINGS_VALUES,
|
||||
});
|
||||
|
||||
const handleSigCountChange = useCallback((count: number) => {
|
||||
setSigCount(count);
|
||||
const addDeleted = useCallback((ids: string[]) => {
|
||||
setPendingIds(prev => {
|
||||
const next = new Set(prev);
|
||||
ids.forEach(id => next.add(id));
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
|
||||
const handleSettingsChange = useCallback((newSettings: SignatureSettingsType) => {
|
||||
setCurrentSettings(newSettings);
|
||||
setVisible(false);
|
||||
}, []);
|
||||
|
||||
const handleLazyDeleteChange = useCallback((value: boolean) => {
|
||||
setCurrentSettings(prev => ({ ...prev, [SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value }));
|
||||
}, []);
|
||||
|
||||
useHotkey(true, ['z'], event => {
|
||||
if (pendingSigs.length > 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
undoPendingFnRef.current();
|
||||
setPendingSigs([]);
|
||||
setPendingTimeRemaining(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
const handleUndoClick = useCallback(() => {
|
||||
undoPendingFnRef.current();
|
||||
setPendingSigs([]);
|
||||
setPendingTimeRemaining(undefined);
|
||||
}, []);
|
||||
|
||||
const handleSettingsButtonClick = useCallback(() => {
|
||||
setVisible(true);
|
||||
}, []);
|
||||
|
||||
const handlePendingChange = useCallback(
|
||||
(pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>, newUndo: () => void) => {
|
||||
setPendingSigs(() => {
|
||||
return Object.values(pending.current).filter(sig => sig.pendingDeletion);
|
||||
});
|
||||
undoPendingFnRef.current = newUndo;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Calculate the minimum time remaining for any pending signature
|
||||
// kick off or clear countdown whenever pendingIds changes
|
||||
useEffect(() => {
|
||||
if (pendingSigs.length === 0) {
|
||||
setPendingTimeRemaining(undefined);
|
||||
// clear any existing timer
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
|
||||
if (pendingIds.size === 0) {
|
||||
setCountdown(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const calculate = () => {
|
||||
setPendingTimeRemaining(() => calculateTimeRemaining(pendingSigs));
|
||||
};
|
||||
// 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;
|
||||
setCountdown(Math.ceil(timeoutMs / 1000));
|
||||
|
||||
calculate();
|
||||
const interval = setInterval(calculate, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [pendingSigs]);
|
||||
// start new interval
|
||||
intervalRef.current = window.setInterval(() => {
|
||||
setCountdown(prev => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(intervalRef.current!);
|
||||
intervalRef.current = null;
|
||||
setPendingIds(new Set());
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [pendingIds, settings[SETTINGS_KEYS.DELETION_TIMING]]);
|
||||
|
||||
// undo handler
|
||||
const handleUndo = useCallback(async () => {
|
||||
if (!systemId || pendingIds.size === 0) return;
|
||||
await outCommand({
|
||||
type: OutCommand.undoDeleteSignatures,
|
||||
data: { system_id: systemId, eve_ids: Array.from(pendingIds) },
|
||||
});
|
||||
setPendingIds(new Set());
|
||||
setCountdown(0);
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, [systemId, pendingIds, outCommand]);
|
||||
|
||||
return {
|
||||
pendingIds,
|
||||
countdown,
|
||||
addDeleted,
|
||||
handleUndo,
|
||||
};
|
||||
}
|
||||
|
||||
export const SystemSignatures = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [sigCount, setSigCount] = useState(0);
|
||||
|
||||
const {
|
||||
data: { selectedSystems },
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
|
||||
const [currentSettings, setCurrentSettings] = useLocalStorageState<SignatureSettingsType>(
|
||||
SIGNATURE_SETTING_STORE_KEY,
|
||||
{
|
||||
defaultValue: SETTINGS_VALUES,
|
||||
},
|
||||
);
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
|
||||
const { pendingIds, countdown, addDeleted, handleUndo } = useSignatureUndo(systemId, currentSettings, outCommand);
|
||||
|
||||
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
|
||||
if (pendingIds.size > 0 && countdown > 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleUndo();
|
||||
}
|
||||
});
|
||||
|
||||
const handleCountChange = useCallback((count: number) => {
|
||||
setSigCount(count);
|
||||
}, []);
|
||||
|
||||
const handleSettingsSave = useCallback(
|
||||
(newSettings: SignatureSettingsType) => {
|
||||
setCurrentSettings(newSettings);
|
||||
setVisible(false);
|
||||
},
|
||||
[setCurrentSettings],
|
||||
);
|
||||
|
||||
const handleLazyDeleteToggle = useCallback(
|
||||
(value: boolean) => {
|
||||
setCurrentSettings(prev => ({
|
||||
...prev,
|
||||
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value,
|
||||
}));
|
||||
},
|
||||
[setCurrentSettings],
|
||||
);
|
||||
|
||||
const openSettings = useCallback(() => setVisible(true), []);
|
||||
|
||||
return (
|
||||
<Widget
|
||||
@@ -101,16 +158,16 @@ export const SystemSignatures = () => {
|
||||
<SystemSignaturesHeader
|
||||
sigCount={sigCount}
|
||||
lazyDeleteValue={currentSettings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
|
||||
pendingCount={pendingSigs.length}
|
||||
pendingTimeRemaining={pendingTimeRemaining}
|
||||
onLazyDeleteChange={handleLazyDeleteChange}
|
||||
onUndoClick={handleUndoClick}
|
||||
onSettingsClick={handleSettingsButtonClick}
|
||||
pendingCount={pendingIds.size}
|
||||
undoCountdown={countdown}
|
||||
onLazyDeleteChange={handleLazyDeleteToggle}
|
||||
onUndoClick={handleUndo}
|
||||
onSettingsClick={openSettings}
|
||||
/>
|
||||
}
|
||||
windowId={SIGNATURE_WINDOW_ID}
|
||||
>
|
||||
{isNotSelectedSystem ? (
|
||||
{!isSystemSelected ? (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
System is not selected
|
||||
</div>
|
||||
@@ -118,22 +175,17 @@ export const SystemSignatures = () => {
|
||||
<SystemSignaturesContent
|
||||
systemId={systemId}
|
||||
settings={currentSettings}
|
||||
deletionTiming={
|
||||
SIGNATURE_DELETION_TIMEOUTS[
|
||||
(currentSettings[SETTINGS_KEYS.DELETION_TIMING] as keyof typeof SIGNATURE_DELETION_TIMEOUTS) ||
|
||||
SIGNATURES_DELETION_TIMING.DEFAULT
|
||||
] as number
|
||||
}
|
||||
onLazyDeleteChange={handleLazyDeleteChange}
|
||||
onCountChange={handleSigCountChange}
|
||||
onPendingChange={handlePendingChange}
|
||||
onLazyDeleteChange={handleLazyDeleteToggle}
|
||||
onCountChange={handleCountChange}
|
||||
onSignatureDeleted={addDeleted}
|
||||
/>
|
||||
)}
|
||||
|
||||
{visible && (
|
||||
<SystemSignatureSettingsDialog
|
||||
settings={currentSettings}
|
||||
onCancel={() => setVisible(false)}
|
||||
onSave={handleSettingsChange}
|
||||
onSave={handleSettingsSave}
|
||||
/>
|
||||
)}
|
||||
</Widget>
|
||||
|
||||
@@ -57,12 +57,8 @@ interface SystemSignaturesContentProps {
|
||||
onSelect?: (signature: SystemSignature) => void;
|
||||
onLazyDeleteChange?: (value: boolean) => void;
|
||||
onCountChange?: (count: number) => void;
|
||||
onPendingChange?: (
|
||||
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
|
||||
undo: () => void,
|
||||
) => void;
|
||||
deletionTiming?: number;
|
||||
filterSignature?: (signature: SystemSignature) => boolean;
|
||||
onSignatureDeleted?: (deletedIds: string[]) => void;
|
||||
}
|
||||
|
||||
export const SystemSignaturesContent = ({
|
||||
@@ -73,9 +69,8 @@ export const SystemSignaturesContent = ({
|
||||
onSelect,
|
||||
onLazyDeleteChange,
|
||||
onCountChange,
|
||||
onPendingChange,
|
||||
deletionTiming,
|
||||
filterSignature,
|
||||
onSignatureDeleted,
|
||||
}: SystemSignaturesContentProps) => {
|
||||
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
|
||||
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
|
||||
@@ -95,15 +90,21 @@ export const SystemSignaturesContent = ({
|
||||
{ defaultValue: SORT_DEFAULT_VALUES },
|
||||
);
|
||||
|
||||
const { signatures, selectedSignatures, setSelectedSignatures, handleDeleteSelected, handleSelectAll, handlePaste } =
|
||||
useSystemSignaturesData({
|
||||
systemId,
|
||||
settings,
|
||||
onCountChange,
|
||||
onPendingChange,
|
||||
onLazyDeleteChange,
|
||||
deletionTiming,
|
||||
});
|
||||
const {
|
||||
signatures,
|
||||
selectedSignatures,
|
||||
setSelectedSignatures,
|
||||
handleDeleteSelected,
|
||||
handleSelectAll,
|
||||
handlePaste,
|
||||
hasUnsupportedLanguage,
|
||||
} = useSystemSignaturesData({
|
||||
systemId,
|
||||
settings,
|
||||
onCountChange,
|
||||
onLazyDeleteChange,
|
||||
onSignatureDeleted,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (selectable) return;
|
||||
@@ -125,6 +126,10 @@ export const SystemSignaturesContent = ({
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (onSignatureDeleted && selectedSignatures.length > 0) {
|
||||
const deletedIds = selectedSignatures.map(s => s.eve_id);
|
||||
onSignatureDeleted(deletedIds);
|
||||
}
|
||||
handleDeleteSelected();
|
||||
});
|
||||
|
||||
@@ -155,7 +160,7 @@ export const SystemSignaturesContent = ({
|
||||
(e: { value: SystemSignature[] }) => {
|
||||
selectable ? onSelect?.(e.value[0]) : setSelectedSignatures(e.value as ExtendedSystemSignature[]);
|
||||
},
|
||||
[selectable],
|
||||
[onSelect, selectable, setSelectedSignatures],
|
||||
);
|
||||
|
||||
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
|
||||
@@ -188,7 +193,11 @@ export const SystemSignaturesContent = ({
|
||||
x => GROUPS_LIST.includes(x as SignatureGroup) && settings[x as SETTINGS_KEYS],
|
||||
);
|
||||
|
||||
return enabledGroups.includes(getGroupIdByRawGroup(sig.group));
|
||||
const mappedGroup = getGroupIdByRawGroup(sig.group);
|
||||
if (!mappedGroup) {
|
||||
return true; // If we can't determine the group, still show it
|
||||
}
|
||||
return enabledGroups.includes(mappedGroup);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -236,113 +245,121 @@ export const SystemSignaturesContent = ({
|
||||
No signatures
|
||||
</div>
|
||||
) : (
|
||||
<DataTable
|
||||
value={filteredSignatures}
|
||||
size="small"
|
||||
selectionMode="multiple"
|
||||
selection={selectedSignatures}
|
||||
metaKeySelection
|
||||
onSelectionChange={handleSelectSignatures}
|
||||
dataKey="eve_id"
|
||||
className="w-full select-none"
|
||||
resizableColumns={false}
|
||||
rowHover
|
||||
selectAll
|
||||
onRowDoubleClick={handleRowClick}
|
||||
sortField={sortSettings.sortField}
|
||||
sortOrder={sortSettings.sortOrder}
|
||||
onSort={handleSortSettings}
|
||||
onRowMouseEnter={onRowMouseEnter}
|
||||
onRowMouseLeave={onRowMouseLeave}
|
||||
// @ts-ignore
|
||||
rowClassName={getRowClassName}
|
||||
>
|
||||
<Column
|
||||
field="icon"
|
||||
header=""
|
||||
body={renderColIcon}
|
||||
bodyClassName="p-0 px-1"
|
||||
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
|
||||
/>
|
||||
<Column
|
||||
field="eve_id"
|
||||
header="Id"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={{ maxWidth: 72, minWidth: 72, width: 72 }}
|
||||
sortable
|
||||
/>
|
||||
<Column
|
||||
field="group"
|
||||
header="Group"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
|
||||
body={sig => sig.group ?? ''}
|
||||
hidden={isCompact}
|
||||
sortable
|
||||
/>
|
||||
<Column
|
||||
field="info"
|
||||
header="Info"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={{ maxWidth: nameColumnWidth }}
|
||||
hidden={isCompact || isMedium}
|
||||
body={renderInfoColumn}
|
||||
/>
|
||||
{showDescriptionColumn && (
|
||||
<Column
|
||||
field="description"
|
||||
header="Description"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
hidden={isCompact}
|
||||
body={renderDescription}
|
||||
sortable
|
||||
/>
|
||||
<>
|
||||
{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.
|
||||
</div>
|
||||
)}
|
||||
<Column
|
||||
field="inserted_at"
|
||||
header="Added"
|
||||
dataType="date"
|
||||
body={renderAddedTimeLeft}
|
||||
style={{ minWidth: 70, maxWidth: 80 }}
|
||||
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
/>
|
||||
{showUpdatedColumn && (
|
||||
<Column
|
||||
field="updated_at"
|
||||
header="Updated"
|
||||
dataType="date"
|
||||
body={renderUpdatedTimeLeft}
|
||||
style={{ minWidth: 70, maxWidth: 80 }}
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCharacterColumn && (
|
||||
<Column
|
||||
field="character_name"
|
||||
header="Character"
|
||||
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
></Column>
|
||||
)}
|
||||
|
||||
{!selectable && (
|
||||
<DataTable
|
||||
value={filteredSignatures}
|
||||
size="small"
|
||||
selectionMode="multiple"
|
||||
selection={selectedSignatures}
|
||||
metaKeySelection
|
||||
onSelectionChange={handleSelectSignatures}
|
||||
dataKey="eve_id"
|
||||
className="w-full select-none"
|
||||
resizableColumns={false}
|
||||
rowHover
|
||||
selectAll
|
||||
onRowDoubleClick={handleRowClick}
|
||||
sortField={sortSettings.sortField}
|
||||
sortOrder={sortSettings.sortOrder}
|
||||
onSort={handleSortSettings}
|
||||
onRowMouseEnter={onRowMouseEnter}
|
||||
onRowMouseLeave={onRowMouseLeave}
|
||||
// @ts-ignore
|
||||
rowClassName={getRowClassName}
|
||||
>
|
||||
<Column
|
||||
field="icon"
|
||||
header=""
|
||||
body={() => (
|
||||
<div className="flex justify-end items-center gap-2 mr-[4px]">
|
||||
<WdTooltipWrapper content="Double-click a row to edit signature">
|
||||
<span className={PrimeIcons.PENCIL + ' text-[10px]'} />
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
)}
|
||||
body={renderColIcon}
|
||||
bodyClassName="p-0 px-1"
|
||||
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
|
||||
bodyClassName="p-0 pl-1 pr-2"
|
||||
/>
|
||||
)}
|
||||
</DataTable>
|
||||
<Column
|
||||
field="eve_id"
|
||||
header="Id"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={{ maxWidth: 72, minWidth: 72, width: 72 }}
|
||||
sortable
|
||||
/>
|
||||
<Column
|
||||
field="group"
|
||||
header="Group"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
|
||||
body={sig => sig.group ?? ''}
|
||||
hidden={isCompact}
|
||||
sortable
|
||||
/>
|
||||
<Column
|
||||
field="info"
|
||||
header="Info"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={{ maxWidth: nameColumnWidth }}
|
||||
hidden={isCompact || isMedium}
|
||||
body={renderInfoColumn}
|
||||
/>
|
||||
{showDescriptionColumn && (
|
||||
<Column
|
||||
field="description"
|
||||
header="Description"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
hidden={isCompact}
|
||||
body={renderDescription}
|
||||
sortable
|
||||
/>
|
||||
)}
|
||||
<Column
|
||||
field="inserted_at"
|
||||
header="Added"
|
||||
dataType="date"
|
||||
body={renderAddedTimeLeft}
|
||||
style={{ minWidth: 70, maxWidth: 80 }}
|
||||
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
/>
|
||||
{showUpdatedColumn && (
|
||||
<Column
|
||||
field="updated_at"
|
||||
header="Updated"
|
||||
dataType="date"
|
||||
body={renderUpdatedTimeLeft}
|
||||
style={{ minWidth: 70, maxWidth: 80 }}
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCharacterColumn && (
|
||||
<Column
|
||||
field="character_name"
|
||||
header="Character"
|
||||
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
></Column>
|
||||
)}
|
||||
|
||||
{!selectable && (
|
||||
<Column
|
||||
header=""
|
||||
body={() => (
|
||||
<div className="flex justify-end items-center gap-2 mr-[4px]">
|
||||
<WdTooltipWrapper content="Double-click a row to edit signature">
|
||||
<span className={PrimeIcons.PENCIL + ' text-[10px]'} />
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
)}
|
||||
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
|
||||
bodyClassName="p-0 pl-1 pr-2"
|
||||
/>
|
||||
)}
|
||||
</DataTable>
|
||||
</>
|
||||
)}
|
||||
|
||||
<WdTooltip
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import {
|
||||
GroupType,
|
||||
SignatureGroup,
|
||||
SignatureGroupDE,
|
||||
SignatureGroupENG,
|
||||
SignatureGroupFR,
|
||||
SignatureGroupRU,
|
||||
SignatureKind,
|
||||
SignatureKindDE,
|
||||
SignatureKindENG,
|
||||
SignatureKindFR,
|
||||
SignatureKindRU,
|
||||
} from '@/hooks/Mapper/types';
|
||||
|
||||
@@ -40,46 +44,58 @@ export const GROUPS: Record<SignatureGroup, GroupType> = {
|
||||
[SignatureGroup.CosmicSignature]: { id: SignatureGroup.CosmicSignature, icon: '/icons/x_close14.png', w: 9, h: 9 },
|
||||
};
|
||||
|
||||
export const MAPPING_GROUP_TO_ENG = {
|
||||
// ENGLISH
|
||||
[SignatureGroupENG.GasSite]: SignatureGroup.GasSite,
|
||||
[SignatureGroupENG.RelicSite]: SignatureGroup.RelicSite,
|
||||
[SignatureGroupENG.DataSite]: SignatureGroup.DataSite,
|
||||
[SignatureGroupENG.OreSite]: SignatureGroup.OreSite,
|
||||
[SignatureGroupENG.CombatSite]: SignatureGroup.CombatSite,
|
||||
[SignatureGroupENG.Wormhole]: SignatureGroup.Wormhole,
|
||||
[SignatureGroupENG.CosmicSignature]: SignatureGroup.CosmicSignature,
|
||||
|
||||
// RUSSIAN
|
||||
[SignatureGroupRU.GasSite]: SignatureGroup.GasSite,
|
||||
[SignatureGroupRU.RelicSite]: SignatureGroup.RelicSite,
|
||||
[SignatureGroupRU.DataSite]: SignatureGroup.DataSite,
|
||||
[SignatureGroupRU.OreSite]: SignatureGroup.OreSite,
|
||||
[SignatureGroupRU.CombatSite]: SignatureGroup.CombatSite,
|
||||
[SignatureGroupRU.Wormhole]: SignatureGroup.Wormhole,
|
||||
[SignatureGroupRU.CosmicSignature]: SignatureGroup.CosmicSignature,
|
||||
export const LANGUAGE_GROUP_MAPPINGS = {
|
||||
EN: {
|
||||
[SignatureGroupENG.GasSite]: SignatureGroup.GasSite,
|
||||
[SignatureGroupENG.RelicSite]: SignatureGroup.RelicSite,
|
||||
[SignatureGroupENG.DataSite]: SignatureGroup.DataSite,
|
||||
[SignatureGroupENG.OreSite]: SignatureGroup.OreSite,
|
||||
[SignatureGroupENG.CombatSite]: SignatureGroup.CombatSite,
|
||||
[SignatureGroupENG.Wormhole]: SignatureGroup.Wormhole,
|
||||
[SignatureGroupENG.CosmicSignature]: SignatureGroup.CosmicSignature,
|
||||
},
|
||||
RU: {
|
||||
[SignatureGroupRU.GasSite]: SignatureGroup.GasSite,
|
||||
[SignatureGroupRU.RelicSite]: SignatureGroup.RelicSite,
|
||||
[SignatureGroupRU.DataSite]: SignatureGroup.DataSite,
|
||||
[SignatureGroupRU.OreSite]: SignatureGroup.OreSite,
|
||||
[SignatureGroupRU.CombatSite]: SignatureGroup.CombatSite,
|
||||
[SignatureGroupRU.Wormhole]: SignatureGroup.Wormhole,
|
||||
[SignatureGroupRU.CosmicSignature]: SignatureGroup.CosmicSignature,
|
||||
},
|
||||
FR: {
|
||||
[SignatureGroupFR.GasSite]: SignatureGroup.GasSite,
|
||||
[SignatureGroupFR.RelicSite]: SignatureGroup.RelicSite,
|
||||
[SignatureGroupFR.DataSite]: SignatureGroup.DataSite,
|
||||
[SignatureGroupFR.OreSite]: SignatureGroup.OreSite,
|
||||
[SignatureGroupFR.CombatSite]: SignatureGroup.CombatSite,
|
||||
[SignatureGroupFR.Wormhole]: SignatureGroup.Wormhole,
|
||||
[SignatureGroupFR.CosmicSignature]: SignatureGroup.CosmicSignature,
|
||||
},
|
||||
DE: {
|
||||
[SignatureGroupDE.GasSite]: SignatureGroup.GasSite,
|
||||
[SignatureGroupDE.RelicSite]: SignatureGroup.RelicSite,
|
||||
[SignatureGroupDE.DataSite]: SignatureGroup.DataSite,
|
||||
[SignatureGroupDE.OreSite]: SignatureGroup.OreSite,
|
||||
[SignatureGroupDE.CombatSite]: SignatureGroup.CombatSite,
|
||||
[SignatureGroupDE.Wormhole]: SignatureGroup.Wormhole,
|
||||
[SignatureGroupDE.CosmicSignature]: SignatureGroup.CosmicSignature,
|
||||
},
|
||||
};
|
||||
|
||||
export const MAPPING_TYPE_TO_ENG = {
|
||||
// ENGLISH
|
||||
[SignatureKindENG.CosmicSignature]: SignatureKind.CosmicSignature,
|
||||
[SignatureKindENG.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
|
||||
[SignatureKindENG.Structure]: SignatureKind.Structure,
|
||||
[SignatureKindENG.Ship]: SignatureKind.Ship,
|
||||
[SignatureKindENG.Deployable]: SignatureKind.Deployable,
|
||||
[SignatureKindENG.Drone]: SignatureKind.Drone,
|
||||
// Flatten the structure for backward compatibility
|
||||
export const MAPPING_GROUP_TO_ENG: Record<string, SignatureGroup> = (() => {
|
||||
const flattened: Record<string, SignatureGroup> = {};
|
||||
for (const [, mappings] of Object.entries(LANGUAGE_GROUP_MAPPINGS)) {
|
||||
Object.assign(flattened, mappings);
|
||||
}
|
||||
return flattened;
|
||||
})();
|
||||
|
||||
// RUSSIAN
|
||||
[SignatureKindRU.CosmicSignature]: SignatureKind.CosmicSignature,
|
||||
[SignatureKindRU.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
|
||||
[SignatureKindRU.Structure]: SignatureKind.Structure,
|
||||
[SignatureKindRU.Ship]: SignatureKind.Ship,
|
||||
[SignatureKindRU.Deployable]: SignatureKind.Deployable,
|
||||
[SignatureKindRU.Drone]: SignatureKind.Drone,
|
||||
export const getGroupIdByRawGroup = (val: string): SignatureGroup | undefined => {
|
||||
return MAPPING_GROUP_TO_ENG[val] || undefined;
|
||||
};
|
||||
|
||||
export const getGroupIdByRawGroup = (val: string) => MAPPING_GROUP_TO_ENG[val as SignatureGroup];
|
||||
|
||||
export const SIGNATURE_WINDOW_ID = 'system_signatures_window';
|
||||
export const SIGNATURE_SETTING_STORE_KEY = 'wanderer_system_signature_settings_v6_5';
|
||||
|
||||
@@ -123,7 +139,7 @@ export type Setting = {
|
||||
name: string;
|
||||
type: SettingsTypes;
|
||||
isSeparator?: boolean;
|
||||
options?: { label: string; value: any }[];
|
||||
options?: { label: string; value: number | string | boolean }[];
|
||||
};
|
||||
|
||||
export enum SIGNATURES_DELETION_TIMING {
|
||||
@@ -208,3 +224,52 @@ export const SIGNATURE_DELETION_TIMEOUTS: SignatureDeletionTimingType = {
|
||||
[SIGNATURES_DELETION_TIMING.IMMEDIATE]: 0,
|
||||
[SIGNATURES_DELETION_TIMING.EXTENDED]: 30_000,
|
||||
};
|
||||
|
||||
// Replace the flat structure with a nested structure by language
|
||||
export const LANGUAGE_TYPE_MAPPINGS = {
|
||||
EN: {
|
||||
[SignatureKindENG.CosmicSignature]: SignatureKind.CosmicSignature,
|
||||
[SignatureKindENG.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
|
||||
[SignatureKindENG.Structure]: SignatureKind.Structure,
|
||||
[SignatureKindENG.Ship]: SignatureKind.Ship,
|
||||
[SignatureKindENG.Deployable]: SignatureKind.Deployable,
|
||||
[SignatureKindENG.Drone]: SignatureKind.Drone,
|
||||
[SignatureKindENG.Starbase]: SignatureKind.Starbase,
|
||||
},
|
||||
RU: {
|
||||
[SignatureKindRU.CosmicSignature]: SignatureKind.CosmicSignature,
|
||||
[SignatureKindRU.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
|
||||
[SignatureKindRU.Structure]: SignatureKind.Structure,
|
||||
[SignatureKindRU.Ship]: SignatureKind.Ship,
|
||||
[SignatureKindRU.Deployable]: SignatureKind.Deployable,
|
||||
[SignatureKindRU.Drone]: SignatureKind.Drone,
|
||||
[SignatureKindRU.Starbase]: SignatureKind.Starbase,
|
||||
},
|
||||
FR: {
|
||||
[SignatureKindFR.CosmicSignature]: SignatureKind.CosmicSignature,
|
||||
[SignatureKindFR.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
|
||||
[SignatureKindFR.Structure]: SignatureKind.Structure,
|
||||
[SignatureKindFR.Ship]: SignatureKind.Ship,
|
||||
[SignatureKindFR.Deployable]: SignatureKind.Deployable,
|
||||
[SignatureKindFR.Drone]: SignatureKind.Drone,
|
||||
[SignatureKindFR.Starbase]: SignatureKind.Starbase,
|
||||
},
|
||||
DE: {
|
||||
[SignatureKindDE.CosmicSignature]: SignatureKind.CosmicSignature,
|
||||
[SignatureKindDE.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
|
||||
[SignatureKindDE.Structure]: SignatureKind.Structure,
|
||||
[SignatureKindDE.Ship]: SignatureKind.Ship,
|
||||
[SignatureKindDE.Deployable]: SignatureKind.Deployable,
|
||||
[SignatureKindDE.Drone]: SignatureKind.Drone,
|
||||
[SignatureKindDE.Starbase]: SignatureKind.Starbase,
|
||||
},
|
||||
};
|
||||
|
||||
// Flatten the structure for backward compatibility
|
||||
export const MAPPING_TYPE_TO_ENG: Record<string, SignatureKind> = (() => {
|
||||
const flattened: Record<string, SignatureKind> = {};
|
||||
for (const [, mappings] of Object.entries(LANGUAGE_TYPE_MAPPINGS)) {
|
||||
Object.assign(flattened, mappings);
|
||||
}
|
||||
return flattened;
|
||||
})();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { getState } from './getState';
|
||||
|
||||
/**
|
||||
@@ -22,6 +22,7 @@ export const getActualSigs = (
|
||||
|
||||
oldSignatures.forEach(oldSig => {
|
||||
const newSig = newSignatures.find(s => s.eve_id === oldSig.eve_id);
|
||||
|
||||
if (newSig) {
|
||||
const needUpgrade = getState(GROUPS_LIST, newSig) > getState(GROUPS_LIST, oldSig);
|
||||
const mergedSig = { ...oldSig };
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { UNKNOWN_SIGNATURE_NAME } from '@/hooks/Mapper/helpers';
|
||||
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
export const getState = (_: string[], newSig: SystemSignature) => {
|
||||
let state = -1;
|
||||
if (!newSig.group) {
|
||||
if (!newSig.group || newSig.group === SignatureGroup.CosmicSignature) {
|
||||
state = 0;
|
||||
} else if (!newSig.name || newSig.name === '') {
|
||||
} else if (!newSig.name || newSig.name === '' || newSig.name === UNKNOWN_SIGNATURE_NAME) {
|
||||
state = 1;
|
||||
} else if (newSig.name !== '') {
|
||||
state = 2;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { useCallback, useRef, useEffect } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { prepareUpdatePayload, scheduleLazyTimers } from '../helpers';
|
||||
import { prepareUpdatePayload } from '../helpers';
|
||||
import { UsePendingDeletionParams } from './types';
|
||||
import { FINAL_DURATION_MS } from '../constants';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
export function usePendingDeletions({
|
||||
systemId,
|
||||
setSignatures,
|
||||
deletionTiming,
|
||||
onPendingChange,
|
||||
}: UsePendingDeletionParams) {
|
||||
}: Omit<UsePendingDeletionParams, 'deletionTiming'>) {
|
||||
const { outCommand } = useMapRootState();
|
||||
const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({});
|
||||
|
||||
// Use the provided deletion timing or fall back to the default
|
||||
const finalDuration = deletionTiming !== undefined ? deletionTiming : FINAL_DURATION_MS;
|
||||
|
||||
const processRemovedSignatures = useCallback(
|
||||
async (
|
||||
removed: ExtendedSystemSignature[],
|
||||
@@ -25,63 +20,15 @@ export function usePendingDeletions({
|
||||
updated: ExtendedSystemSignature[],
|
||||
) => {
|
||||
if (!removed.length) return;
|
||||
|
||||
// If deletion timing is 0, immediately delete without pending state
|
||||
if (finalDuration === 0) {
|
||||
await outCommand({
|
||||
type: OutCommand.updateSignatures,
|
||||
data: prepareUpdatePayload(systemId, added, updated, removed),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const processedRemoved = removed.map(r => ({
|
||||
...r,
|
||||
pendingDeletion: true,
|
||||
pendingUntil: now + finalDuration,
|
||||
}));
|
||||
pendingDeletionMapRef.current = {
|
||||
...pendingDeletionMapRef.current,
|
||||
...processedRemoved.reduce((acc: any, sig) => {
|
||||
acc[sig.eve_id] = sig;
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
|
||||
|
||||
setSignatures(prev =>
|
||||
prev.map(sig => {
|
||||
if (processedRemoved.find(r => r.eve_id === sig.eve_id)) {
|
||||
return { ...sig, pendingDeletion: true, pendingUntil: now + finalDuration };
|
||||
}
|
||||
return sig;
|
||||
}),
|
||||
);
|
||||
|
||||
scheduleLazyTimers(
|
||||
processedRemoved,
|
||||
pendingDeletionMapRef,
|
||||
async sig => {
|
||||
await outCommand({
|
||||
type: OutCommand.updateSignatures,
|
||||
data: prepareUpdatePayload(systemId, [], [], [sig]),
|
||||
});
|
||||
delete pendingDeletionMapRef.current[sig.eve_id];
|
||||
setSignatures(prev => prev.filter(x => x.eve_id !== sig.eve_id));
|
||||
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
|
||||
},
|
||||
finalDuration,
|
||||
);
|
||||
await outCommand({
|
||||
type: OutCommand.updateSignatures,
|
||||
data: prepareUpdatePayload(systemId, added, updated, removed),
|
||||
});
|
||||
},
|
||||
[systemId, outCommand, finalDuration],
|
||||
[systemId, outCommand],
|
||||
);
|
||||
|
||||
const clearPendingDeletions = useCallback(() => {
|
||||
Object.values(pendingDeletionMapRef.current).forEach(({ finalTimeoutId }) => {
|
||||
clearTimeout(finalTimeoutId);
|
||||
});
|
||||
pendingDeletionMapRef.current = {};
|
||||
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
|
||||
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
|
||||
|
||||
@@ -18,16 +18,18 @@ export const useSystemSignaturesData = ({
|
||||
onCountChange,
|
||||
onPendingChange,
|
||||
onLazyDeleteChange,
|
||||
deletionTiming,
|
||||
}: UseSystemSignaturesDataProps) => {
|
||||
onSignatureDeleted,
|
||||
}: Omit<UseSystemSignaturesDataProps, 'deletionTiming'> & {
|
||||
onSignatureDeleted?: (deletedIds: string[]) => void;
|
||||
}) => {
|
||||
const { outCommand } = useMapRootState();
|
||||
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
|
||||
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
|
||||
const [hasUnsupportedLanguage, setHasUnsupportedLanguage] = useState<boolean>(false);
|
||||
|
||||
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
|
||||
systemId,
|
||||
setSignatures,
|
||||
deletionTiming,
|
||||
onPendingChange,
|
||||
});
|
||||
|
||||
@@ -42,6 +44,7 @@ export const useSystemSignaturesData = ({
|
||||
async (clipboardString: string) => {
|
||||
const lazyDeleteValue = settings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean;
|
||||
|
||||
// Parse the incoming signatures
|
||||
const incomingSignatures = parseSignatures(
|
||||
clipboardString,
|
||||
Object.keys(settings).filter(skey => skey in SignatureKind),
|
||||
@@ -51,6 +54,18 @@ export const useSystemSignaturesData = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if any signatures might be using unsupported languages
|
||||
// This is a basic heuristic: if we have signatures where the original group wasn't mapped
|
||||
const clipboardRows = clipboardString.split('\n').filter(row => row.trim() !== '');
|
||||
const detectedSignatureCount = clipboardRows.filter(row => row.match(/^[A-Z]{3}-\d{3}/)).length;
|
||||
|
||||
// If we detected valid IDs but got fewer parsed signatures, we might have language issues
|
||||
if (detectedSignatureCount > 0 && incomingSignatures.length < detectedSignatureCount) {
|
||||
setHasUnsupportedLanguage(true);
|
||||
} else {
|
||||
setHasUnsupportedLanguage(false);
|
||||
}
|
||||
|
||||
const currentNonPending = lazyDeleteValue
|
||||
? signaturesRef.current.filter(sig => !sig.pendingDeletion)
|
||||
: signaturesRef.current.filter(sig => !sig.pendingDeletion || !sig.pendingAddition);
|
||||
@@ -59,6 +74,10 @@ export const useSystemSignaturesData = ({
|
||||
|
||||
if (removed.length > 0) {
|
||||
await processRemovedSignatures(removed, added, updated);
|
||||
if (onSignatureDeleted) {
|
||||
const deletedIds = removed.map(sig => sig.eve_id);
|
||||
onSignatureDeleted(deletedIds);
|
||||
}
|
||||
}
|
||||
|
||||
if (updated.length !== 0 || added.length !== 0) {
|
||||
@@ -78,17 +97,16 @@ export const useSystemSignaturesData = ({
|
||||
onLazyDeleteChange?.(false);
|
||||
}
|
||||
},
|
||||
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange],
|
||||
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange, onSignatureDeleted],
|
||||
);
|
||||
|
||||
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));
|
||||
|
||||
await handleUpdateSignatures(finalList, false, true);
|
||||
setSelectedSignatures([]);
|
||||
}, [selectedSignatures, signatures]);
|
||||
}, [handleUpdateSignatures, selectedSignatures, signatures]);
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
setSelectedSignatures(signatures);
|
||||
@@ -119,11 +137,12 @@ export const useSystemSignaturesData = ({
|
||||
}, [signatures]);
|
||||
|
||||
return {
|
||||
signatures,
|
||||
signatures: signatures.filter(sig => !sig.deleted),
|
||||
selectedSignatures,
|
||||
setSelectedSignatures,
|
||||
handleDeleteSelected,
|
||||
handleSelectAll,
|
||||
handlePaste,
|
||||
hasUnsupportedLanguage,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import {
|
||||
MAPPING_GROUP_TO_ENG,
|
||||
MAPPING_TYPE_TO_ENG,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { MAPPING_TYPE_TO_ENG } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
|
||||
export const UNKNOWN_SIGNATURE_NAME = 'Unknown';
|
||||
|
||||
export const parseSignatures = (value: string, availableKeys: string[]): SystemSignature[] => {
|
||||
const outArr: SystemSignature[] = [];
|
||||
@@ -14,13 +19,39 @@ export const parseSignatures = (value: string, availableKeys: string[]): SystemS
|
||||
continue;
|
||||
}
|
||||
|
||||
const kind = MAPPING_TYPE_TO_ENG[sigArrInfo[1] as SignatureKind];
|
||||
// Extract the signature ID and check if it's valid (XXX-XXX format)
|
||||
const sigId = sigArrInfo[0];
|
||||
|
||||
if (!sigId || !sigId.match(/^[A-Z]{3}-\d{3}$/)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to map the kind, or fall back to CosmicSignature if unknown
|
||||
const typeString = sigArrInfo[1];
|
||||
let kind = SignatureKind.CosmicSignature;
|
||||
|
||||
// Try to map the kind using the flattened mapping
|
||||
const mappedKind = MAPPING_TYPE_TO_ENG[typeString];
|
||||
|
||||
if (mappedKind && availableKeys.includes(mappedKind)) {
|
||||
kind = mappedKind;
|
||||
}
|
||||
|
||||
// Try to map the group, or fall back to CosmicSignature if unknown
|
||||
const rawGroup = sigArrInfo[2];
|
||||
let group = SignatureGroup.CosmicSignature;
|
||||
|
||||
// Try to map the group using the flattened mapping
|
||||
const mappedGroup = MAPPING_GROUP_TO_ENG[rawGroup];
|
||||
if (mappedGroup) {
|
||||
group = mappedGroup;
|
||||
}
|
||||
|
||||
const signature: SystemSignature = {
|
||||
eve_id: sigArrInfo[0],
|
||||
kind: availableKeys.includes(kind) ? kind : SignatureKind.CosmicSignature,
|
||||
group: sigArrInfo[2] as SignatureGroup,
|
||||
name: sigArrInfo[3],
|
||||
eve_id: sigId,
|
||||
kind,
|
||||
group,
|
||||
name: sigArrInfo[3] || UNKNOWN_SIGNATURE_NAME,
|
||||
type: '',
|
||||
};
|
||||
|
||||
|
||||
@@ -242,15 +242,12 @@ export enum OutCommand {
|
||||
addSystemComment = 'addSystemComment',
|
||||
deleteSystemComment = 'deleteSystemComment',
|
||||
getSystemComments = 'getSystemComments',
|
||||
// toggleTrack = 'toggle_track',
|
||||
toggleFollow = 'toggle_follow',
|
||||
getCharacterInfo = 'getCharacterInfo',
|
||||
getCharactersTrackingInfo = 'getCharactersTrackingInfo',
|
||||
updateCharacterTracking = 'updateCharacterTracking',
|
||||
updateFollowingCharacter = 'updateFollowingCharacter',
|
||||
updateMainCharacter = 'updateMainCharacter',
|
||||
|
||||
// Only UI commands
|
||||
openSettings = 'open_settings',
|
||||
showActivity = 'show_activity',
|
||||
showTracking = 'show_tracking',
|
||||
@@ -258,7 +255,7 @@ export enum OutCommand {
|
||||
updateUserSettings = 'update_user_settings',
|
||||
unlinkSignature = 'unlink_signature',
|
||||
searchSystems = 'search_systems',
|
||||
undoDeleteSignatures = 'undo_delete_signatures',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type OutCommandHandler = <T = any>(event: { type: OutCommand; data: any }) => Promise<T>;
|
||||
export type OutCommandHandler = <T = unknown>(event: { type: OutCommand; data: unknown }) => Promise<T>;
|
||||
|
||||
@@ -30,6 +30,7 @@ export type GroupType = {
|
||||
export type SignatureCustomInfo = {
|
||||
k162Type?: string;
|
||||
isEOL?: boolean;
|
||||
isCrit?: boolean;
|
||||
};
|
||||
|
||||
export type SystemSignature = {
|
||||
@@ -46,6 +47,7 @@ export type SystemSignature = {
|
||||
linked_system?: SolarSystemStaticInfoRaw;
|
||||
inserted_at?: string;
|
||||
updated_at?: string;
|
||||
deleted?: boolean;
|
||||
};
|
||||
|
||||
export interface ExtendedSystemSignature extends SystemSignature {
|
||||
@@ -53,6 +55,7 @@ export interface ExtendedSystemSignature extends SystemSignature {
|
||||
pendingAddition?: boolean;
|
||||
pendingUntil?: number;
|
||||
finalTimeoutId?: number;
|
||||
deleted?: boolean;
|
||||
}
|
||||
|
||||
export enum SignatureKindENG {
|
||||
@@ -75,6 +78,26 @@ export enum SignatureKindRU {
|
||||
Starbase = 'Starbase',
|
||||
}
|
||||
|
||||
export enum SignatureKindFR {
|
||||
CosmicSignature = 'Signature cosmique (type)',
|
||||
CosmicAnomaly = 'Anomalie cosmique',
|
||||
Structure = 'Structure',
|
||||
Ship = 'Vaisseau',
|
||||
Deployable = 'Déployable',
|
||||
Drone = 'Drone',
|
||||
Starbase = 'Base stellaire',
|
||||
}
|
||||
|
||||
export enum SignatureKindDE {
|
||||
CosmicSignature = 'Kosmische Signatur (typ)',
|
||||
CosmicAnomaly = 'Kosmische Anomalie',
|
||||
Structure = 'Struktur',
|
||||
Ship = 'Schiff',
|
||||
Deployable = 'Mobile Struktur',
|
||||
Drone = 'Drohne',
|
||||
Starbase = 'Sternenbasis',
|
||||
}
|
||||
|
||||
export enum SignatureGroupENG {
|
||||
CosmicSignature = 'Cosmic Signature',
|
||||
Wormhole = 'Wormhole',
|
||||
@@ -94,3 +117,23 @@ export enum SignatureGroupRU {
|
||||
OreSite = 'Астероидный район',
|
||||
CombatSite = 'Боевой район',
|
||||
}
|
||||
|
||||
export enum SignatureGroupFR {
|
||||
CosmicSignature = 'Signature cosmique (groupe)',
|
||||
Wormhole = 'Trou de ver',
|
||||
GasSite = 'Site de gaz',
|
||||
RelicSite = 'Site de reliques',
|
||||
DataSite = 'Site de données',
|
||||
OreSite = 'Site de minerai',
|
||||
CombatSite = 'Site de combat',
|
||||
}
|
||||
|
||||
export enum SignatureGroupDE {
|
||||
CosmicSignature = 'Kosmische Signatur (gruppe)',
|
||||
Wormhole = 'Wurmloch',
|
||||
GasSite = 'Gasgebiet',
|
||||
RelicSite = 'Reliktgebiet',
|
||||
DataSite = 'Datengebiet',
|
||||
OreSite = 'Mineraliengebiet',
|
||||
CombatSite = 'Kampfgebiet',
|
||||
}
|
||||
|
||||
BIN
assets/static/images/news/05-08-undo/undo.png
Executable file
BIN
assets/static/images/news/05-08-undo/undo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@@ -85,7 +85,7 @@ defmodule WandererApp.Api.Character do
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
accept([:access_token, :refresh_token, :expires_at, :scopes])
|
||||
accept([:name, :access_token, :refresh_token, :expires_at, :scopes])
|
||||
|
||||
change(set_attribute(:deleted, false))
|
||||
end
|
||||
|
||||
@@ -5,6 +5,16 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
@derive {Jason.Encoder, only: [
|
||||
:id,
|
||||
:map_id,
|
||||
:character_id,
|
||||
:tracked,
|
||||
:followed,
|
||||
:inserted_at,
|
||||
:updated_at
|
||||
]}
|
||||
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("map_character_settings_v1")
|
||||
|
||||
@@ -38,7 +38,8 @@ defmodule WandererApp.Api.MapConnection do
|
||||
:map_id,
|
||||
:solar_system_source,
|
||||
:solar_system_target,
|
||||
:type
|
||||
:type,
|
||||
:ship_size_type
|
||||
]
|
||||
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
@@ -24,7 +24,10 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
)
|
||||
|
||||
define(:by_system_id, action: :by_system_id, args: [:system_id])
|
||||
define(:by_system_id_all, action: :by_system_id_all, args: [:system_id])
|
||||
define(:by_system_id_and_eve_ids, action: :by_system_id_and_eve_ids, args: [:system_id, :eve_ids])
|
||||
define(:by_linked_system_id, action: :by_linked_system_id, args: [:linked_system_id])
|
||||
define(:by_deleted_and_updated_before!, action: :by_deleted_and_updated_before, args: [:deleted, :updated_before])
|
||||
end
|
||||
|
||||
actions do
|
||||
@@ -36,7 +39,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:description,
|
||||
:kind,
|
||||
:group,
|
||||
:type
|
||||
:type,
|
||||
:deleted
|
||||
]
|
||||
|
||||
defaults [:read, :destroy]
|
||||
@@ -64,7 +68,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:kind,
|
||||
:group,
|
||||
:type,
|
||||
:custom_info
|
||||
:custom_info,
|
||||
:deleted
|
||||
]
|
||||
|
||||
argument :system_id, :uuid, allow_nil?: false
|
||||
@@ -83,7 +88,7 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:group,
|
||||
:type,
|
||||
:custom_info,
|
||||
:updated
|
||||
:deleted
|
||||
]
|
||||
|
||||
primary? true
|
||||
@@ -105,14 +110,32 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
read :by_system_id do
|
||||
argument(:system_id, :string, allow_nil?: false)
|
||||
|
||||
filter(expr(system_id == ^arg(:system_id) and deleted == false))
|
||||
end
|
||||
|
||||
read :by_system_id_all do
|
||||
argument(:system_id, :string, allow_nil?: false)
|
||||
filter(expr(system_id == ^arg(:system_id)))
|
||||
end
|
||||
|
||||
read :by_system_id_and_eve_ids do
|
||||
argument(:system_id, :string, allow_nil?: false)
|
||||
argument(:eve_ids, {:array, :string}, allow_nil?: false)
|
||||
filter(expr(system_id == ^arg(:system_id) and eve_id in ^arg(:eve_ids)))
|
||||
end
|
||||
|
||||
read :by_linked_system_id do
|
||||
argument(:linked_system_id, :integer, allow_nil?: false)
|
||||
|
||||
filter(expr(linked_system_id == ^arg(:linked_system_id)))
|
||||
end
|
||||
|
||||
read :by_deleted_and_updated_before do
|
||||
argument(:deleted, :boolean, allow_nil?: false)
|
||||
argument(:updated_before, :utc_datetime, allow_nil?: false)
|
||||
|
||||
filter(expr(deleted == ^arg(:deleted) and updated_at < ^arg(:updated_before)))
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
@@ -149,7 +172,10 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :updated, :integer
|
||||
attribute :deleted, :boolean do
|
||||
allow_nil? false
|
||||
default false
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
@@ -164,4 +190,23 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
identities do
|
||||
identity :uniq_system_eve_id, [:system_id, :eve_id]
|
||||
end
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
only: [
|
||||
:id,
|
||||
:system_id,
|
||||
:eve_id,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:description,
|
||||
:type,
|
||||
:linked_system_id,
|
||||
:kind,
|
||||
:group,
|
||||
:custom_info,
|
||||
:deleted,
|
||||
:inserted_at,
|
||||
:updated_at
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
@@ -4,6 +4,27 @@ defmodule WandererApp.Api.MapSystemStructure do
|
||||
|
||||
"""
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
only: [
|
||||
:id,
|
||||
:system_id,
|
||||
:solar_system_id,
|
||||
:solar_system_name,
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time,
|
||||
:inserted_at,
|
||||
:updated_at
|
||||
]
|
||||
}
|
||||
|
||||
use Ash.Resource,
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
@@ -13,35 +13,40 @@ defmodule WandererApp.Application do
|
||||
WandererAppWeb.Telemetry,
|
||||
WandererApp.Vault,
|
||||
WandererApp.Repo,
|
||||
|
||||
{Phoenix.PubSub, name: WandererApp.PubSub, adapter_name: Phoenix.PubSub.PG2},
|
||||
|
||||
{
|
||||
Finch,
|
||||
name: WandererApp.Finch,
|
||||
pools: %{
|
||||
default: [
|
||||
size: 25, # number of connections per pool
|
||||
count: 2, # number of pools (so total 50 connections)
|
||||
# number of connections per pool
|
||||
size: 25,
|
||||
# number of pools (so total 50 connections)
|
||||
count: 2
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
WandererApp.Cache,
|
||||
|
||||
Supervisor.child_spec({Cachex, name: :system_static_info_cache}, id: :system_static_info_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :ship_types_cache}, id: :ship_types_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_cache}, id: :character_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :map_cache}, id: :map_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_state_cache}, id: :character_state_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :api_cache}, id: :api_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :system_static_info_cache},
|
||||
id: :system_static_info_cache_worker
|
||||
),
|
||||
Supervisor.child_spec({Cachex, name: :ship_types_cache}, id: :ship_types_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_cache}, id: :character_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :map_cache}, id: :map_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_state_cache},
|
||||
id: :character_state_cache_worker
|
||||
),
|
||||
Supervisor.child_spec({Cachex, name: :tracked_characters},
|
||||
id: :tracked_characters_cache_worker
|
||||
),
|
||||
WandererApp.Scheduler,
|
||||
{Registry, keys: :unique, name: WandererApp.MapRegistry},
|
||||
{Registry, keys: :unique, name: WandererApp.Character.TrackerRegistry},
|
||||
{PartitionSupervisor, child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor, child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
WandererApp.Zkb.Supervisor,
|
||||
WandererApp.Server.ServerStatusTracker,
|
||||
WandererApp.Server.TheraDataFetcher,
|
||||
@@ -49,11 +54,10 @@ defmodule WandererApp.Application do
|
||||
WandererApp.Character.TrackerManager,
|
||||
WandererApp.Map.Manager,
|
||||
WandererApp.Map.ZkbDataFetcher,
|
||||
|
||||
WandererAppWeb.Presence,
|
||||
WandererAppWeb.Endpoint
|
||||
]
|
||||
++ maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?())
|
||||
] ++
|
||||
maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?())
|
||||
|
||||
opts = [strategy: :one_for_one, name: WandererApp.Supervisor]
|
||||
|
||||
|
||||
@@ -39,40 +39,49 @@ defmodule WandererApp.CachedInfo do
|
||||
def get_system_static_info(solar_system_id) do
|
||||
case Cachex.get(:system_static_info_cache, solar_system_id) do
|
||||
{:ok, nil} ->
|
||||
{:ok, systems} = WandererApp.Api.MapSolarSystem.read()
|
||||
case WandererApp.Api.MapSolarSystem.read() do
|
||||
{:ok, systems} ->
|
||||
systems
|
||||
|> Enum.each(fn system ->
|
||||
Cachex.put(
|
||||
:system_static_info_cache,
|
||||
system.solar_system_id,
|
||||
Map.take(system, [
|
||||
:solar_system_id,
|
||||
:region_id,
|
||||
:constellation_id,
|
||||
:solar_system_name,
|
||||
:solar_system_name_lc,
|
||||
:constellation_name,
|
||||
:region_name,
|
||||
:system_class,
|
||||
:security,
|
||||
:type_description,
|
||||
:class_title,
|
||||
:is_shattered,
|
||||
:effect_name,
|
||||
:effect_power,
|
||||
:statics,
|
||||
:wandering,
|
||||
:triglavian_invasion_status,
|
||||
:sun_type_id
|
||||
])
|
||||
)
|
||||
end)
|
||||
|
||||
systems
|
||||
|> Enum.each(fn system ->
|
||||
Cachex.put(
|
||||
:system_static_info_cache,
|
||||
system.solar_system_id,
|
||||
Map.take(system, [
|
||||
:solar_system_id,
|
||||
:region_id,
|
||||
:constellation_id,
|
||||
:solar_system_name,
|
||||
:solar_system_name_lc,
|
||||
:constellation_name,
|
||||
:region_name,
|
||||
:system_class,
|
||||
:security,
|
||||
:type_description,
|
||||
:class_title,
|
||||
:is_shattered,
|
||||
:effect_name,
|
||||
:effect_power,
|
||||
:statics,
|
||||
:wandering,
|
||||
:triglavian_invasion_status,
|
||||
:sun_type_id
|
||||
])
|
||||
)
|
||||
end)
|
||||
Cachex.get(:system_static_info_cache, solar_system_id)
|
||||
|
||||
Cachex.get(:system_static_info_cache, solar_system_id)
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to read solar systems from API: #{inspect(reason)}")
|
||||
{:error, :api_error}
|
||||
end
|
||||
|
||||
{:ok, system_static_info} ->
|
||||
{:ok, system_static_info}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to get system static info from cache: #{inspect(reason)}")
|
||||
{:error, :cache_error}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
status: binary()
|
||||
}
|
||||
|
||||
@online_error_timeout :timer.minutes(5)
|
||||
@online_error_timeout :timer.minutes(3)
|
||||
@forbidden_ttl :timer.minutes(1)
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
|
||||
@@ -103,7 +103,9 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> update_ship()
|
||||
end
|
||||
|
||||
def update_ship(%{character_id: character_id, track_ship: true} = character_state) do
|
||||
def update_ship(
|
||||
%{character_id: character_id, track_ship: true, is_online: true} = character_state
|
||||
) do
|
||||
character_id
|
||||
|> WandererApp.Character.get_character()
|
||||
|> case do
|
||||
@@ -137,6 +139,13 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("#{__MODULE__} failed to update_ship: #{inspect(error)}")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:ship_forbidden",
|
||||
true,
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
@@ -154,7 +163,9 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> update_location()
|
||||
end
|
||||
|
||||
def update_location(%{track_location: true, character_id: character_id} = character_state) do
|
||||
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")
|
||||
@@ -187,6 +198,13 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("#{__MODULE__} failed to update_location: #{inspect(error)}")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:location_forbidden",
|
||||
true,
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
|
||||
{:error, error}
|
||||
end
|
||||
|
||||
@@ -301,18 +319,12 @@ defmodule WandererApp.Character.Tracker do
|
||||
duration = DateTime.diff(DateTime.utc_now(), error_time, :second)
|
||||
|
||||
if duration >= @online_error_timeout do
|
||||
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
|
||||
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.Cache.delete("character:#{character_id}:location_started")
|
||||
WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
character_state
|
||||
| is_online: false,
|
||||
track_ship: false,
|
||||
track_location: false
|
||||
is_online: false
|
||||
})
|
||||
|
||||
:ok
|
||||
@@ -756,5 +768,5 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
defp get_online(%{"online" => online}), do: %{online: online}
|
||||
|
||||
defp get_online(_), do: %{online: true}
|
||||
defp get_online(_), do: %{online: false}
|
||||
end
|
||||
|
||||
@@ -31,9 +31,7 @@ defmodule WandererApp.Character.TrackerManager do
|
||||
def init(args) do
|
||||
Logger.info("#{__MODULE__} started")
|
||||
|
||||
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
|
||||
{:ok, Impl.init(args |> Keyword.merge(characters: tracked_characters)), {:continue, :start}}
|
||||
{:ok, Impl.init(args), {:continue, :start}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
@@ -32,71 +32,72 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|> new()
|
||||
end
|
||||
|
||||
def start(%{opts: opts} = state) do
|
||||
opts[:characters]
|
||||
|> Enum.reduce(state, fn character_id, acc ->
|
||||
start_tracking(acc, character_id, %{})
|
||||
def start(state) do
|
||||
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
WandererApp.Cache.insert("tracked_characters", [])
|
||||
|
||||
tracked_characters
|
||||
|> Enum.each(fn character_id ->
|
||||
start_tracking(state, character_id, %{})
|
||||
end)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def start_tracking(%__MODULE__{characters: characters} = state, character_id, opts) do
|
||||
case Enum.member?(characters, character_id) do
|
||||
true ->
|
||||
state
|
||||
def start_tracking(state, character_id, opts) do
|
||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||
false <- Enum.member?(characters, character_id) do
|
||||
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
|
||||
|
||||
false ->
|
||||
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
|
||||
tracked_characters = [character_id | characters] |> Enum.uniq()
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
|
||||
character_id,
|
||||
%{opts: opts}
|
||||
])
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
tracked_characters = [character_id | state.characters] |> Enum.uniq()
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
is_online: false
|
||||
})
|
||||
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
|
||||
|
||||
%{state | characters: tracked_characters}
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
|
||||
character_id,
|
||||
%{opts: opts}
|
||||
])
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def stop_tracking(%__MODULE__{characters: characters} = state, character_id) do
|
||||
case Enum.member?(characters, character_id) do
|
||||
true ->
|
||||
{:ok, character_state} = WandererApp.Character.get_character_state(character_id, false)
|
||||
def stop_tracking(state, character_id) do
|
||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||
true <- Enum.member?(characters, character_id),
|
||||
{:ok, %{start_time: start_time}} <-
|
||||
WandererApp.Character.get_character_state(character_id, false) do
|
||||
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
|
||||
|
||||
case character_state do
|
||||
nil ->
|
||||
state
|
||||
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)
|
||||
|
||||
%{start_time: start_time} ->
|
||||
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
|
||||
tracked_characters =
|
||||
characters |> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{
|
||||
duration: duration
|
||||
})
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
|
||||
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
|
||||
|
||||
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)
|
||||
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
|
||||
|
||||
tracked_characters =
|
||||
state.characters |> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{
|
||||
duration: duration
|
||||
})
|
||||
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
|
||||
|
||||
%{state | characters: tracked_characters}
|
||||
end
|
||||
|
||||
false ->
|
||||
state
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def update_track_settings(
|
||||
@@ -135,12 +136,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
end
|
||||
|
||||
def get_characters(
|
||||
%{
|
||||
characters: characters
|
||||
} = state,
|
||||
state,
|
||||
_opts \\ []
|
||||
),
|
||||
do: {characters, state}
|
||||
) do
|
||||
{:ok, characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
{characters, state}
|
||||
end
|
||||
|
||||
def handle_event({ref, result}, state) do
|
||||
Process.demonitor(ref, [:flush])
|
||||
@@ -163,13 +164,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|
||||
def handle_info(
|
||||
:garbage_collect,
|
||||
%{
|
||||
characters: characters
|
||||
} =
|
||||
state
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
|
||||
|
||||
{:ok, characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
@@ -214,8 +214,6 @@ 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}:tracked")
|
||||
|
||||
{:ok, character_state} =
|
||||
WandererApp.Character.Tracker.update_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
@@ -235,19 +233,14 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
end
|
||||
|
||||
def handle_info({:stop_track, character_id}, state) do
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:is_stop_tracking")
|
||||
|> case do
|
||||
false ->
|
||||
WandererApp.Cache.insert("character:#{character_id}:is_stop_tracking", true)
|
||||
Logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end)
|
||||
state = state |> stop_tracking(character_id)
|
||||
WandererApp.Cache.delete("character:#{character_id}:is_stop_tracking")
|
||||
|
||||
state
|
||||
|
||||
_ ->
|
||||
state
|
||||
if not WandererApp.Cache.has_key?("character:#{character_id}:is_stop_tracking") do
|
||||
WandererApp.Cache.insert("character:#{character_id}:is_stop_tracking", true)
|
||||
Logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end)
|
||||
stop_tracking(state, character_id)
|
||||
WandererApp.Cache.delete("character:#{character_id}:is_stop_tracking")
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_info(_event, state),
|
||||
|
||||
@@ -17,11 +17,11 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
@unique_registry :unique_tracker_pool_registry
|
||||
|
||||
@update_location_interval :timer.seconds(2)
|
||||
@update_online_interval :timer.seconds(10)
|
||||
@update_online_interval :timer.seconds(5)
|
||||
@check_online_errors_interval :timer.seconds(30)
|
||||
@update_ship_interval :timer.seconds(5)
|
||||
@update_ship_interval :timer.seconds(2)
|
||||
@update_info_interval :timer.minutes(1)
|
||||
@update_wallet_interval :timer.minutes(5)
|
||||
@update_wallet_interval :timer.minutes(1)
|
||||
@inactive_character_timeout :timer.minutes(5)
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
@@ -167,10 +167,22 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
|
||||
def handle_info(
|
||||
:update_online,
|
||||
state
|
||||
%{
|
||||
characters: characters
|
||||
} =
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :update_online, @update_online_interval)
|
||||
|
||||
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)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
|
||||
@@ -161,61 +161,86 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
end
|
||||
|
||||
# Helper functions for character tracking
|
||||
def track(_, _, false, _), do: :ok
|
||||
|
||||
def track([], _map_id, _is_track_character?, _), do: :ok
|
||||
|
||||
def track([character | characters], map_id, true, caller_pid) do
|
||||
with :ok <- track_character(character, map_id, caller_pid) do
|
||||
track(characters, map_id, true, caller_pid)
|
||||
def track([character | characters], map_id, is_track_allowed, caller_pid) do
|
||||
with :ok <- track_character(character, map_id, is_track_allowed, caller_pid) do
|
||||
track(characters, map_id, is_track_allowed, caller_pid)
|
||||
end
|
||||
end
|
||||
|
||||
defp track_character(
|
||||
%{
|
||||
id: character_id,
|
||||
eve_id: eve_id,
|
||||
corporation_id: corporation_id,
|
||||
alliance_id: alliance_id
|
||||
eve_id: eve_id
|
||||
},
|
||||
map_id,
|
||||
is_track_allowed,
|
||||
caller_pid
|
||||
) do
|
||||
with false <- is_nil(caller_pid) do
|
||||
WandererAppWeb.Presence.track(caller_pid, map_id, character_id, %{})
|
||||
)
|
||||
when not is_nil(caller_pid) do
|
||||
WandererAppWeb.Presence.update(caller_pid, map_id, character_id, %{
|
||||
tracked: is_track_allowed,
|
||||
from: DateTime.utc_now()
|
||||
})
|
||||
|> case do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
cache_key = "#{inspect(caller_pid)}_map_#{map_id}:character_#{character_id}:tracked"
|
||||
{:error, :nopresence} ->
|
||||
WandererAppWeb.Presence.track(caller_pid, map_id, character_id, %{
|
||||
tracked: is_track_allowed,
|
||||
from: DateTime.utc_now()
|
||||
})
|
||||
|
||||
case WandererApp.Cache.lookup!(cache_key, false) do
|
||||
true ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
:ok = Phoenix.PubSub.subscribe(WandererApp.PubSub, "character:#{eve_id}")
|
||||
:ok = WandererApp.Cache.put(cache_key, true)
|
||||
end
|
||||
|
||||
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
|
||||
else
|
||||
true ->
|
||||
Logger.error("caller_pid is required for tracking characters")
|
||||
{:error, "caller_pid is required"}
|
||||
error ->
|
||||
Logger.error("Failed to update presence: #{inspect(error)}")
|
||||
{:error, "Failed to update presence"}
|
||||
end
|
||||
|
||||
cache_key = "#{inspect(caller_pid)}_map_#{map_id}:character_#{character_id}:tracked"
|
||||
|
||||
case WandererApp.Cache.lookup!(cache_key, false) do
|
||||
true ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
:ok = Phoenix.PubSub.subscribe(WandererApp.PubSub, "character:#{eve_id}")
|
||||
:ok = WandererApp.Cache.put(cache_key, true)
|
||||
end
|
||||
|
||||
if is_track_allowed do
|
||||
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp track_character(
|
||||
_character,
|
||||
_map_id,
|
||||
_is_track_allowed,
|
||||
_caller_pid
|
||||
) do
|
||||
Logger.error("caller_pid is required for tracking characters")
|
||||
{:error, "caller_pid is required"}
|
||||
end
|
||||
|
||||
def untrack(characters, map_id, caller_pid) do
|
||||
with false <- is_nil(caller_pid) do
|
||||
character_ids = characters |> Enum.map(& &1.id)
|
||||
|
||||
characters
|
||||
|> Enum.each(fn character ->
|
||||
WandererAppWeb.Presence.untrack(caller_pid, map_id, character.id)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"#{inspect(caller_pid)}_map_#{map_id}:character_#{character.id}:tracked",
|
||||
false
|
||||
)
|
||||
|
||||
:ok = Phoenix.PubSub.unsubscribe(WandererApp.PubSub, "character:#{character.eve_id}")
|
||||
WandererAppWeb.Presence.update(caller_pid, map_id, character.id, %{
|
||||
tracked: false,
|
||||
from: DateTime.utc_now()
|
||||
})
|
||||
end)
|
||||
|
||||
WandererApp.Map.Server.untrack_characters(map_id, character_ids)
|
||||
|
||||
:ok
|
||||
else
|
||||
true ->
|
||||
|
||||
@@ -31,6 +31,9 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
avoid: []
|
||||
}
|
||||
|
||||
@zarzakh_system 30_100_000
|
||||
@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)]
|
||||
@@ -99,7 +102,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
chains = _remove_intersection([map_chains | thera_chains] |> List.flatten())
|
||||
chains = remove_intersection([map_chains | thera_chains] |> List.flatten())
|
||||
|
||||
chains =
|
||||
case routes_settings.include_cruise do
|
||||
@@ -170,7 +173,10 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
avoidance_list
|
||||
end
|
||||
|
||||
avoidance_list = [routes_settings.avoid | avoidance_list] |> List.flatten() |> Enum.uniq()
|
||||
avoidance_list =
|
||||
(@default_avoid_systems ++ [routes_settings.avoid | avoidance_list])
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|
||||
params =
|
||||
%{
|
||||
@@ -296,7 +302,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_killmail(killmail_id, killmail_hash, opts \\ []) do
|
||||
get("/killmails/#{killmail_id}/#{killmail_hash}/", opts)
|
||||
get("/killmails/#{killmail_id}/#{killmail_hash}/", opts, @cache_opts)
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
@@ -319,7 +325,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
def get_character_info(eve_id, opts \\ []) do
|
||||
case get(
|
||||
"/characters/#{eve_id}/",
|
||||
opts
|
||||
opts,
|
||||
@cache_opts
|
||||
) do
|
||||
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:error, error} -> {:error, error}
|
||||
@@ -333,25 +340,35 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
def get_custom_route_base_url, do: WandererApp.Env.custom_route_base_url()
|
||||
|
||||
def get_character_wallet(character_eve_id, opts \\ []),
|
||||
do: _get_character_auth_data(character_eve_id, "wallet", opts)
|
||||
do: get_character_auth_data(character_eve_id, "wallet", opts ++ @cache_opts)
|
||||
|
||||
def get_corporation_wallets(corporation_id, opts \\ []),
|
||||
do: _get_corporation_auth_data(corporation_id, "wallets", opts)
|
||||
do: get_corporation_auth_data(corporation_id, "wallets", opts)
|
||||
|
||||
def get_corporation_wallet_journal(corporation_id, division, opts \\ []),
|
||||
do: _get_corporation_auth_data(corporation_id, "wallets/#{division}/journal", opts)
|
||||
do:
|
||||
get_corporation_auth_data(
|
||||
corporation_id,
|
||||
"wallets/#{division}/journal",
|
||||
opts
|
||||
)
|
||||
|
||||
def get_corporation_wallet_transactions(corporation_id, division, opts \\ []),
|
||||
do: _get_corporation_auth_data(corporation_id, "wallets/#{division}/transactions", opts)
|
||||
do:
|
||||
get_corporation_auth_data(
|
||||
corporation_id,
|
||||
"wallets/#{division}/transactions",
|
||||
opts
|
||||
)
|
||||
|
||||
def get_character_location(character_eve_id, opts \\ []),
|
||||
do: _get_character_auth_data(character_eve_id, "location", opts)
|
||||
do: get_character_auth_data(character_eve_id, "location", opts ++ @cache_opts)
|
||||
|
||||
def get_character_online(character_eve_id, opts \\ []),
|
||||
do: _get_character_auth_data(character_eve_id, "online", opts)
|
||||
do: get_character_auth_data(character_eve_id, "online", opts ++ @cache_opts)
|
||||
|
||||
def get_character_ship(character_eve_id, opts \\ []),
|
||||
do: _get_character_auth_data(character_eve_id, "ship", opts)
|
||||
do: get_character_auth_data(character_eve_id, "ship", opts ++ @cache_opts)
|
||||
|
||||
def search(character_eve_id, opts \\ []) do
|
||||
search_val = to_string(opts[:params][:search] || "")
|
||||
@@ -366,7 +383,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
]
|
||||
|
||||
merged_opts = Keyword.put(opts, :params, query_params)
|
||||
_search(character_eve_id, search_val, categories_val, merged_opts)
|
||||
get_search(character_eve_id, search_val, categories_val, merged_opts)
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
@@ -374,11 +391,11 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
key: "search-#{character_eve_id}-#{categories_val}-#{search_val |> Slug.slugify()}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
defp _search(character_eve_id, search_val, categories_val, merged_opts) do
|
||||
_get_character_auth_data(character_eve_id, "search", merged_opts)
|
||||
defp get_search(character_eve_id, search_val, categories_val, merged_opts) do
|
||||
get_character_auth_data(character_eve_id, "search", merged_opts)
|
||||
end
|
||||
|
||||
defp _remove_intersection(pairs_arr) do
|
||||
defp remove_intersection(pairs_arr) do
|
||||
tuples = pairs_arr |> Enum.map(fn x -> {x.first, x.second} end)
|
||||
|
||||
tuples
|
||||
@@ -399,9 +416,9 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
|
||||
defp _get_routes(origin, destination, params, opts),
|
||||
do: _get_routes_eve(origin, destination, params, opts)
|
||||
do: get_routes_eve(origin, destination, params, opts)
|
||||
|
||||
defp _get_routes_eve(origin, destination, params, opts) do
|
||||
defp get_routes_eve(origin, destination, params, opts) do
|
||||
esi_params =
|
||||
Map.merge(params, %{
|
||||
connections: params.connections |> Enum.join(","),
|
||||
@@ -410,7 +427,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
get(
|
||||
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
|
||||
opts
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
end
|
||||
|
||||
@@ -420,17 +438,19 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
do:
|
||||
get(
|
||||
"/alliances/#{alliance_eve_id}/#{info_path}",
|
||||
opts
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
|
||||
defp _get_corporation_info(corporation_eve_id, info_path, opts),
|
||||
do:
|
||||
get(
|
||||
"/corporations/#{corporation_eve_id}/#{info_path}",
|
||||
opts
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
|
||||
defp _get_character_auth_data(character_eve_id, info_path, opts) do
|
||||
defp get_character_auth_data(character_eve_id, info_path, opts) do
|
||||
path = "/characters/#{character_eve_id}/#{info_path}"
|
||||
|
||||
auth_opts =
|
||||
@@ -439,7 +459,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
character_id = opts |> Keyword.get(:character_id, nil)
|
||||
|
||||
if not _is_access_token_expired?(character_id) do
|
||||
if not is_access_token_expired?(character_id) do
|
||||
get(
|
||||
path,
|
||||
auth_opts,
|
||||
@@ -450,7 +470,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
defp _is_access_token_expired?(character_id) do
|
||||
defp is_access_token_expired?(character_id) do
|
||||
{:ok, %{expires_at: expires_at} = _character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
@@ -459,13 +479,13 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
expires_at - now <= 0
|
||||
end
|
||||
|
||||
defp _get_corporation_auth_data(corporation_eve_id, info_path, opts),
|
||||
defp get_corporation_auth_data(corporation_eve_id, info_path, opts),
|
||||
do:
|
||||
get(
|
||||
"/corporations/#{corporation_eve_id}/#{info_path}",
|
||||
[params: opts[:params] || []] ++
|
||||
(opts |> get_auth_opts()),
|
||||
opts
|
||||
opts ++ @cache_opts
|
||||
)
|
||||
|
||||
defp with_user_agent_opts(opts) do
|
||||
@@ -487,6 +507,16 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
)
|
||||
|
||||
defp get(path, api_opts \\ [], opts \\ []) do
|
||||
case Cachex.get(:api_cache, path) do
|
||||
{:ok, cached_data} when not is_nil(cached_data) ->
|
||||
{:ok, cached_data}
|
||||
|
||||
_ ->
|
||||
do_get_request(path, api_opts, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_request(path, api_opts \\ [], opts \\ []) do
|
||||
try do
|
||||
case Req.get(
|
||||
"#{@base_url}#{path}",
|
||||
@@ -496,7 +526,9 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|> Keyword.merge(@retry_opts)
|
||||
|> Keyword.merge(@timeout_opts)
|
||||
) do
|
||||
{:ok, %{status: 200, body: body}} ->
|
||||
{:ok, %{status: 200, body: body, headers: headers}} ->
|
||||
maybe_cache_response(path, body, headers, opts)
|
||||
|
||||
{:ok, body}
|
||||
|
||||
{:ok, %{status: 504}} ->
|
||||
@@ -525,6 +557,30 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_cache_response(path, body, %{"expires" => [expires]}, opts)
|
||||
when is_binary(path) and not is_nil(expires) do
|
||||
try do
|
||||
if opts |> Keyword.get(:cache, false) do
|
||||
cached_ttl =
|
||||
DateTime.diff(Timex.parse!(expires, "{RFC1123}"), DateTime.utc_now(), :millisecond)
|
||||
|
||||
Cachex.put(
|
||||
:api_cache,
|
||||
path,
|
||||
body,
|
||||
ttl: cached_ttl
|
||||
)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
@logger.error(Exception.message(e))
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_cache_response(_path, _body, _headers, _opts), do: :ok
|
||||
|
||||
defp post(url, opts) do
|
||||
try do
|
||||
case Req.post("#{url}", opts |> with_user_agent_opts()) do
|
||||
@@ -582,63 +638,91 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
{:ok, %{expires_at: expires_at, refresh_token: refresh_token, scopes: scopes} = character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
case WandererApp.Ueberauth.Strategy.Eve.OAuth.get_refresh_token([],
|
||||
with_wallet: WandererApp.Character.can_track_wallet?(character),
|
||||
is_admin?: WandererApp.Character.can_track_corp_wallet?(character),
|
||||
token: %OAuth2.AccessToken{refresh_token: refresh_token}
|
||||
) do
|
||||
{:ok, %OAuth2.AccessToken{} = token} ->
|
||||
{:ok, _character} =
|
||||
character
|
||||
|> WandererApp.Api.Character.update(%{
|
||||
access_token: token.access_token,
|
||||
expires_at: token.expires_at,
|
||||
scopes: scopes
|
||||
})
|
||||
refresh_token_result =
|
||||
WandererApp.Ueberauth.Strategy.Eve.OAuth.get_refresh_token([],
|
||||
with_wallet: WandererApp.Character.can_track_wallet?(character),
|
||||
is_admin?: WandererApp.Character.can_track_corp_wallet?(character),
|
||||
token: %OAuth2.AccessToken{refresh_token: refresh_token}
|
||||
)
|
||||
|
||||
WandererApp.Character.update_character(character_id, %{
|
||||
access_token: token.access_token,
|
||||
expires_at: token.expires_at
|
||||
})
|
||||
handle_refresh_token_result(refresh_token_result, character, character_id, expires_at, scopes)
|
||||
end
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}",
|
||||
:token_updated
|
||||
)
|
||||
defp handle_refresh_token_result(
|
||||
{:ok, %OAuth2.AccessToken{} = token},
|
||||
character,
|
||||
character_id,
|
||||
_expires_at,
|
||||
scopes
|
||||
) do
|
||||
{:ok, _character} =
|
||||
character
|
||||
|> WandererApp.Api.Character.update(%{
|
||||
access_token: token.access_token,
|
||||
expires_at: token.expires_at,
|
||||
scopes: scopes
|
||||
})
|
||||
|
||||
{:ok, token}
|
||||
WandererApp.Character.update_character(character_id, %{
|
||||
access_token: token.access_token,
|
||||
expires_at: token.expires_at
|
||||
})
|
||||
|
||||
{:error, {"invalid_grant", error_message}} ->
|
||||
{:ok, _character} =
|
||||
character
|
||||
|> WandererApp.Api.Character.update(%{
|
||||
access_token: nil,
|
||||
refresh_token: nil,
|
||||
expires_at: expires_at,
|
||||
scopes: scopes
|
||||
})
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}",
|
||||
:token_updated
|
||||
)
|
||||
|
||||
WandererApp.Character.update_character(character_id, %{
|
||||
access_token: nil,
|
||||
refresh_token: nil,
|
||||
expires_at: expires_at,
|
||||
scopes: scopes
|
||||
})
|
||||
{:ok, token}
|
||||
end
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}",
|
||||
:character_token_invalid
|
||||
)
|
||||
defp handle_refresh_token_result(
|
||||
{:error, {"invalid_grant", error_message}},
|
||||
character,
|
||||
character_id,
|
||||
expires_at,
|
||||
scopes
|
||||
) do
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{error_message}")
|
||||
{:error, :invalid_grant}
|
||||
end
|
||||
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{error_message}")
|
||||
{:error, :invalid_grant}
|
||||
defp handle_refresh_token_result(
|
||||
{:error, %OAuth2.Error{} = error},
|
||||
character,
|
||||
character_id,
|
||||
expires_at,
|
||||
scopes
|
||||
) do
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{inspect(error)}")
|
||||
{:error, :invalid_grant}
|
||||
end
|
||||
|
||||
defp handle_refresh_token_result(error, character, character_id, expires_at, scopes) do
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{inspect(error)}")
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
{:error, :failed}
|
||||
end
|
||||
|
||||
defp invalidate_character_tokens(character, character_id, expires_at, scopes) do
|
||||
attrs = %{access_token: nil, refresh_token: nil, expires_at: expires_at, scopes: scopes}
|
||||
|
||||
with {:ok, _} <- WandererApp.Api.Character.update(character, attrs),
|
||||
{:ok, _} <- WandererApp.Character.update_character(character_id, attrs) do
|
||||
:ok
|
||||
else
|
||||
error ->
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{inspect(error)}")
|
||||
{:error, :failed}
|
||||
Logger.error("Failed to clear tokens for #{character_id}: #{inspect(error)}")
|
||||
end
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}",
|
||||
:character_token_invalid
|
||||
)
|
||||
end
|
||||
|
||||
defp map_route_info(
|
||||
|
||||
@@ -9,12 +9,15 @@ defmodule WandererApp.Map.Manager do
|
||||
|
||||
alias WandererApp.Map.Server
|
||||
alias WandererApp.Map.ServerSupervisor
|
||||
alias WandererApp.Api.MapSystemSignature
|
||||
|
||||
@maps_start_per_second 5
|
||||
@maps_start_interval 1000
|
||||
@maps_queue :maps_queue
|
||||
@garbage_collection_interval :timer.hours(1)
|
||||
@check_maps_queue_interval :timer.seconds(1)
|
||||
@signatures_cleanup_interval :timer.minutes(30)
|
||||
@delete_after_minutes 30
|
||||
|
||||
def start_map(map_id) when is_binary(map_id),
|
||||
do: WandererApp.Queue.push_uniq(@maps_queue, map_id)
|
||||
@@ -44,6 +47,9 @@ defmodule WandererApp.Map.Manager do
|
||||
{:ok, garbage_collector_timer} =
|
||||
:timer.send_interval(@garbage_collection_interval, :garbage_collect)
|
||||
|
||||
{:ok, signatures_cleanup_timer} =
|
||||
:timer.send_interval(@signatures_cleanup_interval, :cleanup_signatures)
|
||||
|
||||
try do
|
||||
Task.async(fn ->
|
||||
start_last_active_maps()
|
||||
@@ -56,7 +62,8 @@ defmodule WandererApp.Map.Manager do
|
||||
{:ok,
|
||||
%{
|
||||
garbage_collector_timer: garbage_collector_timer,
|
||||
check_maps_queue_timer: check_maps_queue_timer
|
||||
check_maps_queue_timer: check_maps_queue_timer,
|
||||
signatures_cleanup_timer: signatures_cleanup_timer
|
||||
}}
|
||||
end
|
||||
|
||||
@@ -118,6 +125,36 @@ defmodule WandererApp.Map.Manager do
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:cleanup_signatures, state) do
|
||||
try do
|
||||
cleanup_deleted_signatures()
|
||||
{:noreply, state}
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Failed to cleanup signatures: #{inspect(e)}")
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def cleanup_deleted_signatures() do
|
||||
delete_after_date = DateTime.utc_now() |> DateTime.add(-1 * @delete_after_minutes, :minute)
|
||||
|
||||
case MapSystemSignature.by_deleted_and_updated_before!(true, delete_after_date) do
|
||||
{:ok, deleted_signatures} ->
|
||||
|
||||
Enum.each(deleted_signatures, fn sig ->
|
||||
Ash.destroy!(sig)
|
||||
end)
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("Failed to fetch deleted signatures: #{inspect(error)}")
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_last_active_maps() do
|
||||
{:ok, last_map_states} =
|
||||
WandererApp.Api.MapState.get_last_active(
|
||||
|
||||
128
lib/wanderer_app/map/map_operations.ex
Normal file
128
lib/wanderer_app/map/map_operations.ex
Normal file
@@ -0,0 +1,128 @@
|
||||
# File: lib/wanderer_app/map/operations.ex
|
||||
defmodule WandererApp.Map.Operations do
|
||||
@moduledoc """
|
||||
Central entrypoint for map operations. Delegates responsibilities to specialized submodules:
|
||||
- Owner: Fetching and caching owner character info
|
||||
- Systems: CRUD and batch upsert for systems
|
||||
- Connections: CRUD and batch upsert for connections
|
||||
- Structures: CRUD for structures
|
||||
- Signatures: CRUD for signatures
|
||||
"""
|
||||
|
||||
alias WandererApp.Map.Operations.{
|
||||
Owner,
|
||||
Systems,
|
||||
Connections,
|
||||
Structures,
|
||||
Signatures
|
||||
}
|
||||
|
||||
# -- Owner Info -------------------------------------------------------------
|
||||
|
||||
@doc "Fetch cached main character info for a map owner"
|
||||
@spec get_owner_character_id(String.t()) ::
|
||||
{:ok, %{id: term(), user_id: term()}} | {:error, String.t()}
|
||||
defdelegate get_owner_character_id(map_id), to: Owner
|
||||
|
||||
# -- Systems ----------------------------------------------------------------
|
||||
|
||||
@doc "List visible systems"
|
||||
@spec list_systems(String.t()) :: [map()]
|
||||
defdelegate list_systems(map_id), to: Systems
|
||||
|
||||
@doc "Get a specific system"
|
||||
@spec get_system(String.t(), integer()) :: {:ok, map()} | {:error, :not_found}
|
||||
defdelegate get_system(map_id, system_id), to: Systems
|
||||
|
||||
@doc "Create a system"
|
||||
@spec create_system(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
|
||||
defdelegate create_system(map_id, params), to: Systems
|
||||
|
||||
@doc "Update a system"
|
||||
@spec update_system(String.t(), integer(), map()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
defdelegate update_system(map_id, system_id, attrs), to: Systems
|
||||
|
||||
@doc "Delete a system"
|
||||
@spec delete_system(String.t(), integer()) :: {:ok, integer()} | {:error, term()}
|
||||
defdelegate delete_system(map_id, system_id), to: Systems
|
||||
|
||||
@doc "Upsert systems and connections in batch"
|
||||
@spec upsert_systems_and_connections(String.t(), [map()], [map()]) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
defdelegate upsert_systems_and_connections(map_id, systems, connections), to: Systems
|
||||
|
||||
# -- Connections -----------------------------------------------------------
|
||||
|
||||
@doc "List all connections"
|
||||
@spec list_connections(String.t()) :: [map()]
|
||||
defdelegate list_connections(map_id), to: Connections
|
||||
|
||||
@doc "List connections for a specific system"
|
||||
@spec list_connections(String.t(), integer()) :: [map()]
|
||||
defdelegate list_connections(map_id, system_id), to: Connections
|
||||
|
||||
@doc "Get a connection"
|
||||
@spec get_connection(String.t(), String.t()) :: {:ok, map()} | {:error, String.t()}
|
||||
defdelegate get_connection(map_id, connection_id), to: Connections
|
||||
|
||||
@doc "Create a connection"
|
||||
@spec create_connection(String.t(), map()) ::
|
||||
{:ok, map()} | {:skip, :exists} | {:error, String.t()}
|
||||
defdelegate create_connection(map_id, attrs), to: Connections
|
||||
|
||||
@doc "Force-create a connection with explicit character ID"
|
||||
@spec create_connection(String.t(), map(), integer()) ::
|
||||
{:ok, map()} | {:skip, :exists} | {:error, String.t()}
|
||||
defdelegate create_connection(map_id, attrs, char_id), to: Connections
|
||||
|
||||
@doc "Update a connection"
|
||||
@spec update_connection(String.t(), String.t(), map()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
defdelegate update_connection(map_id, connection_id, attrs), to: Connections
|
||||
|
||||
@doc "Delete a connection"
|
||||
@spec delete_connection(String.t(), integer(), integer()) :: :ok | {:error, term()}
|
||||
defdelegate delete_connection(map_id, src_id, tgt_id), to: Connections
|
||||
|
||||
@doc "Get a connection by source and target system IDs"
|
||||
@spec get_connection_by_systems(String.t(), integer(), integer()) :: {:ok, map()} | {:error, String.t()}
|
||||
defdelegate get_connection_by_systems(map_id, source, target), to: Connections
|
||||
|
||||
# -- Structures ------------------------------------------------------------
|
||||
|
||||
@doc "List all structures"
|
||||
@spec list_structures(String.t()) :: [map()]
|
||||
defdelegate list_structures(map_id), to: Structures
|
||||
|
||||
@doc "Create a structure"
|
||||
@spec create_structure(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
|
||||
defdelegate create_structure(map_id, params), to: Structures
|
||||
|
||||
@doc "Update a structure"
|
||||
@spec update_structure(String.t(), String.t(), map()) :: {:ok, map()} | {:error, String.t()}
|
||||
defdelegate update_structure(map_id, struct_id, params), to: Structures
|
||||
|
||||
@doc "Delete a structure"
|
||||
@spec delete_structure(String.t(), String.t()) :: :ok | {:error, String.t()}
|
||||
defdelegate delete_structure(map_id, struct_id), to: Structures
|
||||
|
||||
# -- Signatures ------------------------------------------------------------
|
||||
|
||||
@doc "List all signatures"
|
||||
@spec list_signatures(String.t()) :: [map()]
|
||||
defdelegate list_signatures(map_id), to: Signatures
|
||||
|
||||
@doc "Create a signature"
|
||||
@spec create_signature(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
|
||||
defdelegate create_signature(map_id, params), to: Signatures
|
||||
|
||||
@doc "Update a signature"
|
||||
@spec update_signature(String.t(), String.t(), map()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
defdelegate update_signature(map_id, sig_id, params), to: Signatures
|
||||
|
||||
@doc "Delete a signature in a map"
|
||||
@spec delete_signature(String.t(), String.t()) :: :ok | {:error, String.t()}
|
||||
defdelegate delete_signature(map_id, sig_id), to: Signatures
|
||||
end
|
||||
@@ -340,7 +340,7 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
end)
|
||||
|
||||
{:error, :no_active_subscription} ->
|
||||
Logger.warn(
|
||||
Logger.warning(
|
||||
"Cannot create license for map #{map.id}: No active subscription found"
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ defmodule WandererApp.Map.ZkbDataFetcher do
|
||||
@interval :timer.seconds(15)
|
||||
@store_map_kills_timeout :timer.hours(1)
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
|
||||
# This means 120 “ticks” of 15s each → ~30 minutes
|
||||
@preload_cycle_ticks 120
|
||||
@@ -118,7 +117,10 @@ defmodule WandererApp.Map.ZkbDataFetcher do
|
||||
|> Enum.map(&elem(&1, 0))
|
||||
|
||||
if changed_systems == [] do
|
||||
Logger.debug(fn -> "[ZkbDataFetcher] No changes in detailed kills for map_id=#{map_id}" end)
|
||||
Logger.debug(fn ->
|
||||
"[ZkbDataFetcher] No changes in detailed kills for map_id=#{map_id}"
|
||||
end)
|
||||
|
||||
:ok
|
||||
else
|
||||
# Build new details for each changed system
|
||||
@@ -153,10 +155,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
|
||||
|
||||
changed_data = Map.take(updated_details_map, changed_systems)
|
||||
|
||||
@pubsub_client.broadcast!(WandererApp.PubSub, map_id, %{
|
||||
event: :detailed_kills_updated,
|
||||
payload: changed_data
|
||||
})
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :detailed_kills_updated, changed_data)
|
||||
|
||||
:ok
|
||||
end
|
||||
@@ -173,6 +172,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
|
||||
Enum.filter(all_system_ids, fn system_id ->
|
||||
new_kills_count = Map.get(new_kills_map, system_id, 0)
|
||||
old_kills_count = Map.get(old_kills_map, system_id, 0)
|
||||
|
||||
new_kills_count != old_kills_count and
|
||||
(new_kills_count > 0 or (old_kills_count > 0 and new_kills_count == 0))
|
||||
end)
|
||||
@@ -187,16 +187,13 @@ defmodule WandererApp.Map.ZkbDataFetcher do
|
||||
|
||||
payload = Map.take(new_kills_map, changed_system_ids)
|
||||
|
||||
@pubsub_client.broadcast!(WandererApp.PubSub, map_id, %{
|
||||
event: :kills_updated,
|
||||
payload: payload
|
||||
})
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :kills_updated, payload)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp with_started_map(map_id, label \\ "operation", fun) when is_function(fun, 0) do
|
||||
defp with_started_map(map_id, label, fun) when is_function(fun, 0) do
|
||||
if WandererApp.Cache.lookup!("map_#{map_id}:started", false) do
|
||||
fun.()
|
||||
else
|
||||
|
||||
312
lib/wanderer_app/map/operations/connections.ex
Normal file
312
lib/wanderer_app/map/operations/connections.ex
Normal file
@@ -0,0 +1,312 @@
|
||||
defmodule WandererApp.Map.Operations.Connections do
|
||||
@moduledoc """
|
||||
Operations for managing map connections, including creation, updates, and deletions.
|
||||
Handles special cases like C1 wormhole sizing rules and unique constraint handling.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
alias WandererApp.Map.Server.{ConnectionsImpl, Server}
|
||||
alias Ash.Error.Invalid
|
||||
alias WandererApp.MapConnectionRepo
|
||||
|
||||
# Connection type constants
|
||||
@connection_type_wormhole 0
|
||||
@connection_type_stargate 1
|
||||
|
||||
# Ship size constants
|
||||
@small_ship_size 0
|
||||
@medium_ship_size 1
|
||||
@large_ship_size 2
|
||||
@xlarge_ship_size 3
|
||||
|
||||
# System class constants
|
||||
@c1_system_class "C1"
|
||||
|
||||
@doc """
|
||||
Creates a connection between two systems, applying special rules for C1 wormholes.
|
||||
Handles parsing of input parameters, validates system information, and manages
|
||||
unique constraint violations gracefully.
|
||||
"""
|
||||
def create(attrs, map_id, char_id) do
|
||||
do_create(attrs, map_id, char_id)
|
||||
end
|
||||
|
||||
defp do_create(attrs, map_id, char_id) do
|
||||
with {:ok, source} <- parse_int(attrs["solar_system_source"], "solar_system_source"),
|
||||
{:ok, target} <- parse_int(attrs["solar_system_target"], "solar_system_target"),
|
||||
{:ok, src_info} <- ConnectionsImpl.get_system_static_info(source),
|
||||
{:ok, tgt_info} <- ConnectionsImpl.get_system_static_info(target) do
|
||||
build_and_add_connection(attrs, map_id, char_id, src_info, tgt_info)
|
||||
else
|
||||
{:error, reason} -> handle_precondition_error(reason, attrs)
|
||||
{:ok, []} -> {:error, :inconsistent_state}
|
||||
other -> {:error, :unexpected_precondition_error, other}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_and_add_connection(attrs, map_id, char_id, src_info, tgt_info) do
|
||||
info = %{
|
||||
solar_system_source_id: src_info.solar_system_id,
|
||||
solar_system_target_id: tgt_info.solar_system_id,
|
||||
character_id: char_id,
|
||||
type: parse_type(attrs["type"]),
|
||||
ship_size_type: resolve_ship_size(attrs, src_info, tgt_info)
|
||||
}
|
||||
|
||||
case Server.add_connection(map_id, info) do
|
||||
:ok -> {:ok, :created}
|
||||
{:ok, []} -> log_warn_and(:inconsistent_state, info)
|
||||
{:error, %Invalid{errors: errs}} = err ->
|
||||
if Enum.any?(errs, &is_unique_constraint_error?/1), do: {:skip, :exists}, else: err
|
||||
{:error, _} = err -> Logger.error("[add_connection] #{inspect(err)}"); {:error, :server_error}
|
||||
other -> Logger.error("[add_connection] unexpected: #{inspect(other)}"); {:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_ship_size(attrs, src_info, tgt_info) do
|
||||
type = parse_type(attrs["type"])
|
||||
|
||||
if type == @connection_type_wormhole and
|
||||
(src_info.system_class == @c1_system_class or
|
||||
tgt_info.system_class == @c1_system_class) do
|
||||
@medium_ship_size
|
||||
else
|
||||
parse_ship_size(attrs["ship_size_type"], @large_ship_size)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_ship_size(nil, default), do: default
|
||||
defp parse_ship_size(val, _default) when is_integer(val), do: val
|
||||
defp parse_ship_size(val, default) when is_binary(val) do
|
||||
case Integer.parse(val) do
|
||||
{i, _} -> i
|
||||
:error -> default
|
||||
end
|
||||
end
|
||||
defp parse_ship_size(_, default), do: default
|
||||
|
||||
defp parse_type(nil), do: @connection_type_wormhole
|
||||
defp parse_type(val) when is_integer(val), do: val
|
||||
defp parse_type(val) when is_binary(val) do
|
||||
case Integer.parse(val) do
|
||||
{i, _} -> i
|
||||
:error -> @connection_type_wormhole
|
||||
end
|
||||
end
|
||||
defp parse_type(_), do: @connection_type_wormhole
|
||||
|
||||
defp parse_int(nil, field), do: {:error, {:missing_field, field}}
|
||||
defp parse_int(val, _) when is_integer(val), do: {:ok, val}
|
||||
defp parse_int(val, _) when is_binary(val) do
|
||||
case Integer.parse(val) do
|
||||
{i, _} -> {:ok, i}
|
||||
:error -> {:error, :invalid_integer}
|
||||
end
|
||||
end
|
||||
defp parse_int(_, field), do: {:error, {:invalid_field, field}}
|
||||
|
||||
defp handle_precondition_error(reason, attrs) do
|
||||
Logger.warning("[add_connection] precondition failed: #{inspect(reason)} for #{inspect(attrs)}")
|
||||
{:error, :precondition_failed, reason}
|
||||
end
|
||||
|
||||
defp log_warn_and(return, info) do
|
||||
Logger.warning("[add_connection] inconsistent for #{inspect(info)}")
|
||||
{:error, return}
|
||||
end
|
||||
|
||||
defp is_unique_constraint_error?(%{code: :unique_constraint}), do: true
|
||||
defp is_unique_constraint_error?(_), do: false
|
||||
|
||||
@spec list_connections(String.t()) :: [map()] | {:error, atom()}
|
||||
def list_connections(map_id) do
|
||||
with {:ok, conns} <- MapConnectionRepo.get_by_map(map_id) do
|
||||
conns
|
||||
else
|
||||
{:error, err} ->
|
||||
Logger.warning("[list_connections] Repo error: #{inspect(err)}")
|
||||
{:error, :repo_error}
|
||||
other ->
|
||||
Logger.error("[list_connections] Unexpected repo result: #{inspect(other)}")
|
||||
{:error, :unexpected_repo_result}
|
||||
end
|
||||
end
|
||||
|
||||
@spec list_connections(String.t(), integer()) :: [map()]
|
||||
def list_connections(map_id, system_id) do
|
||||
list_connections(map_id)
|
||||
|> Enum.filter(fn c ->
|
||||
c.solar_system_source == system_id or c.solar_system_target == system_id
|
||||
end)
|
||||
end
|
||||
|
||||
@spec get_connection(String.t(), String.t()) :: {:ok, map()} | {:error, String.t()}
|
||||
def get_connection(map_id, conn_id) do
|
||||
case MapConnectionRepo.get_by_id(map_id, conn_id) do
|
||||
{:ok, conn} -> {:ok, conn}
|
||||
_ -> {:error, "Connection not found"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_connection(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def update_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, conn_id, attrs) do
|
||||
with {:ok, conn_struct} <- MapConnectionRepo.get_by_id(map_id, conn_id),
|
||||
result <- (
|
||||
try do
|
||||
_allowed_keys = [
|
||||
:mass_status,
|
||||
:ship_size_type,
|
||||
:type
|
||||
]
|
||||
_update_map =
|
||||
attrs
|
||||
|> Enum.filter(fn {k, _v} -> k in ["mass_status", "ship_size_type", "type"] end)
|
||||
|> Enum.map(fn {k, v} -> {String.to_atom(k), v} end)
|
||||
|> Enum.into(%{})
|
||||
res = apply_connection_updates(map_id, conn_struct, attrs, char_id)
|
||||
res
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("[update_connection] Exception: #{inspect(error)}")
|
||||
{:error, :exception}
|
||||
end
|
||||
),
|
||||
:ok <- result,
|
||||
{:ok, updated_conn} <- MapConnectionRepo.get_by_id(map_id, conn_id) do
|
||||
{:ok, updated_conn}
|
||||
else
|
||||
{:error, err} -> {:error, err}
|
||||
_ -> {:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
def update_connection(_conn, _conn_id, _attrs), do: {:error, :missing_params}
|
||||
|
||||
@spec delete_connection(Plug.Conn.t(), integer(), integer()) :: :ok | {:error, atom()}
|
||||
def delete_connection(%{assigns: %{map_id: map_id}} = _conn, src, tgt) do
|
||||
case Server.delete_connection(map_id, %{solar_system_source_id: src, solar_system_target_id: tgt}) do
|
||||
:ok -> :ok
|
||||
{:error, :not_found} ->
|
||||
Logger.warning("[delete_connection] Connection not found: source=#{inspect(src)}, target=#{inspect(tgt)}")
|
||||
{:error, :not_found}
|
||||
{:error, _} = err ->
|
||||
Logger.error("[delete_connection] Server error: #{inspect(err)}")
|
||||
{:error, :server_error}
|
||||
_ ->
|
||||
Logger.error("[delete_connection] Unknown error")
|
||||
{:error, :unknown}
|
||||
end
|
||||
end
|
||||
def delete_connection(_conn, _src, _tgt), do: {:error, :missing_params}
|
||||
|
||||
@doc "Batch upsert for connections"
|
||||
@spec upsert_batch(Plug.Conn.t(), [map()]) :: %{created: integer(), updated: integer(), skipped: integer()}
|
||||
def upsert_batch(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = conn, conns) do
|
||||
_assigns = %{map_id: map_id, char_id: char_id}
|
||||
Enum.reduce(conns, %{created: 0, updated: 0, skipped: 0}, fn conn_attrs, acc ->
|
||||
case upsert_single(conn, conn_attrs) do
|
||||
{:ok, :created} -> %{acc | created: acc.created + 1}
|
||||
{:ok, :updated} -> %{acc | updated: acc.updated + 1}
|
||||
_ -> %{acc | skipped: acc.skipped + 1}
|
||||
end
|
||||
end)
|
||||
end
|
||||
def upsert_batch(_conn, _conns), do: %{created: 0, updated: 0, skipped: 0}
|
||||
|
||||
@doc "Upsert a single connection"
|
||||
@spec upsert_single(Plug.Conn.t(), map()) :: {:ok, :created | :updated} | {:error, atom()}
|
||||
def upsert_single(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = conn, conn_data) do
|
||||
source = conn_data["solar_system_source"] || conn_data[:solar_system_source]
|
||||
target = conn_data["solar_system_target"] || conn_data[:solar_system_target]
|
||||
with {:ok, %{} = existing_conn} <- get_connection_by_systems(map_id, source, target),
|
||||
{:ok, _} <- update_connection(conn, existing_conn.id, conn_data) do
|
||||
{:ok, :updated}
|
||||
else
|
||||
{:ok, nil} ->
|
||||
case create_connection(map_id, conn_data, char_id) do
|
||||
{:ok, _} -> {:ok, :created}
|
||||
{:skip, :exists} -> {:ok, :updated}
|
||||
err -> {:error, err}
|
||||
end
|
||||
{:error, _} = err ->
|
||||
Logger.warning("[upsert_single] Connection lookup error: #{inspect(err)}")
|
||||
{:error, :lookup_error}
|
||||
err ->
|
||||
Logger.error("[upsert_single] Update failed: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
def upsert_single(_conn, _conn_data), do: {:error, :missing_params}
|
||||
|
||||
@doc "Get a connection by source and target system IDs"
|
||||
@spec get_connection_by_systems(String.t(), integer(), integer()) :: {:ok, map()} | {:error, String.t()}
|
||||
def get_connection_by_systems(map_id, source, target) do
|
||||
with {:ok, conn} <- WandererApp.Map.find_connection(map_id, source, target) do
|
||||
if conn, do: {:ok, conn}, else: WandererApp.Map.find_connection(map_id, target, source)
|
||||
else
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# -- Helpers ---------------------------------------------------------------
|
||||
|
||||
defp apply_connection_updates(map_id, conn, attrs, _char_id) do
|
||||
Enum.reduce_while(attrs, :ok, fn {key, val}, _acc ->
|
||||
result =
|
||||
case key do
|
||||
"mass_status" -> maybe_update_mass_status(map_id, conn, val)
|
||||
"ship_size_type" -> maybe_update_ship_size_type(map_id, conn, val)
|
||||
"type" -> maybe_update_type(map_id, conn, val)
|
||||
_ -> :ok
|
||||
end
|
||||
if result == :ok do
|
||||
{:cont, :ok}
|
||||
else
|
||||
{:halt, result}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
:ok -> :ok
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_update_mass_status(_map_id, _conn, nil), do: :ok
|
||||
defp maybe_update_mass_status(map_id, conn, value) do
|
||||
Server.update_connection_mass_status(map_id, %{
|
||||
solar_system_source_id: conn.solar_system_source,
|
||||
solar_system_target_id: conn.solar_system_target,
|
||||
mass_status: value
|
||||
})
|
||||
end
|
||||
|
||||
defp maybe_update_ship_size_type(_map_id, _conn, nil), do: :ok
|
||||
defp maybe_update_ship_size_type(map_id, conn, value) do
|
||||
Server.update_connection_ship_size_type(map_id, %{
|
||||
solar_system_source_id: conn.solar_system_source,
|
||||
solar_system_target_id: conn.solar_system_target,
|
||||
ship_size_type: value
|
||||
})
|
||||
end
|
||||
|
||||
defp maybe_update_type(_map_id, _conn, nil), do: :ok
|
||||
defp maybe_update_type(map_id, conn, value) do
|
||||
Server.update_connection_type(map_id, %{
|
||||
solar_system_source_id: conn.solar_system_source,
|
||||
solar_system_target_id: conn.solar_system_target,
|
||||
type: value
|
||||
})
|
||||
end
|
||||
|
||||
@doc "Creates a connection between two systems"
|
||||
@spec create_connection(String.t(), map(), String.t()) :: {:ok, :created} | {:skip, :exists} | {:error, atom()}
|
||||
def create_connection(map_id, attrs, char_id) do
|
||||
do_create(attrs, map_id, char_id)
|
||||
end
|
||||
|
||||
@doc "Creates a connection between two systems from a Plug.Conn"
|
||||
@spec create_connection(Plug.Conn.t(), map()) :: {:ok, :created} | {:skip, :exists} | {:error, atom()}
|
||||
def create_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, attrs) do
|
||||
do_create(attrs, map_id, char_id)
|
||||
end
|
||||
|
||||
end
|
||||
75
lib/wanderer_app/map/operations/owner.ex
Normal file
75
lib/wanderer_app/map/operations/owner.ex
Normal file
@@ -0,0 +1,75 @@
|
||||
defmodule WandererApp.Map.Operations.Owner do
|
||||
@moduledoc """
|
||||
Handles fetching and caching of the main character info for a map owner.
|
||||
"""
|
||||
|
||||
# Cache TTL in milliseconds (24 hours)
|
||||
@owner_info_cache_ttl 86_400_000
|
||||
|
||||
alias WandererApp.{
|
||||
MapRepo,
|
||||
MapCharacterSettingsRepo,
|
||||
MapUserSettingsRepo,
|
||||
Cache
|
||||
}
|
||||
alias WandererApp.Character
|
||||
alias WandererApp.Character.TrackingUtils
|
||||
|
||||
@spec get_owner_character_id(String.t()) :: {:ok, %{id: term(), user_id: term()}} | {:error, String.t()}
|
||||
def get_owner_character_id(map_id) do
|
||||
cache_key = "map_#{map_id}:owner_info"
|
||||
|
||||
case Cache.lookup!(cache_key) do
|
||||
nil ->
|
||||
with {:ok, owner} <- fetch_map_owner(map_id),
|
||||
{:ok, char_ids} <- fetch_character_ids(map_id),
|
||||
{:ok, characters} <- load_characters(char_ids),
|
||||
{:ok, user_settings} <- MapUserSettingsRepo.get(map_id, owner.id),
|
||||
{:ok, main} <- TrackingUtils.get_main_character(user_settings, characters, characters) do
|
||||
result = %{id: main.id, user_id: main.user_id}
|
||||
Cache.insert(cache_key, result, ttl: @owner_info_cache_ttl)
|
||||
{:ok, result}
|
||||
else
|
||||
{:error, msg} -> {:error, msg}
|
||||
_ -> {:error, "Failed to resolve main character"}
|
||||
end
|
||||
|
||||
cached ->
|
||||
{:ok, cached}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map_owner(map_id) do
|
||||
case MapRepo.get(map_id, [:owner]) do
|
||||
{:ok, %{owner: %_{} = owner}} -> {:ok, owner}
|
||||
{:ok, %{owner: nil}} -> {:error, "Map has no owner"}
|
||||
{:error, _} -> {:error, "Map not found"}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_character_ids(map_id) do
|
||||
case MapCharacterSettingsRepo.get_all_by_map(map_id) do
|
||||
{:ok, settings} when is_list(settings) and settings != [] ->
|
||||
{:ok, Enum.map(settings, & &1.character_id)}
|
||||
|
||||
{:ok, []} ->
|
||||
{:error, "No character settings found"}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Failed to fetch character settings"}
|
||||
end
|
||||
end
|
||||
|
||||
defp load_characters(ids) when is_list(ids) do
|
||||
ids
|
||||
|> Enum.map(&Character.get_character/1)
|
||||
|> Enum.flat_map(fn
|
||||
{:ok, ch} -> [ch]
|
||||
_ -> []
|
||||
end)
|
||||
|> case do
|
||||
[] -> {:error, "No valid characters found"}
|
||||
chars -> {:ok, chars}
|
||||
end
|
||||
end
|
||||
end
|
||||
114
lib/wanderer_app/map/operations/signatures.ex
Normal file
114
lib/wanderer_app/map/operations/signatures.ex
Normal file
@@ -0,0 +1,114 @@
|
||||
defmodule WandererApp.Map.Operations.Signatures do
|
||||
@moduledoc """
|
||||
CRUD for map signatures.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
alias WandererApp.Map.Operations
|
||||
alias WandererApp.Api.{MapSystem, MapSystemSignature}
|
||||
alias WandererApp.Map.Server
|
||||
|
||||
@spec list_signatures(String.t()) :: [map()]
|
||||
def list_signatures(map_id) do
|
||||
systems = Operations.list_systems(map_id)
|
||||
if systems != [] do
|
||||
systems
|
||||
|> Enum.flat_map(fn sys ->
|
||||
with {:ok, sigs} <- MapSystemSignature.by_system_id(sys.id) do
|
||||
sigs
|
||||
else
|
||||
err ->
|
||||
Logger.error("[list_signatures] error: #{inspect(err)}")
|
||||
[]
|
||||
end
|
||||
end)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_signature(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def create_signature(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, %{"solar_system_id" => _solar_system_id} = params) do
|
||||
attrs = Map.put(params, "character_eve_id", char_id)
|
||||
case Server.update_signatures(map_id, %{
|
||||
added_signatures: [attrs],
|
||||
updated_signatures: [],
|
||||
removed_signatures: [],
|
||||
solar_system_id: params["solar_system_id"],
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
}) do
|
||||
:ok -> {:ok, attrs}
|
||||
err ->
|
||||
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def create_signature(_conn, _params), do: {:error, :missing_params}
|
||||
|
||||
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def update_signature(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, sig_id, params) do
|
||||
with {:ok, sig} <- MapSystemSignature.by_id(sig_id),
|
||||
{:ok, system} <- MapSystem.by_id(sig.system_id) do
|
||||
base = %{
|
||||
"eve_id" => sig.eve_id,
|
||||
"name" => sig.name,
|
||||
"kind" => sig.kind,
|
||||
"group" => sig.group,
|
||||
"type" => sig.type,
|
||||
"custom_info" => sig.custom_info,
|
||||
"character_eve_id" => char_id,
|
||||
"description" => sig.description,
|
||||
"linked_system_id" => sig.linked_system_id
|
||||
}
|
||||
attrs = Map.merge(base, params)
|
||||
:ok = Server.update_signatures(map_id, %{
|
||||
added_signatures: [],
|
||||
updated_signatures: [attrs],
|
||||
removed_signatures: [],
|
||||
solar_system_id: system.solar_system_id,
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
})
|
||||
{:ok, attrs}
|
||||
else
|
||||
err ->
|
||||
Logger.error("[update_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def update_signature(_conn, _sig_id, _params), do: {:error, :missing_params}
|
||||
|
||||
@spec delete_signature(Plug.Conn.t(), String.t()) :: :ok | {:error, atom()}
|
||||
def delete_signature(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, sig_id) do
|
||||
with {:ok, sig} <- MapSystemSignature.by_id(sig_id),
|
||||
{:ok, system} <- MapSystem.by_id(sig.system_id) do
|
||||
removed = [%{
|
||||
"eve_id" => sig.eve_id,
|
||||
"name" => sig.name,
|
||||
"kind" => sig.kind,
|
||||
"group" => sig.group
|
||||
}]
|
||||
:ok = Server.update_signatures(map_id, %{
|
||||
added_signatures: [],
|
||||
updated_signatures: [],
|
||||
removed_signatures: removed,
|
||||
solar_system_id: system.solar_system_id,
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
})
|
||||
:ok
|
||||
else
|
||||
err ->
|
||||
Logger.error("[delete_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def delete_signature(_conn, _sig_id), do: {:error, :missing_params}
|
||||
end
|
||||
104
lib/wanderer_app/map/operations/structures.ex
Normal file
104
lib/wanderer_app/map/operations/structures.ex
Normal file
@@ -0,0 +1,104 @@
|
||||
defmodule WandererApp.Map.Operations.Structures do
|
||||
@moduledoc """
|
||||
CRUD for map structures.
|
||||
"""
|
||||
|
||||
alias WandererApp.Map.Operations
|
||||
alias WandererApp.Api.MapSystem
|
||||
alias WandererApp.Api.MapSystemStructure
|
||||
alias WandererApp.Structure
|
||||
require Logger
|
||||
|
||||
@spec list_structures(String.t()) :: [map()]
|
||||
def list_structures(map_id) do
|
||||
with systems when is_list(systems) and systems != [] <- (
|
||||
case Operations.list_systems(map_id) do
|
||||
{:ok, systems} -> systems
|
||||
systems when is_list(systems) -> systems
|
||||
_ -> []
|
||||
end
|
||||
) do
|
||||
systems
|
||||
|> Enum.flat_map(fn sys ->
|
||||
with {:ok, structs} <- MapSystemStructure.by_system_id(sys.id) do
|
||||
structs
|
||||
else
|
||||
_other -> []
|
||||
end
|
||||
end)
|
||||
else
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_structure(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def create_structure(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, %{"solar_system_id" => _solar_system_id} = params) do
|
||||
with {:ok, system} <- MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: params["solar_system_id"]}),
|
||||
attrs <- Map.put(prepare_attrs(params), "system_id", system.id),
|
||||
:ok <- Structure.update_structures(system, [attrs], [], [], char_id, user_id),
|
||||
name = Map.get(attrs, "name"),
|
||||
structure_type_id = Map.get(attrs, "structureTypeId"),
|
||||
struct when not is_nil(struct) <-
|
||||
MapSystemStructure.by_system_id!(system.id)
|
||||
|> Enum.find(fn s -> s.name == name and s.structure_type_id == structure_type_id end) do
|
||||
{:ok, struct}
|
||||
else
|
||||
nil ->
|
||||
Logger.warning("[create_structure] Structure not found after creation")
|
||||
{:error, :structure_not_found}
|
||||
err ->
|
||||
Logger.error("[create_structure] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def create_structure(_conn, _params), do: {:error, "missing params"}
|
||||
|
||||
@spec update_structure(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def update_structure(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, struct_id, params) do
|
||||
with {:ok, struct} <- MapSystemStructure.by_id(struct_id),
|
||||
{:ok, system} <- MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: struct.solar_system_id}) do
|
||||
attrs = Map.merge(prepare_attrs(params), %{"id" => struct_id})
|
||||
:ok = Structure.update_structures(system, [], [attrs], [], char_id, user_id)
|
||||
case MapSystemStructure.by_id(struct_id) do
|
||||
{:ok, updated} -> {:ok, updated}
|
||||
err ->
|
||||
Logger.error("[update_structure] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
else
|
||||
err ->
|
||||
Logger.error("[update_structure] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def update_structure(_conn, _struct_id, _params), do: {:error, "missing params"}
|
||||
|
||||
@spec delete_structure(Plug.Conn.t(), String.t()) :: :ok | {:error, atom()}
|
||||
def delete_structure(%{assigns: %{map_id: _map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, struct_id) do
|
||||
with {:ok, struct} <- MapSystemStructure.by_id(struct_id),
|
||||
{:ok, system} <- MapSystem.by_id(struct.system_id) do
|
||||
:ok = Structure.update_structures(system, [], [], [%{"id" => struct_id}], char_id, user_id)
|
||||
:ok
|
||||
else
|
||||
err ->
|
||||
Logger.error("[delete_structure] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def delete_structure(_conn, _struct_id), do: {:error, "missing params"}
|
||||
|
||||
defp prepare_attrs(params) do
|
||||
params
|
||||
|> Enum.map(fn
|
||||
{"structure_type", v} -> {"structureType", v}
|
||||
{"structure_type_id", v} -> {"structureTypeId", v}
|
||||
{"end_time", v} -> {"endTime", v}
|
||||
{k, v} -> {k, v}
|
||||
end)
|
||||
|> Map.new()
|
||||
|> Map.take(["name", "structureType", "structureTypeId", "status", "notes", "endTime"])
|
||||
end
|
||||
end
|
||||
195
lib/wanderer_app/map/operations/systems.ex
Normal file
195
lib/wanderer_app/map/operations/systems.ex
Normal file
@@ -0,0 +1,195 @@
|
||||
defmodule WandererApp.Map.Operations.Systems do
|
||||
@moduledoc """
|
||||
CRUD and batch upsert for map systems.
|
||||
"""
|
||||
|
||||
alias WandererApp.MapSystemRepo
|
||||
alias WandererApp.Map.Server
|
||||
alias WandererApp.Map.Operations.Connections
|
||||
require Logger
|
||||
|
||||
@spec list_systems(String.t()) :: [map()]
|
||||
def list_systems(map_id) do
|
||||
with {:ok, systems} <- MapSystemRepo.get_visible_by_map(map_id) do
|
||||
systems
|
||||
else
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_system(String.t(), integer()) :: {:ok, map()} | {:error, :not_found}
|
||||
def get_system(map_id, system_id) do
|
||||
MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id)
|
||||
end
|
||||
|
||||
@spec create_system(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def create_system(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, params) do
|
||||
do_create_system(map_id, user_id, char_id, params)
|
||||
end
|
||||
def create_system(_conn, _params), do: {:error, :missing_params}
|
||||
|
||||
# Private helper for batch upsert
|
||||
defp create_system_batch(%{map_id: map_id, user_id: user_id, char_id: char_id}, params) do
|
||||
do_create_system(map_id, user_id, char_id, params)
|
||||
end
|
||||
|
||||
defp do_create_system(map_id, user_id, char_id, params) do
|
||||
with {:ok, system_id} <- fetch_system_id(params),
|
||||
coords <- normalize_coordinates(params),
|
||||
:ok <- Server.add_system(map_id, %{solar_system_id: system_id, coordinates: coords}, user_id, char_id),
|
||||
{:ok, system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id) do
|
||||
{:ok, system}
|
||||
else
|
||||
{:error, reason} when is_binary(reason) ->
|
||||
Logger.warning("[do_create_system] Expected error: #{inspect(reason)}")
|
||||
{:error, :expected_error}
|
||||
_ ->
|
||||
Logger.error("[do_create_system] Unexpected error")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_system(Plug.Conn.t(), integer(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def update_system(%{assigns: %{map_id: map_id}} = _conn, system_id, attrs) do
|
||||
with {:ok, current} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id),
|
||||
x_raw <- Map.get(attrs, "position_x", Map.get(attrs, :position_x, current.position_x)),
|
||||
y_raw <- Map.get(attrs, "position_y", Map.get(attrs, :position_y, current.position_y)),
|
||||
{:ok, x} <- parse_int(x_raw, "position_x"),
|
||||
{:ok, y} <- parse_int(y_raw, "position_y"),
|
||||
coords = %{x: x, y: y},
|
||||
:ok <- apply_system_updates(map_id, system_id, attrs, coords),
|
||||
{:ok, system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id) do
|
||||
{:ok, system}
|
||||
else
|
||||
{:error, reason} when is_binary(reason) ->
|
||||
Logger.warning("[update_system] Expected error: #{inspect(reason)}")
|
||||
{:error, :expected_error}
|
||||
_ ->
|
||||
Logger.error("[update_system] Unexpected error")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
def update_system(_conn, _system_id, _attrs), do: {:error, :missing_params}
|
||||
|
||||
@spec delete_system(Plug.Conn.t(), integer()) :: {:ok, integer()} | {:error, atom()}
|
||||
def delete_system(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, system_id) do
|
||||
with {:ok, _} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id),
|
||||
:ok <- Server.delete_systems(map_id, [system_id], user_id, char_id) do
|
||||
{:ok, 1}
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
Logger.warning("[delete_system] System not found: #{inspect(system_id)}")
|
||||
{:error, :not_found}
|
||||
_ ->
|
||||
Logger.error("[delete_system] Unexpected error")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
def delete_system(_conn, _system_id), do: {:error, :missing_params}
|
||||
|
||||
@spec upsert_systems_and_connections(Plug.Conn.t(), [map()], [map()]) :: {:ok, map()} | {:error, atom()}
|
||||
def upsert_systems_and_connections(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = conn, systems, connections) do
|
||||
assigns = %{map_id: map_id, user_id: user_id, char_id: char_id}
|
||||
{created_s, updated_s, _skipped_s} = upsert_each(systems, fn sys -> create_system_batch(assigns, sys) end, 0, 0, 0)
|
||||
conn_results =
|
||||
connections
|
||||
|> Enum.reduce(%{created: 0, updated: 0, skipped: 0}, fn conn_data, acc ->
|
||||
case Connections.upsert_single(conn, conn_data) do
|
||||
{:ok, :created} -> %{acc | created: acc.created + 1}
|
||||
{:ok, :updated} -> %{acc | updated: acc.updated + 1}
|
||||
_ -> %{acc | skipped: acc.skipped + 1}
|
||||
end
|
||||
end)
|
||||
{:ok, %{
|
||||
systems: %{created: created_s, updated: updated_s},
|
||||
connections: %{created: conn_results.created, updated: conn_results.updated}
|
||||
}}
|
||||
end
|
||||
def upsert_systems_and_connections(_conn, _systems, _connections), do: {:error, :missing_params}
|
||||
|
||||
# -- Internal Helpers -------------------------------------------------------
|
||||
|
||||
defp fetch_system_id(%{"solar_system_id" => id}), do: parse_int(id, "solar_system_id")
|
||||
defp fetch_system_id(%{solar_system_id: id}) when not is_nil(id), do: parse_int(id, "solar_system_id")
|
||||
defp fetch_system_id(_), do: {:error, "Missing system identifier (id)"}
|
||||
|
||||
defp parse_int(val, _field) when is_integer(val), do: {:ok, val}
|
||||
defp parse_int(val, field) when is_binary(val) do
|
||||
case Integer.parse(val) do
|
||||
{i, _} -> {:ok, i}
|
||||
_ -> {:error, "Invalid #{field}: #{val}"}
|
||||
end
|
||||
end
|
||||
defp parse_int(nil, field), do: {:error, "Missing #{field}"}
|
||||
defp parse_int(val, field), do: {:error, "Invalid #{field} type: #{inspect(val)}"}
|
||||
|
||||
defp normalize_coordinates(%{"coordinates" => %{"x" => x, "y" => y}}) when is_number(x) and is_number(y),
|
||||
do: %{x: x, y: y}
|
||||
defp normalize_coordinates(%{coordinates: %{x: x, y: y}}) when is_number(x) and is_number(y),
|
||||
do: %{x: x, y: y}
|
||||
defp normalize_coordinates(params) do
|
||||
%{
|
||||
x: params |> Map.get("position_x", Map.get(params, :position_x, 0)),
|
||||
y: params |> Map.get("position_y", Map.get(params, :position_y, 0))
|
||||
}
|
||||
end
|
||||
|
||||
defp apply_system_updates(map_id, system_id, attrs, %{x: x, y: y}) do
|
||||
with :ok <- Server.update_system_position(map_id, %{solar_system_id: system_id, position_x: round(x), position_y: round(y)}) do
|
||||
attrs
|
||||
|> Map.drop([:coordinates, :position_x, :position_y, :solar_system_id,
|
||||
"coordinates", "position_x", "position_y", "solar_system_id"])
|
||||
|> Enum.reduce_while(:ok, fn {key, val}, _ ->
|
||||
case update_system_field(map_id, system_id, to_string(key), val) do
|
||||
:ok -> {:cont, :ok}
|
||||
err -> {:halt, err}
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp update_system_field(map_id, system_id, field, val) do
|
||||
case field do
|
||||
"status" -> Server.update_system_status(map_id, %{solar_system_id: system_id, status: convert_status(val)})
|
||||
"description" -> Server.update_system_description(map_id, %{solar_system_id: system_id, description: val})
|
||||
"tag" -> Server.update_system_tag(map_id, %{solar_system_id: system_id, tag: val})
|
||||
"locked" ->
|
||||
bool = val in [true, "true", 1, "1"]
|
||||
Server.update_system_locked(map_id, %{solar_system_id: system_id, locked: bool})
|
||||
f when f in ["label", "labels"] ->
|
||||
labels = cond do
|
||||
is_list(val) -> val
|
||||
is_binary(val) -> String.split(val, ",", trim: true)
|
||||
true -> []
|
||||
end
|
||||
Server.update_system_labels(map_id, %{solar_system_id: system_id, labels: Enum.join(labels, ",")})
|
||||
"temporary_name" -> Server.update_system_temporary_name(map_id, %{solar_system_id: system_id, temporary_name: val})
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp convert_status("CLEAR"), do: 0
|
||||
defp convert_status("DANGEROUS"), do: 1
|
||||
defp convert_status("OCCUPIED"), do: 2
|
||||
defp convert_status("MASS_CRITICAL"), do: 3
|
||||
defp convert_status("TIME_CRITICAL"), do: 4
|
||||
defp convert_status("REINFORCED"), do: 5
|
||||
defp convert_status(i) when is_integer(i), do: i
|
||||
defp convert_status(s) when is_binary(s) do
|
||||
case Integer.parse(s) do
|
||||
{i, _} -> i
|
||||
_ -> 0
|
||||
end
|
||||
end
|
||||
defp convert_status(_), do: 0
|
||||
|
||||
defp upsert_each([], _fun, c, u, d), do: {c, u, d}
|
||||
defp upsert_each([item | rest], fun, c, u, d) do
|
||||
case fun.(item) do
|
||||
{:ok, _} -> upsert_each(rest, fun, c + 1, u, d)
|
||||
:ok -> upsert_each(rest, fun, c + 1, u, d)
|
||||
{:skip, _} -> upsert_each(rest, fun, c, u + 1, d)
|
||||
_ -> upsert_each(rest, fun, c, u, d + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,8 +3,6 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Map.Server.Impl
|
||||
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
|
||||
def handle_map_acl_updated(%{map_id: map_id, map: old_map} = state, added_acls, removed_acls) do
|
||||
@@ -86,7 +84,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_acl_deleted(map_id, acl_id) do
|
||||
def handle_acl_deleted(map_id, _acl_id) do
|
||||
{:ok, map} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
|
||||
@@ -69,6 +69,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
@connection_time_status_eol 1
|
||||
@connection_type_wormhole 0
|
||||
@connection_type_stargate 1
|
||||
@medium_ship_size 1
|
||||
|
||||
def get_connection_auto_expire_hours(), do: WandererApp.Env.map_connection_auto_expire_hours()
|
||||
|
||||
@@ -87,14 +88,11 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
:timer.minutes(get_eol_expire_timeout_mins())
|
||||
|
||||
def init_eol_cache(map_id, connections_eol_time) do
|
||||
eol_expire_timeout = get_eol_expire_timeout()
|
||||
|
||||
connections_eol_time
|
||||
|> Enum.each(fn {connection_id, connection_eol_time} ->
|
||||
WandererApp.Cache.put(
|
||||
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
|
||||
connection_eol_time,
|
||||
ttl: eol_expire_timeout
|
||||
connection_eol_time
|
||||
)
|
||||
end)
|
||||
end
|
||||
@@ -176,11 +174,12 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
%{time_status: old_time_status}, %{id: connection_id, time_status: time_status} ->
|
||||
case time_status == @connection_time_status_eol do
|
||||
true ->
|
||||
WandererApp.Cache.put(
|
||||
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
|
||||
DateTime.utc_now(),
|
||||
ttl: get_eol_expire_timeout()
|
||||
)
|
||||
if old_time_status != @connection_time_status_eol do
|
||||
WandererApp.Cache.put(
|
||||
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
_ ->
|
||||
if old_time_status == @connection_time_status_eol do
|
||||
@@ -355,12 +354,26 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
@connection_type_wormhole
|
||||
end
|
||||
|
||||
# Check if either system is C1 before creating the connection
|
||||
{:ok, source_system_info} = get_system_static_info(old_location.solar_system_id)
|
||||
{:ok, target_system_info} = get_system_static_info(location.solar_system_id)
|
||||
|
||||
# Set ship size type to medium only for wormhole connections involving C1 systems
|
||||
ship_size_type = if connection_type == @connection_type_wormhole and
|
||||
(source_system_info.system_class == @c1 or
|
||||
target_system_info.system_class == @c1) do
|
||||
@medium_ship_size
|
||||
else
|
||||
2 # Default to large for non-wormhole or non-C1 connections
|
||||
end
|
||||
|
||||
{:ok, connection} =
|
||||
WandererApp.MapConnectionRepo.create(%{
|
||||
map_id: map_id,
|
||||
solar_system_source: old_location.solar_system_id,
|
||||
solar_system_target: location.solar_system_id,
|
||||
type: connection_type
|
||||
type: connection_type,
|
||||
ship_size_type: ship_size_type
|
||||
})
|
||||
|
||||
if connection_type == @connection_type_wormhole do
|
||||
@@ -499,7 +512,13 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
known_jumps |> Enum.empty?()
|
||||
|
||||
:stargates ->
|
||||
not is_prohibited_system_class?(from_system_static_info.system_class) and
|
||||
# For stargates, we need to check:
|
||||
# 1. Both systems are in known space (HS, LS, NS)
|
||||
# 2. There is a known jump between them
|
||||
# 3. Neither system is prohibited
|
||||
from_system_static_info.system_class in @known_space and
|
||||
to_system_static_info.system_class in @known_space and
|
||||
not is_prohibited_system_class?(from_system_static_info.system_class) and
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (known_jumps |> Enum.empty?())
|
||||
end
|
||||
|
||||
@@ -319,7 +319,11 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
|
||||
def broadcast!(map_id, event, payload \\ nil) do
|
||||
if can_broadcast?(map_id) do
|
||||
@pubsub_client.broadcast!(WandererApp.PubSub, map_id, %{event: event, payload: payload})
|
||||
@pubsub_client.broadcast!(WandererApp.PubSub, map_id, %{
|
||||
event: event,
|
||||
payload: payload,
|
||||
timestamp: DateTime.utc_now()
|
||||
})
|
||||
end
|
||||
|
||||
:ok
|
||||
|
||||
@@ -3,147 +3,187 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Api.{MapSystem, MapSystemSignature}
|
||||
alias WandererApp.Character
|
||||
alias WandererApp.User.ActivityTracker
|
||||
alias WandererApp.Map.Server.{Impl, ConnectionsImpl, SystemsImpl}
|
||||
|
||||
@doc """
|
||||
Public entrypoint for updating signatures on a map system.
|
||||
"""
|
||||
def update_signatures(
|
||||
%{map_id: map_id} = state,
|
||||
%{
|
||||
solar_system_id: solar_system_id,
|
||||
character_id: character_id,
|
||||
solar_system_id: system_solar_id,
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: delete_connection_with_sigs,
|
||||
added_signatures: added_signatures,
|
||||
updated_signatures: updated_signatures,
|
||||
removed_signatures: removed_signatures
|
||||
} =
|
||||
_signatures_update
|
||||
delete_connection_with_sigs: delete_conn?,
|
||||
added_signatures: added_params,
|
||||
updated_signatures: updated_params,
|
||||
removed_signatures: removed_params
|
||||
}
|
||||
)
|
||||
when not is_nil(character_id) do
|
||||
WandererApp.Api.MapSystem.read_by_map_and_solar_system(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id
|
||||
})
|
||||
|> case do
|
||||
{:ok, system} ->
|
||||
{:ok, %{eve_id: character_eve_id}} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
added_signatures =
|
||||
added_signatures
|
||||
|> parse_signatures(character_eve_id, system.id)
|
||||
|
||||
updated_signatures =
|
||||
updated_signatures
|
||||
|> parse_signatures(character_eve_id, system.id)
|
||||
|
||||
updated_signatures_eve_ids =
|
||||
updated_signatures
|
||||
|> Enum.map(fn s -> s.eve_id end)
|
||||
|
||||
removed_signatures_eve_ids =
|
||||
removed_signatures
|
||||
|> parse_signatures(character_eve_id, system.id)
|
||||
|> Enum.map(fn s -> s.eve_id end)
|
||||
|
||||
WandererApp.Api.MapSystemSignature.by_system_id!(system.id)
|
||||
|> Enum.filter(fn s -> s.eve_id in removed_signatures_eve_ids end)
|
||||
|> Enum.each(fn s ->
|
||||
if delete_connection_with_sigs && not is_nil(s.linked_system_id) do
|
||||
state
|
||||
|> ConnectionsImpl.delete_connection(%{
|
||||
solar_system_source_id: system.solar_system_id,
|
||||
solar_system_target_id: s.linked_system_id
|
||||
})
|
||||
end
|
||||
|
||||
if not is_nil(s.linked_system_id) do
|
||||
state
|
||||
|> SystemsImpl.update_system_linked_sig_eve_id(%{
|
||||
solar_system_id: s.linked_system_id,
|
||||
linked_sig_eve_id: nil
|
||||
})
|
||||
end
|
||||
|
||||
s
|
||||
|> Ash.destroy!()
|
||||
end)
|
||||
|
||||
WandererApp.Api.MapSystemSignature.by_system_id!(system.id)
|
||||
|> Enum.filter(fn s -> s.eve_id in updated_signatures_eve_ids end)
|
||||
|> Enum.each(fn s ->
|
||||
updated = updated_signatures |> Enum.find(fn u -> u.eve_id == s.eve_id end)
|
||||
|
||||
if not is_nil(updated) do
|
||||
s
|
||||
|> WandererApp.Api.MapSystemSignature.update(
|
||||
updated
|
||||
|> Map.put(:updated, System.os_time())
|
||||
)
|
||||
end
|
||||
end)
|
||||
|
||||
added_signatures
|
||||
|> Enum.each(fn s ->
|
||||
s |> WandererApp.Api.MapSystemSignature.create!()
|
||||
end)
|
||||
|
||||
added_signatures_eve_ids =
|
||||
added_signatures
|
||||
|> Enum.map(fn s -> s.eve_id end)
|
||||
|
||||
if not (added_signatures_eve_ids |> Enum.empty?()) do
|
||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_added, %{
|
||||
character_id: character_id,
|
||||
user_id: user_id,
|
||||
map_id: map_id,
|
||||
solar_system_id: system.solar_system_id,
|
||||
signatures: added_signatures_eve_ids
|
||||
})
|
||||
end
|
||||
|
||||
if not (removed_signatures_eve_ids |> Enum.empty?()) do
|
||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_removed, %{
|
||||
character_id: character_id,
|
||||
user_id: user_id,
|
||||
map_id: map_id,
|
||||
solar_system_id: system.solar_system_id,
|
||||
signatures: removed_signatures_eve_ids
|
||||
})
|
||||
end
|
||||
|
||||
Impl.broadcast!(map_id, :signatures_updated, system.solar_system_id)
|
||||
|
||||
state
|
||||
|
||||
_ ->
|
||||
when not is_nil(char_id) do
|
||||
with {:ok, system} <-
|
||||
MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: system_solar_id}),
|
||||
{:ok, %{eve_id: char_eve_id}} <- Character.get_character(char_id) do
|
||||
do_update_signatures(
|
||||
state,
|
||||
system,
|
||||
char_eve_id,
|
||||
user_id,
|
||||
delete_conn?,
|
||||
added_params,
|
||||
updated_params,
|
||||
removed_params
|
||||
)
|
||||
else
|
||||
error ->
|
||||
Logger.warning("Skipping signature update: #{inspect(error)}")
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
def update_signatures(
|
||||
state,
|
||||
_signatures_update
|
||||
),
|
||||
do: state
|
||||
def update_signatures(state, _), do: state
|
||||
|
||||
defp parse_signatures(signatures, character_eve_id, system_id),
|
||||
do:
|
||||
signatures
|
||||
|> Enum.map(fn %{
|
||||
"eve_id" => eve_id,
|
||||
"name" => name,
|
||||
"kind" => kind,
|
||||
"group" => group
|
||||
} = signature ->
|
||||
%{
|
||||
system_id: system_id,
|
||||
eve_id: eve_id,
|
||||
name: name,
|
||||
description: Map.get(signature, "description"),
|
||||
kind: kind,
|
||||
group: group,
|
||||
type: Map.get(signature, "type"),
|
||||
custom_info: Map.get(signature, "custom_info"),
|
||||
character_eve_id: character_eve_id
|
||||
}
|
||||
end)
|
||||
defp do_update_signatures(
|
||||
state,
|
||||
system,
|
||||
character_eve_id,
|
||||
user_id,
|
||||
delete_conn?,
|
||||
added_params,
|
||||
updated_params,
|
||||
removed_params
|
||||
) do
|
||||
# parse incoming DTOs
|
||||
added_sigs = parse_signatures(added_params, character_eve_id, system.id)
|
||||
updated_sigs = parse_signatures(updated_params, character_eve_id, system.id)
|
||||
removed_sigs = parse_signatures(removed_params, character_eve_id, system.id)
|
||||
|
||||
# fetch both current & all (including deleted) signatures once
|
||||
existing_current = MapSystemSignature.by_system_id!(system.id)
|
||||
existing_all = MapSystemSignature.by_system_id_all!(system.id)
|
||||
|
||||
removed_ids = Enum.map(removed_sigs, & &1.eve_id)
|
||||
updated_ids = Enum.map(updated_sigs, & &1.eve_id)
|
||||
added_ids = Enum.map(added_sigs, & &1.eve_id)
|
||||
|
||||
# 1. Removals
|
||||
existing_current
|
||||
|> Enum.filter(&(&1.eve_id in removed_ids))
|
||||
|> Enum.each(&remove_signature(&1, state, system, delete_conn?))
|
||||
|
||||
# 2. Updates
|
||||
existing_current
|
||||
|> Enum.filter(&(&1.eve_id in updated_ids))
|
||||
|> Enum.each(fn existing ->
|
||||
update = Enum.find(updated_sigs, &(&1.eve_id == existing.eve_id))
|
||||
apply_update_signature(existing, update)
|
||||
end)
|
||||
|
||||
# 3. Additions & restorations
|
||||
added_eve_ids = Enum.map(added_sigs, & &1.eve_id)
|
||||
existing_index = MapSystemSignature.by_system_id_all!(system.id)
|
||||
|> Enum.filter(&(&1.eve_id in added_eve_ids))
|
||||
|> Map.new(&{&1.eve_id, &1})
|
||||
|
||||
added_sigs
|
||||
|> Enum.each(fn sig ->
|
||||
case existing_index[sig.eve_id] do
|
||||
nil ->
|
||||
MapSystemSignature.create!(sig)
|
||||
|
||||
%MapSystemSignature{deleted: true} = deleted_sig ->
|
||||
MapSystemSignature.update!(
|
||||
deleted_sig,
|
||||
Map.take(sig, [:name, :description, :kind, :group, :type, :custom_info, :deleted])
|
||||
)
|
||||
|
||||
_ ->
|
||||
:noop
|
||||
end
|
||||
end)
|
||||
|
||||
# 4. Activity tracking
|
||||
if added_ids != [] do
|
||||
track_activity(:signatures_added, state.map_id, system.solar_system_id, user_id, character_eve_id,
|
||||
added_ids
|
||||
)
|
||||
end
|
||||
|
||||
if removed_ids != [] do
|
||||
track_activity(
|
||||
:signatures_removed,
|
||||
state.map_id,
|
||||
system.solar_system_id,
|
||||
user_id,
|
||||
character_eve_id,
|
||||
removed_ids
|
||||
)
|
||||
end
|
||||
|
||||
# 5. Broadcast to any live subscribers
|
||||
Impl.broadcast!(state.map_id, :signatures_updated, system.solar_system_id)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
defp remove_signature(sig, state, system, delete_conn?) do
|
||||
# optionally remove the linked connection
|
||||
if delete_conn? && sig.linked_system_id do
|
||||
ConnectionsImpl.delete_connection(state, %{
|
||||
solar_system_source_id: system.solar_system_id,
|
||||
solar_system_target_id: sig.linked_system_id
|
||||
})
|
||||
end
|
||||
|
||||
# clear any linked_sig_eve_id on the target system
|
||||
if sig.linked_system_id do
|
||||
SystemsImpl.update_system_linked_sig_eve_id(state, %{
|
||||
solar_system_id: sig.linked_system_id,
|
||||
linked_sig_eve_id: nil
|
||||
})
|
||||
end
|
||||
|
||||
# mark as deleted
|
||||
MapSystemSignature.update!(sig, %{deleted: true})
|
||||
end
|
||||
|
||||
defp apply_update_signature(%MapSystemSignature{} = existing, update_params)
|
||||
when not is_nil(update_params) do
|
||||
case MapSystemSignature.update(existing, update_params) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to update signature #{existing.id}: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp track_activity(event, map_id, solar_system_id, user_id, character_id, signatures) do
|
||||
ActivityTracker.track_map_event(event, %{
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id,
|
||||
user_id: user_id,
|
||||
character_id: character_id,
|
||||
signatures: signatures
|
||||
})
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp parse_signatures(signatures, character_eve_id, system_id) do
|
||||
Enum.map(signatures, fn sig ->
|
||||
%{
|
||||
system_id: system_id,
|
||||
eve_id: sig["eve_id"],
|
||||
name: sig["name"],
|
||||
description: Map.get(sig, "description"),
|
||||
kind: sig["kind"],
|
||||
group: sig["group"],
|
||||
type: Map.get(sig, "type"),
|
||||
custom_info: Map.get(sig, "custom_info"),
|
||||
character_eve_id: character_eve_id,
|
||||
deleted: false
|
||||
}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ defmodule WandererApp.Maps do
|
||||
:is_shattered
|
||||
]
|
||||
|
||||
def find_routes(map_id, hubs, origin, routes_settings) do
|
||||
def find_routes(map_id, hubs, origin, routes_settings, false) do
|
||||
{:ok, routes} =
|
||||
WandererApp.Esi.find_routes(
|
||||
map_id,
|
||||
@@ -48,6 +48,19 @@ defmodule WandererApp.Maps do
|
||||
{:ok, %{routes: routes, systems_static_data: systems_static_data}}
|
||||
end
|
||||
|
||||
def find_routes(map_id, hubs, origin, routes_settings, true) do
|
||||
origin = origin |> String.to_integer()
|
||||
hubs = hubs |> Enum.map(&(&1 |> String.to_integer()))
|
||||
|
||||
routes =
|
||||
hubs
|
||||
|> Enum.map(fn hub ->
|
||||
%{origin: origin, destination: hub, success: false, systems: [], has_connection: false}
|
||||
end)
|
||||
|
||||
{:ok, %{routes: routes, systems_static_data: []}}
|
||||
end
|
||||
|
||||
def get_available_maps() do
|
||||
case WandererApp.Api.Map.available() do
|
||||
{:ok, maps} -> {:ok, maps}
|
||||
|
||||
@@ -95,4 +95,12 @@ defmodule WandererApp.MapConnectionRepo do
|
||||
do:
|
||||
connection
|
||||
|> WandererApp.Api.MapConnection.update_custom_info(update)
|
||||
|
||||
def get_by_id(map_id, id) do
|
||||
case WandererApp.Api.MapConnection.by_id(id) do
|
||||
{:ok, conn} when conn.map_id == map_id -> {:ok, conn}
|
||||
{:ok, _} -> {:error, :not_found}
|
||||
{:error, _} -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,9 @@ defmodule WandererApp.Structure do
|
||||
alias WandererApp.Api.MapSystemStructure
|
||||
alias WandererApp.Character
|
||||
|
||||
def update_structures(system, added, updated, removed, main_character_eve_id) do
|
||||
def update_structures(system, added, updated, removed, main_character_eve_id, user_id \\ nil) do
|
||||
Logger.info("[Structure] update_structures called by user_id=#{inspect(user_id)}")
|
||||
|
||||
added_structs =
|
||||
parse_structures(added, main_character_eve_id, system)
|
||||
|> Enum.map(&Map.delete(&1, :id))
|
||||
@@ -105,7 +107,40 @@ defmodule WandererApp.Structure do
|
||||
# remove PK so Ash doesn't treat it as a new record
|
||||
updated_data = Map.delete(updated_data, :id)
|
||||
|
||||
new_record = MapSystemStructure.update(existing, updated_data)
|
||||
# Merge update data with existing record to avoid nil required fields
|
||||
merged_data =
|
||||
Map.merge(Map.from_struct(existing), updated_data, fn _k, v1, v2 ->
|
||||
if is_nil(v2), do: v1, else: v2
|
||||
end)
|
||||
|
||||
# Only keep fields accepted by Ash update action
|
||||
allowed_keys = [
|
||||
:system_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
]
|
||||
|
||||
filtered_data = Map.take(merged_data, allowed_keys)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"[Structure] update_structures_in_db: calling update for id=#{existing.id} with: #{inspect(filtered_data)}"
|
||||
end)
|
||||
|
||||
new_record = MapSystemStructure.update(existing, filtered_data)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"[Structure] update_structures_in_db: update result for id=#{existing.id}: #{inspect(new_record)}"
|
||||
end)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"[Structure] updated record =>\n" <> inspect(new_record, pretty: true)
|
||||
|
||||
@@ -162,7 +162,7 @@ defmodule WandererApp.Zkb.KillsPreloader do
|
||||
"[KillsPreloader] Starting #{pass_type} pass => #{length(unique_systems)} systems"
|
||||
)
|
||||
|
||||
{final_state, kills_map} =
|
||||
{final_state, _kills_map} =
|
||||
unique_systems
|
||||
|> Task.async_stream(
|
||||
fn {_map_id, system_id} ->
|
||||
|
||||
@@ -23,7 +23,9 @@ defmodule WandererApp.Zkb.Supervisor do
|
||||
},
|
||||
opts: [
|
||||
name: {:local, :zkb_kills_provider},
|
||||
mint_upgrade_opts: [Mint.WebSocket.PerMessageDeflate]
|
||||
reconnect: true,
|
||||
reconnect_after: 5_000,
|
||||
max_reconnects: :infinity
|
||||
]
|
||||
},
|
||||
preloader_child
|
||||
|
||||
@@ -11,7 +11,6 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
|
||||
use Retry
|
||||
|
||||
@heartbeat_interval 1_000
|
||||
@max_esi_retries 3
|
||||
|
||||
# Called by `KillsProvider.handle_connect`
|
||||
def handle_connect(_status, _headers, %{connected: _} = state) do
|
||||
|
||||
@@ -27,6 +27,5 @@ defmodule WandererAppWeb.ApiSpec do
|
||||
},
|
||||
security: [%{"bearerAuth" => []}]
|
||||
}
|
||||
|> OpenApiSpex.resolve_schema_modules()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,12 +66,44 @@ defmodule WandererAppWeb.Layouts do
|
||||
"""
|
||||
end
|
||||
|
||||
def youtube_container(assigns) do
|
||||
~H"""
|
||||
<.link
|
||||
href="https://www.youtube.com/@wanderer_ltd"
|
||||
class="flex flex-col p-4 items-center absolute bottom-52 left-0 gap-2 tooltip tooltip-right text-gray-400 hover:text-white"
|
||||
>
|
||||
<svg
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M20.5245 6.00694C20.3025 5.81544 20.0333 5.70603 19.836 5.63863C19.6156 5.56337 19.3637 5.50148 19.0989 5.44892C18.5677 5.34348 17.9037 5.26005 17.1675 5.19491C15.6904 5.06419 13.8392 5 12 5C10.1608 5 8.30956 5.06419 6.83246 5.1949C6.09632 5.26005 5.43231 5.34348 4.9011 5.44891C4.63628 5.50147 4.38443 5.56337 4.16403 5.63863C3.96667 5.70603 3.69746 5.81544 3.47552 6.00694C3.26514 6.18846 3.14612 6.41237 3.07941 6.55976C3.00507 6.724 2.94831 6.90201 2.90314 7.07448C2.81255 7.42043 2.74448 7.83867 2.69272 8.28448C2.58852 9.18195 2.53846 10.299 2.53846 11.409C2.53846 12.5198 2.58859 13.6529 2.69218 14.5835C2.74378 15.047 2.81086 15.4809 2.89786 15.8453C2.97306 16.1603 3.09841 16.5895 3.35221 16.9023C3.58757 17.1925 3.92217 17.324 4.08755 17.3836C4.30223 17.461 4.55045 17.5218 4.80667 17.572C5.32337 17.6733 5.98609 17.7527 6.72664 17.8146C8.2145 17.9389 10.1134 18 12 18C13.8865 18 15.7855 17.9389 17.2733 17.8146C18.0139 17.7527 18.6766 17.6733 19.1933 17.572C19.4495 17.5218 19.6978 17.461 19.9124 17.3836C20.0778 17.324 20.4124 17.1925 20.6478 16.9023C20.9016 16.5895 21.0269 16.1603 21.1021 15.8453C21.1891 15.4809 21.2562 15.047 21.3078 14.5835C21.4114 13.6529 21.4615 12.5198 21.4615 11.409C21.4615 10.299 21.4115 9.18195 21.3073 8.28448C21.2555 7.83868 21.1874 7.42043 21.0969 7.07448C21.0517 6.90201 20.9949 6.72401 20.9206 6.55976C20.8539 6.41236 20.7349 6.18846 20.5245 6.00694Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14.5385 11.5L10.0962 14.3578L10.0962 8.64207L14.5385 11.5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</.link>
|
||||
"""
|
||||
end
|
||||
|
||||
def donate_container(assigns) do
|
||||
~H"""
|
||||
<.link
|
||||
href="https://www.patreon.com/WandererLtd"
|
||||
target="_blank"
|
||||
class="flex flex-col p-4 items-center absolute bottom-52 left-1 gap-2 tooltip tooltip-right text-gray-400 hover:text-white"
|
||||
class="flex flex-col p-4 items-center absolute bottom-64 left-1 gap-2 tooltip tooltip-right text-gray-400 hover:text-white"
|
||||
>
|
||||
<.icon name="hero-banknotes-solid" class="h-4 w-4" />
|
||||
</.link>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<.ping_container rtt_class={@rtt_class} />
|
||||
<.donate_container />
|
||||
<.feedback_container />
|
||||
<.youtube_container />
|
||||
<.new_version_banner app_version={@app_version} enabled={@map_subscriptions_enabled?} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias WandererApp.Api.{AccessList, Character}
|
||||
alias WandererAppWeb.UtilAPIController, as: Util
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
import Ash.Query
|
||||
require Logger
|
||||
|
||||
@@ -200,52 +200,32 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||
operation :index,
|
||||
summary: "List ACLs for a Map",
|
||||
description: "Lists the ACLs for a given map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
|
||||
description: "Lists the ACLs for a given map. Provide only one of map_id or slug as a query parameter. If both are provided, the request will fail.",
|
||||
parameters: [
|
||||
map_id: [
|
||||
in: :query,
|
||||
description: "Map identifier (UUID) - Either map_id or slug must be provided",
|
||||
description: "Map identifier (UUID) - Provide only one of map_id or slug.",
|
||||
type: :string,
|
||||
required: false,
|
||||
example: "00000000-0000-0000-0000-000000000000"
|
||||
required: false
|
||||
],
|
||||
slug: [
|
||||
in: :query,
|
||||
description: "Map slug - Either map_id or slug must be provided",
|
||||
description: "Map slug - Provide only one of map_id or slug.",
|
||||
type: :string,
|
||||
required: false,
|
||||
example: "map-name"
|
||||
required: false
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
ok: {
|
||||
"List of ACLs",
|
||||
"application/json",
|
||||
@acl_index_response_schema
|
||||
},
|
||||
ok: {"List of ACLs", "application/json", @acl_index_response_schema},
|
||||
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{type: :string}
|
||||
},
|
||||
properties: %{error: %OpenApiSpex.Schema{type: :string}},
|
||||
required: ["error"],
|
||||
example: %{
|
||||
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG as a query parameter"
|
||||
}
|
||||
}},
|
||||
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{type: :string}
|
||||
},
|
||||
required: ["error"],
|
||||
example: %{
|
||||
"error" => "Map not found. Please provide a valid map_id or slug as a query parameter."
|
||||
}
|
||||
example: %{"error" => "Must provide only one of map_id or slug as a query parameter"}
|
||||
}}
|
||||
]
|
||||
def index(conn, params) do
|
||||
case Util.fetch_map_id(params) do
|
||||
case APIUtils.fetch_map_id(params) do
|
||||
{:ok, map_identifier} ->
|
||||
with {:ok, map} <- get_map(map_identifier),
|
||||
{:ok, loaded_map} <- Ash.load(map, acls: [:owner]) do
|
||||
@@ -277,50 +257,34 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
"""
|
||||
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||
operation :create,
|
||||
summary: "Create a new ACL",
|
||||
description: "Creates a new ACL for a map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
|
||||
summary: "Create ACL for a Map",
|
||||
description: "Creates a new ACL for a given map. Provide only one of map_id or slug as a query parameter. If both are provided, the request will fail.",
|
||||
parameters: [
|
||||
map_id: [
|
||||
in: :query,
|
||||
description: "Map identifier (UUID) - Either map_id or slug must be provided",
|
||||
description: "Map identifier (UUID) - Provide only one of map_id or slug.",
|
||||
type: :string,
|
||||
required: false,
|
||||
example: "00000000-0000-0000-0000-000000000000"
|
||||
required: false
|
||||
],
|
||||
slug: [
|
||||
in: :query,
|
||||
description: "Map slug - Either map_id or slug must be provided",
|
||||
description: "Map slug - Provide only one of map_id or slug.",
|
||||
type: :string,
|
||||
required: false,
|
||||
example: "map-name"
|
||||
required: false
|
||||
]
|
||||
],
|
||||
request_body: {"Access List parameters", "application/json", @acl_create_request_schema},
|
||||
request_body: {"ACL parameters", "application/json", @acl_create_request_schema},
|
||||
responses: [
|
||||
ok: {"Access List", "application/json", @acl_create_response_schema},
|
||||
created: {"Created ACL", "application/json", @acl_create_response_schema},
|
||||
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{type: :string}
|
||||
},
|
||||
properties: %{error: %OpenApiSpex.Schema{type: :string}},
|
||||
required: ["error"],
|
||||
example: %{
|
||||
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG as a query parameter"
|
||||
}
|
||||
}},
|
||||
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{type: :string}
|
||||
},
|
||||
required: ["error"],
|
||||
example: %{
|
||||
"error" => "Map not found. Please provide a valid map_id or slug as a query parameter."
|
||||
}
|
||||
example: %{"error" => "Must provide only one of map_id or slug as a query parameter"}
|
||||
}}
|
||||
]
|
||||
def create(conn, params) do
|
||||
with {:ok, map_identifier} <- Util.fetch_map_id(params),
|
||||
with {:ok, map_identifier} <- APIUtils.fetch_map_id(params),
|
||||
{:ok, map} <- get_map(map_identifier),
|
||||
%{"acl" => acl_params} <- params,
|
||||
owner_eve_id when not is_nil(owner_eve_id) <- Map.get(acl_params, "owner_eve_id"),
|
||||
|
||||
@@ -25,6 +25,7 @@ defmodule WandererAppWeb.AuthController do
|
||||
case WandererApp.Api.Character.by_eve_id(character_data.eve_id) do
|
||||
{:ok, character} ->
|
||||
character_update = %{
|
||||
name: auth.info.name,
|
||||
access_token: auth.credentials.token,
|
||||
refresh_token: auth.credentials.refresh_token,
|
||||
expires_at: auth.credentials.expires_at,
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
<div>
|
||||
<div class="flex mt-20px max-h-[calc(100vh-50px)] max-w-[100vw] flex-col items-center justify-start xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="shrink xl:w-1/2">
|
||||
<div class="flex min-h-[calc(100vh-100px)] items-center justify-center px-2 py-10 text-center xl:justify-start xl:pe-0 xl:ps-10 xl:text-start">
|
||||
<div class="flex mt-20px max-h-[calc(100vh-50px)] max-w-[100vw] flex-col items-center">
|
||||
<div class="shrink">
|
||||
<div class="flex min-h-[calc(100vh-100px)] items-center justify-center px-2 py-10 text-center xl:pe-0 xl:ps-10">
|
||||
<div>
|
||||
<div class="flex flex-col items-center gap-6 xl:flex-row">
|
||||
<div class="tooltip tooltip-accent" data-tip="copy">
|
||||
<button class="btn btn-sm cursor-copy rounded-full font-mono font-light">
|
||||
<pre><code>From: Wanderer Team</code></pre>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-3"></div>
|
||||
<h1 class="font-title text-center text-[clamp(2rem,6vw,4.2rem)] font-black leading-[1.1] [word-break:auto-phrase] xl:w-[115%] xl:text-start [:root[dir=rtl]_&]:leading-[1.35]">
|
||||
<h1 class="text-center text-[clamp(2rem,6vw,4rem)] font-black leading-[1.1] [word-break:auto-phrase] xl:w-[115%] xl:text-start [:root[dir=rtl]_&]:leading-[1.35]">
|
||||
<span class="[&::selection]:text-base-content brightness-150 contrast-150 [&::selection]:bg-blue-700/20">
|
||||
Join or support us!
|
||||
<!---->
|
||||
@@ -19,7 +11,7 @@
|
||||
</h1>
|
||||
<div class="h-10"></div>
|
||||
<div>
|
||||
<div class="inline-flex w-full flex-col items-stretch justify-center gap-2 px-4 md:flex-row xl:justify-start xl:px-0">
|
||||
<div class="inline-flex w-full items-stretch justify-center gap-2 px-4 flex-col">
|
||||
<.link
|
||||
href="https://discord.gg/cafERvDD2k"
|
||||
class="btn md:btn-lg group shrink-0 rounded-full [@media(min-width:768px)]:px-10 bg-[oklch(64.74%_0.124_270.62)] border-[oklch(64.74%_0.124_270.62)] hover:bg-[oklch(60%_0.124_270.62)] hover:border-[oklch(60%_0.124_270.62)]"
|
||||
@@ -226,6 +218,52 @@
|
||||
</path>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.youtube.com/@wanderer_ltd"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="YouTube"
|
||||
class="btn md:btn-lg group shrink-0 rounded-full [@media(min-width:768px)]:px-10 bg-[#ff0033] border-[#ff0033] hover:bg-[#ff0033] hover:border-[#ff0033] text-white"
|
||||
>
|
||||
<svg
|
||||
width="48px"
|
||||
height="48px"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="transition-opacity opacity-100 group-hover:opacity-0"
|
||||
>
|
||||
<path
|
||||
d="M20.5245 6.00694C20.3025 5.81544 20.0333 5.70603 19.836 5.63863C19.6156 5.56337 19.3637 5.50148 19.0989 5.44892C18.5677 5.34348 17.9037 5.26005 17.1675 5.19491C15.6904 5.06419 13.8392 5 12 5C10.1608 5 8.30956 5.06419 6.83246 5.1949C6.09632 5.26005 5.43231 5.34348 4.9011 5.44891C4.63628 5.50147 4.38443 5.56337 4.16403 5.63863C3.96667 5.70603 3.69746 5.81544 3.47552 6.00694C3.26514 6.18846 3.14612 6.41237 3.07941 6.55976C3.00507 6.724 2.94831 6.90201 2.90314 7.07448C2.81255 7.42043 2.74448 7.83867 2.69272 8.28448C2.58852 9.18195 2.53846 10.299 2.53846 11.409C2.53846 12.5198 2.58859 13.6529 2.69218 14.5835C2.74378 15.047 2.81086 15.4809 2.89786 15.8453C2.97306 16.1603 3.09841 16.5895 3.35221 16.9023C3.58757 17.1925 3.92217 17.324 4.08755 17.3836C4.30223 17.461 4.55045 17.5218 4.80667 17.572C5.32337 17.6733 5.98609 17.7527 6.72664 17.8146C8.2145 17.9389 10.1134 18 12 18C13.8865 18 15.7855 17.9389 17.2733 17.8146C18.0139 17.7527 18.6766 17.6733 19.1933 17.572C19.4495 17.5218 19.6978 17.461 19.9124 17.3836C20.0778 17.324 20.4124 17.1925 20.6478 16.9023C20.9016 16.5895 21.0269 16.1603 21.1021 15.8453C21.1891 15.4809 21.2562 15.047 21.3078 14.5835C21.4114 13.6529 21.4615 12.5198 21.4615 11.409C21.4615 10.299 21.4115 9.18195 21.3073 8.28448C21.2555 7.83868 21.1874 7.42043 21.0969 7.07448C21.0517 6.90201 20.9949 6.72401 20.9206 6.55976C20.8539 6.41236 20.7349 6.18846 20.5245 6.00694Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14.5385 11.5L10.0962 14.3578L10.0962 8.64207L14.5385 11.5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-lg font-bold transition-opacity opacity-100 group-hover:opacity-0 ml-2">
|
||||
YouTube
|
||||
</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block absolute w-6 fill-white transition-all scale-90 group-hover:scale-100 opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M19,14 L19,19 C19,20.1045695 18.1045695,21 17,21 L5,21 C3.8954305,21 3,20.1045695 3,19 L3,7 C3,5.8954305 3.8954305,5 5,5 L10,5 L10,7 L5,7 L5,19 L17,19 L17,14 L19,14 Z M18.9971001,6.41421356 L11.7042068,13.7071068 L10.2899933,12.2928932 L17.5828865,5 L12.9971001,5 L12.9971001,3 L20.9971001,3 L20.9971001,11 L18.9971001,11 L18.9971001,6.41421356 Z"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,7 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias WandererApp.CachedInfo
|
||||
alias WandererAppWeb.UtilAPIController, as: Util
|
||||
alias WandererApp.EveDataService
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
|
||||
@system_static_response_schema %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
@@ -87,8 +86,8 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
}
|
||||
]
|
||||
def show_system_static(conn, params) do
|
||||
with {:ok, solar_system_str} <- Util.require_param(params, "id"),
|
||||
{:ok, solar_system_id} <- Util.parse_int(solar_system_str) do
|
||||
with {:ok, solar_system_str} <- APIUtils.require_param(params, "id"),
|
||||
{:ok, solar_system_id} <- APIUtils.parse_int(solar_system_str) do
|
||||
case CachedInfo.get_system_static_info(solar_system_id) do
|
||||
{:ok, system} ->
|
||||
# Get basic system data
|
||||
@@ -113,11 +112,6 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a system map to a JSON-friendly format.
|
||||
|
||||
Takes only the fields that are needed for the API response.
|
||||
"""
|
||||
defp static_system_to_json(system) do
|
||||
system
|
||||
|> Map.take([
|
||||
@@ -142,12 +136,6 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Enhances system data with wormhole type information.
|
||||
|
||||
If the system has static wormholes, adds detailed information about each static.
|
||||
Otherwise, returns the original data unchanged.
|
||||
"""
|
||||
defp enhance_with_static_details(data) do
|
||||
if data[:statics] && length(data[:statics]) > 0 do
|
||||
# Add the enhanced static details to the response
|
||||
@@ -158,11 +146,6 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets detailed information for each static wormhole.
|
||||
|
||||
Uses the CachedInfo to get both wormhole type data and wormhole class data.
|
||||
"""
|
||||
defp get_static_details(statics) do
|
||||
# Get wormhole data from CachedInfo
|
||||
{:ok, wormhole_types} = CachedInfo.get_wormhole_types()
|
||||
@@ -186,12 +169,6 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates detailed wormhole information when the wormhole type is found.
|
||||
|
||||
Includes information about the destination and properties of the wormhole.
|
||||
Ensures that destination.id is always a string to match the OpenAPI schema.
|
||||
"""
|
||||
defp create_wormhole_details(wh_type, classes_by_id) do
|
||||
# Get destination class info
|
||||
dest_class = Map.get(classes_by_id, wh_type.dest)
|
||||
@@ -213,11 +190,6 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates fallback information when a wormhole type is not found.
|
||||
|
||||
Provides a placeholder structure with nil values for unknown wormhole types.
|
||||
"""
|
||||
defp create_fallback_wormhole_details(static_name) do
|
||||
%{
|
||||
name: static_name,
|
||||
|
||||
50
lib/wanderer_app_web/controllers/fallback_controller.ex
Normal file
50
lib/wanderer_app_web/controllers/fallback_controller.ex
Normal file
@@ -0,0 +1,50 @@
|
||||
defmodule WandererAppWeb.FallbackController do
|
||||
use WandererAppWeb, :controller
|
||||
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
|
||||
# Handles not_found errors from with/else
|
||||
def call(conn, {:error, :not_found}) do
|
||||
APIUtils.error_response(conn, :not_found, "Not found", "The requested resource could not be found")
|
||||
end
|
||||
|
||||
# Handles invalid_id errors
|
||||
def call(conn, {:error, :invalid_id}) do
|
||||
APIUtils.error_response(conn, :bad_request, "Invalid system ID")
|
||||
end
|
||||
|
||||
# Handles invalid_coordinates_format errors
|
||||
def call(conn, {:error, :invalid_coordinates_format}) do
|
||||
APIUtils.error_response(conn, :bad_request, "Invalid coordinates format. Use %{\"coordinates\" => %{\"x\" => number, \"y\" => number}}")
|
||||
end
|
||||
|
||||
# Handles not_associated errors
|
||||
def call(conn, {:error, :not_associated}) do
|
||||
APIUtils.error_response(conn, :not_found, "Connection not associated with specified system")
|
||||
end
|
||||
|
||||
# Handles not_involved errors
|
||||
def call(conn, {:error, :not_involved}) do
|
||||
APIUtils.error_response(conn, :bad_request, "Connection must involve specified system")
|
||||
end
|
||||
|
||||
# Handles creation_failed errors
|
||||
def call(conn, {:error, :creation_failed}) do
|
||||
APIUtils.error_response(conn, :internal_server_error, "Failed to create resource")
|
||||
end
|
||||
|
||||
# Handles deletion_failed errors
|
||||
def call(conn, {:error, :deletion_failed}) do
|
||||
APIUtils.error_response(conn, :internal_server_error, "Failed to delete resource")
|
||||
end
|
||||
|
||||
# Handles any other {:error, message} returns
|
||||
def call(conn, {:error, msg}) when is_binary(msg) do
|
||||
APIUtils.error_response(conn, :bad_request, msg)
|
||||
end
|
||||
|
||||
# Handles any other unmatched errors
|
||||
def call(conn, _error) do
|
||||
APIUtils.error_response(conn, :internal_server_error, "An unexpected error occurred")
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,17 +2,11 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
import Ash.Query, only: [filter: 2]
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Api
|
||||
alias WandererApp.Api.Character
|
||||
alias WandererApp.MapSystemRepo
|
||||
alias WandererApp.MapCharacterSettingsRepo
|
||||
|
||||
alias WandererApp.Zkb.KillsProvider.KillsCache
|
||||
|
||||
alias WandererAppWeb.UtilAPIController, as: Util
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Inline Schemas
|
||||
@@ -117,8 +111,8 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
)
|
||||
|
||||
def index(conn, params) do
|
||||
with {:ok, map_id} <- Util.fetch_map_id(params),
|
||||
{:ok, period} <- Util.require_param(params, "period"),
|
||||
with {:ok, map_id} <- APIUtils.fetch_map_id(params),
|
||||
{:ok, period} <- APIUtils.require_param(params, "period"),
|
||||
query <- WandererApp.Map.Audit.get_activity_query(map_id, period, "all"),
|
||||
{:ok, data} <-
|
||||
Api.read(query) do
|
||||
@@ -158,15 +152,4 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp get_original_system_name(solar_system_id) do
|
||||
# Fetch the original system name from the MapSolarSystem resource
|
||||
case WandererApp.Api.MapSolarSystem.by_solar_system_id(solar_system_id) do
|
||||
{:ok, system} ->
|
||||
system.solar_system_name
|
||||
|
||||
_error ->
|
||||
"Unknown System"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
# lib/wanderer_app_web/controllers/map_connection_api_controller.ex
|
||||
defmodule WandererAppWeb.MapConnectionAPIController do
|
||||
@moduledoc """
|
||||
API controller for managing map connections.
|
||||
Provides operations to list, show, create, delete, and batch-delete connections, with legacy routing support.
|
||||
"""
|
||||
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
require Logger
|
||||
|
||||
alias OpenApiSpex.Schema
|
||||
alias WandererApp.Map, as: MapData
|
||||
alias WandererApp.Map.Operations
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
alias WandererAppWeb.Schemas.ResponseSchemas
|
||||
|
||||
action_fallback WandererAppWeb.FallbackController
|
||||
|
||||
# -- JSON Schemas --
|
||||
@connection_request_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_source: %Schema{type: :integer, description: "Source system ID"},
|
||||
solar_system_target: %Schema{type: :integer, description: "Target system ID"},
|
||||
type: %Schema{type: :integer, description: "Connection type (default 0)"},
|
||||
mass_status: %Schema{type: :integer, description: "Mass status (0-3)", nullable: true},
|
||||
time_status: %Schema{type: :integer, description: "Time status (0-3)", nullable: true},
|
||||
ship_size_type: %Schema{type: :integer, description: "Ship size limit (0-3)", nullable: true},
|
||||
locked: %Schema{type: :boolean, description: "Locked flag", nullable: true},
|
||||
custom_info: %Schema{type: :string, nullable: true, description: "Optional metadata"},
|
||||
wormhole_type: %Schema{type: :string, nullable: true, description: "Wormhole code"}
|
||||
},
|
||||
required: ~w(solar_system_source solar_system_target)a,
|
||||
example: %{
|
||||
solar_system_source: 30_000_142,
|
||||
solar_system_target: 30_000_144,
|
||||
type: 0,
|
||||
mass_status: 1,
|
||||
time_status: 2,
|
||||
ship_size_type: 1,
|
||||
locked: false,
|
||||
custom_info: "Frigate only",
|
||||
wormhole_type: "C2"
|
||||
}
|
||||
}
|
||||
|
||||
@list_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
map_id: %Schema{type: :string},
|
||||
solar_system_source: %Schema{type: :integer},
|
||||
solar_system_target: %Schema{type: :integer},
|
||||
type: %Schema{type: :integer},
|
||||
mass_status: %Schema{type: :integer},
|
||||
time_status: %Schema{type: :integer},
|
||||
ship_size_type: %Schema{type: :integer},
|
||||
locked: %Schema{type: :boolean},
|
||||
custom_info: %Schema{type: :string, nullable: true},
|
||||
wormhole_type: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: [
|
||||
%{
|
||||
id: "conn-uuid-1",
|
||||
map_id: "map-uuid-1",
|
||||
solar_system_source: 30_000_142,
|
||||
solar_system_target: 30_000_144,
|
||||
type: 0,
|
||||
mass_status: 1,
|
||||
time_status: 2,
|
||||
ship_size_type: 1,
|
||||
locked: false,
|
||||
custom_info: "Frigate only",
|
||||
wormhole_type: "C2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@detail_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
map_id: %Schema{type: :string},
|
||||
solar_system_source: %Schema{type: :integer},
|
||||
solar_system_target: %Schema{type: :integer},
|
||||
type: %Schema{type: :integer},
|
||||
mass_status: %Schema{type: :integer},
|
||||
time_status: %Schema{type: :integer},
|
||||
ship_size_type: %Schema{type: :integer},
|
||||
locked: %Schema{type: :boolean},
|
||||
custom_info: %Schema{type: :string, nullable: true},
|
||||
wormhole_type: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: %{
|
||||
id: "conn-uuid-1",
|
||||
map_id: "map-uuid-1",
|
||||
solar_system_source: 30_000_142,
|
||||
solar_system_target: 30_000_144,
|
||||
type: 0,
|
||||
mass_status: 1,
|
||||
time_status: 2,
|
||||
ship_size_type: 1,
|
||||
locked: false,
|
||||
custom_info: "Frigate only",
|
||||
wormhole_type: "C2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -- Actions --
|
||||
|
||||
operation :index,
|
||||
summary: "List Map Connections",
|
||||
description: "Lists all connections for a map.",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
solar_system_source: [
|
||||
in: :query,
|
||||
description: "Filter connections by source system ID",
|
||||
type: :integer,
|
||||
required: false,
|
||||
example: 30000142
|
||||
],
|
||||
solar_system_target: [
|
||||
in: :query,
|
||||
description: "Filter connections by target system ID",
|
||||
type: :integer,
|
||||
required: false,
|
||||
example: 30000144
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
ok: {
|
||||
"List of Map Connections",
|
||||
"application/json",
|
||||
@list_response_schema
|
||||
},
|
||||
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{type: :string}
|
||||
},
|
||||
required: ["error"],
|
||||
example: %{
|
||||
"error" => "Map not found"
|
||||
}
|
||||
}}
|
||||
]
|
||||
def index(%{assigns: %{map_id: map_id}} = conn, params) do
|
||||
with {:ok, src_filter} <- parse_optional(params, "solar_system_source"),
|
||||
{:ok, tgt_filter} <- parse_optional(params, "solar_system_target") do
|
||||
conns = MapData.list_connections!(map_id)
|
||||
conns =
|
||||
conns
|
||||
|> filter_by_source(src_filter)
|
||||
|> filter_by_target(tgt_filter)
|
||||
data = Enum.map(conns, &APIUtils.connection_to_json/1)
|
||||
APIUtils.respond_data(conn, data)
|
||||
else
|
||||
{:error, msg} when is_binary(msg) ->
|
||||
conn
|
||||
|> Plug.Conn.put_status(:bad_request)
|
||||
|> APIUtils.error_response(:bad_request, msg)
|
||||
{:error, _} ->
|
||||
conn
|
||||
|> Plug.Conn.put_status(:bad_request)
|
||||
|> APIUtils.error_response(:bad_request, "Invalid filter parameter")
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_optional(params, key) do
|
||||
case Map.get(params, key) do
|
||||
nil -> {:ok, nil}
|
||||
val -> APIUtils.parse_int(val)
|
||||
end
|
||||
end
|
||||
|
||||
defp filter_by_source(conns, nil), do: conns
|
||||
defp filter_by_source(conns, s), do: Enum.filter(conns, &(&1.solar_system_source == s))
|
||||
|
||||
defp filter_by_target(conns, nil), do: conns
|
||||
defp filter_by_target(conns, t), do: Enum.filter(conns, &(&1.solar_system_target == t))
|
||||
|
||||
operation :show,
|
||||
summary: "Show Connection (by id or by source/target)",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
id: [in: :path, type: :string, required: false],
|
||||
solar_system_source: [in: :query, type: :integer, required: false],
|
||||
solar_system_target: [in: :query, type: :integer, required: false]
|
||||
],
|
||||
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||
def show(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
|
||||
case Operations.get_connection(map_id, id) do
|
||||
{:ok, conn_struct} -> APIUtils.respond_data(conn, APIUtils.connection_to_json(conn_struct))
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
def show(%{assigns: %{map_id: map_id}} = conn, %{"solar_system_source" => src, "solar_system_target" => tgt}) do
|
||||
with {:ok, source} <- APIUtils.parse_int(src),
|
||||
{:ok, target} <- APIUtils.parse_int(tgt),
|
||||
{:ok, conn_struct} <- Operations.get_connection_by_systems(map_id, source, target) do
|
||||
APIUtils.respond_data(conn, APIUtils.connection_to_json(conn_struct))
|
||||
else
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
operation :create,
|
||||
summary: "Create Connection",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
]
|
||||
],
|
||||
request_body: {"Connection create", "application/json", @connection_request_schema},
|
||||
responses: ResponseSchemas.create_responses(@detail_response_schema)
|
||||
def create(conn, params) do
|
||||
case Operations.create_connection(conn, params) do
|
||||
{:ok, conn_struct} when is_map(conn_struct) ->
|
||||
conn
|
||||
|> APIUtils.respond_data(APIUtils.connection_to_json(conn_struct), :created)
|
||||
{:ok, :created} ->
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(%{data: %{result: "created"}})
|
||||
{:skip, :exists} ->
|
||||
conn
|
||||
|> put_status(:ok)
|
||||
|> json(%{data: %{result: "exists"}})
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: reason})
|
||||
_other ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "Unexpected error"})
|
||||
end
|
||||
end
|
||||
|
||||
operation :delete,
|
||||
summary: "Delete Connection (by id or by source/target)",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
id: [in: :path, type: :string, required: false],
|
||||
solar_system_source: [in: :query, type: :integer, required: false],
|
||||
solar_system_target: [in: :query, type: :integer, required: false]
|
||||
],
|
||||
responses: ResponseSchemas.delete_responses(nil)
|
||||
def delete(%{assigns: %{map_id: _map_id}} = conn, %{"id" => id}) do
|
||||
delete_connection_id(conn, id)
|
||||
end
|
||||
|
||||
def delete(%{assigns: %{map_id: _map_id}} = conn, %{"solar_system_source" => src, "solar_system_target" => tgt}) do
|
||||
delete_by_systems(conn, src, tgt)
|
||||
end
|
||||
|
||||
# Private helpers for delete/2
|
||||
|
||||
defp delete_connection_id(conn, id) do
|
||||
case Operations.get_connection(conn, id) do
|
||||
{:ok, conn_struct} ->
|
||||
source_id = conn_struct.solar_system_source
|
||||
target_id = conn_struct.solar_system_target
|
||||
case Operations.delete_connection(conn, source_id, target_id) do
|
||||
:ok -> {:ok, conn_struct}
|
||||
error -> error
|
||||
end
|
||||
_ -> {:error, :invalid_id}
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_by_systems(conn, src, tgt) do
|
||||
with {:ok, source} <- APIUtils.parse_int(src),
|
||||
{:ok, target} <- APIUtils.parse_int(tgt) do
|
||||
do_delete_by_systems(conn, source, target, src, tgt)
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
Logger.error("[delete_connection] Connection not found for source=#{inspect(src)}, target=#{inspect(tgt)}")
|
||||
{:error, :not_found}
|
||||
{:error, reason} ->
|
||||
Logger.error("[delete_connection] Error: #{inspect(reason)}")
|
||||
{:error, reason}
|
||||
error ->
|
||||
Logger.error("[delete_connection] Unexpected error: #{inspect(error)}")
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_delete_by_systems(conn, source, target, src, tgt) do
|
||||
map_id = conn.assigns.map_id
|
||||
case Operations.get_connection_by_systems(map_id, source, target) do
|
||||
{:ok, nil} ->
|
||||
Logger.error("[delete_connection] No connection found for source=#{inspect(source)}, target=#{inspect(target)}")
|
||||
try_reverse_delete(conn, source, target, src, tgt)
|
||||
{:ok, conn_struct} ->
|
||||
case Operations.delete_connection(conn, conn_struct.solar_system_source, conn_struct.solar_system_target) do
|
||||
:ok -> send_resp(conn, :no_content, "")
|
||||
error -> {:error, error}
|
||||
end
|
||||
{:error, _} ->
|
||||
try_reverse_delete(conn, source, target, src, tgt)
|
||||
end
|
||||
end
|
||||
|
||||
defp try_reverse_delete(conn, source, target, src, tgt) do
|
||||
map_id = conn.assigns.map_id
|
||||
case Operations.get_connection_by_systems(map_id, target, source) do
|
||||
{:ok, nil} ->
|
||||
Logger.error("[delete_connection] No connection found for source=#{inspect(target)}, target=#{inspect(source)}")
|
||||
{:error, :not_found}
|
||||
{:ok, conn_struct} ->
|
||||
case Operations.delete_connection(conn, conn_struct.solar_system_source, conn_struct.solar_system_target) do
|
||||
:ok -> send_resp(conn, :no_content, "")
|
||||
error -> {:error, error}
|
||||
end
|
||||
{:error, reason} ->
|
||||
Logger.error("[delete_connection] Connection not found for source=#{inspect(src)}, target=#{inspect(tgt)} (both orders)")
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
operation :update,
|
||||
summary: "Update Connection (by id or by source/target)",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
id: [in: :path, type: :string, required: false],
|
||||
solar_system_source: [in: :query, type: :integer, required: false],
|
||||
solar_system_target: [in: :query, type: :integer, required: false]
|
||||
],
|
||||
request_body: {"Connection update", "application/json", @connection_request_schema},
|
||||
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||
def update(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
|
||||
allowed_fields = ["mass_status", "ship_size_type", "locked", "custom_info", "type"]
|
||||
attrs =
|
||||
conn.body_params
|
||||
|> Map.take(allowed_fields)
|
||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||
|> Enum.into(%{})
|
||||
update_by_id(conn, map_id, id, attrs)
|
||||
end
|
||||
|
||||
def update(%{assigns: %{map_id: map_id}} = conn, %{"solar_system_source" => src, "solar_system_target" => tgt}) do
|
||||
allowed_fields = ["mass_status", "ship_size_type", "locked", "custom_info", "type"]
|
||||
attrs =
|
||||
conn.body_params
|
||||
|> Map.take(allowed_fields)
|
||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||
|> Enum.into(%{})
|
||||
update_by_systems(conn, map_id, src, tgt, attrs)
|
||||
end
|
||||
|
||||
# Private helpers for update/2
|
||||
|
||||
defp update_by_id(conn, _map_id, id, attrs) do
|
||||
case Operations.update_connection(conn, id, attrs) do
|
||||
{:ok, updated_conn} -> APIUtils.respond_data(conn, APIUtils.connection_to_json(updated_conn))
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
defp update_by_systems(conn, _map_id, src, tgt, attrs) do
|
||||
require Logger
|
||||
with {:ok, source} <- APIUtils.parse_int(src),
|
||||
{:ok, target} <- APIUtils.parse_int(tgt) do
|
||||
do_update_by_systems(conn, source, target, src, tgt, attrs)
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
Logger.error("[update_connection] Connection not found for source=#{inspect(src)}, target=#{inspect(tgt)}")
|
||||
{:error, :not_found}
|
||||
{:error, reason} ->
|
||||
Logger.error("[update_connection] Error: #{inspect(reason)}")
|
||||
{:error, reason}
|
||||
error ->
|
||||
Logger.error("[update_connection] Unexpected error: #{inspect(error)}")
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update_by_systems(conn, source, target, src, tgt, attrs) do
|
||||
map_id = conn.assigns.map_id
|
||||
case Operations.get_connection_by_systems(map_id, source, target) do
|
||||
{:ok, nil} ->
|
||||
Logger.error("[update_connection] No connection found for source=#{inspect(source)}, target=#{inspect(target)}")
|
||||
try_reverse_update(conn, source, target, src, tgt, attrs)
|
||||
{:ok, conn_struct} ->
|
||||
do_update_connection(conn, conn_struct.id, attrs)
|
||||
{:error, _} ->
|
||||
try_reverse_update(conn, source, target, src, tgt, attrs)
|
||||
end
|
||||
end
|
||||
|
||||
defp try_reverse_update(conn, source, target, src, tgt, attrs) do
|
||||
map_id = conn.assigns.map_id
|
||||
case Operations.get_connection_by_systems(map_id, target, source) do
|
||||
{:ok, nil} ->
|
||||
Logger.error("[update_connection] No connection found for source=#{inspect(target)}, target=#{inspect(source)}")
|
||||
{:error, :not_found}
|
||||
{:ok, conn_struct} ->
|
||||
do_update_connection(conn, conn_struct.id, attrs)
|
||||
{:error, reason} ->
|
||||
Logger.error("[update_connection] Connection not found for source=#{inspect(src)}, target=#{inspect(tgt)} (both orders)")
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update_connection(conn, id, attrs) do
|
||||
case Operations.update_connection(conn, id, attrs) do
|
||||
{:ok, updated_conn} -> APIUtils.respond_data(conn, APIUtils.connection_to_json(updated_conn))
|
||||
{:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]}} ->
|
||||
Logger.error("[update_connection] Ash update NotFound for id=#{id}")
|
||||
{:error, :not_found}
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated "Use GET /api/maps/:map_identifier/systems instead"
|
||||
operation :list_all_connections,
|
||||
summary: "List All Connections (Legacy)",
|
||||
description: "Legacy endpoint for listing connections. Use GET /api/maps/:map_identifier/connections instead. Requires exactly one of map_id or slug as a query parameter. If both are provided, a 400 Bad Request will be returned.",
|
||||
deprecated: true,
|
||||
parameters: [
|
||||
map_id: [
|
||||
in: :query,
|
||||
description: "Map identifier (UUID) - Exactly one of map_id or slug must be provided",
|
||||
type: :string,
|
||||
required: false
|
||||
],
|
||||
slug: [
|
||||
in: :query,
|
||||
description: "Map slug - Exactly one of map_id or slug must be provided",
|
||||
type: :string,
|
||||
required: false
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
ok: {
|
||||
"List of Map Connections",
|
||||
"application/json",
|
||||
@list_response_schema
|
||||
},
|
||||
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{type: :string}
|
||||
},
|
||||
required: ["error"],
|
||||
example: %{
|
||||
"error" => "Must provide exactly one of map_id or slug as a query parameter"
|
||||
}
|
||||
}},
|
||||
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{type: :string}
|
||||
},
|
||||
required: ["error"],
|
||||
example: %{
|
||||
"error" => "Map not found. Please provide a valid map_id or slug as a query parameter."
|
||||
}
|
||||
}}
|
||||
]
|
||||
def list_all_connections(%{assigns: %{map_id: map_id}} = conn, _params) do
|
||||
connections = Operations.list_connections(map_id)
|
||||
data = Enum.map(connections, &APIUtils.connection_to_json/1)
|
||||
APIUtils.respond_data(conn, data)
|
||||
end
|
||||
end
|
||||
536
lib/wanderer_app_web/controllers/map_system_api_controller.ex
Normal file
536
lib/wanderer_app_web/controllers/map_system_api_controller.ex
Normal file
@@ -0,0 +1,536 @@
|
||||
# lib/wanderer_app_web/controllers/map_system_api_controller.ex
|
||||
defmodule WandererAppWeb.MapSystemAPIController do
|
||||
@moduledoc """
|
||||
API controller for managing map systems and their associated connections.
|
||||
Provides CRUD operations and batch upsert for systems and connections.
|
||||
"""
|
||||
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias OpenApiSpex.Schema
|
||||
alias WandererApp.Map.Operations
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
alias WandererAppWeb.Schemas.{ApiSchemas, ResponseSchemas}
|
||||
|
||||
action_fallback WandererAppWeb.FallbackController
|
||||
|
||||
# -- JSON Schemas --
|
||||
@map_system_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, description: "Map system UUID"},
|
||||
map_id: %Schema{type: :string, description: "Map UUID"},
|
||||
solar_system_id: %Schema{type: :integer, description: "EVE solar system ID"},
|
||||
solar_system_name: %Schema{type: :string, description: "EVE solar system name"},
|
||||
region_name: %Schema{type: :string, description: "EVE region name"},
|
||||
position_x: %Schema{type: :integer, description: "X coordinate"},
|
||||
position_y: %Schema{type: :integer, description: "Y coordinate"},
|
||||
status: %Schema{
|
||||
type: :integer,
|
||||
description: "System status (0: unknown, 1: friendly, 2: warning, 3: targetPrimary, 4: targetSecondary, 5: dangerousPrimary, 6: dangerousSecondary, 7: lookingFor, 8: home)"
|
||||
},
|
||||
visible: %Schema{type: :boolean, description: "Visibility flag"},
|
||||
description: %Schema{type: :string, nullable: true, description: "Custom description"},
|
||||
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
|
||||
locked: %Schema{type: :boolean, description: "Lock flag"},
|
||||
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
|
||||
labels: %Schema{type: :string, description: "Comma-separated list of labels"}
|
||||
},
|
||||
required: ~w(id map_id solar_system_id)a
|
||||
}
|
||||
|
||||
@system_request_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_id: %Schema{type: :integer, description: "EVE solar system ID"},
|
||||
solar_system_name: %Schema{type: :string, description: "EVE solar system name"},
|
||||
position_x: %Schema{type: :integer, description: "X coordinate"},
|
||||
position_y: %Schema{type: :integer, description: "Y coordinate"},
|
||||
status: %Schema{
|
||||
type: :integer,
|
||||
description: "System status (0: unknown, 1: friendly, 2: warning, 3: targetPrimary, 4: targetSecondary, 5: dangerousPrimary, 6: dangerousSecondary, 7: lookingFor, 8: home)"
|
||||
},
|
||||
visible: %Schema{type: :boolean, description: "Visibility flag"},
|
||||
description: %Schema{type: :string, nullable: true, description: "Custom description"},
|
||||
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
|
||||
locked: %Schema{type: :boolean, description: "Lock flag"},
|
||||
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
|
||||
labels: %Schema{type: :string, description: "Comma-separated list of labels"}
|
||||
},
|
||||
required: ~w(solar_system_id)a,
|
||||
example: %{
|
||||
solar_system_id: 30_000_142,
|
||||
solar_system_name: "Jita",
|
||||
position_x: 100,
|
||||
position_y: 200,
|
||||
visible: true,
|
||||
labels: "market,hub"
|
||||
}
|
||||
}
|
||||
|
||||
@system_update_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_name: %Schema{type: :string, description: "EVE solar system name", nullable: true},
|
||||
position_x: %Schema{type: :integer, description: "X coordinate", nullable: true},
|
||||
position_y: %Schema{type: :integer, description: "Y coordinate", nullable: true},
|
||||
status: %Schema{
|
||||
type: :integer,
|
||||
description: "System status (0: unknown, 1: friendly, 2: warning, 3: targetPrimary, 4: targetSecondary, 5: dangerousPrimary, 6: dangerousSecondary, 7: lookingFor, 8: home)",
|
||||
nullable: true
|
||||
},
|
||||
visible: %Schema{type: :boolean, description: "Visibility flag", nullable: true},
|
||||
description: %Schema{type: :string, nullable: true, description: "Custom description"},
|
||||
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
|
||||
locked: %Schema{type: :boolean, description: "Lock flag", nullable: true},
|
||||
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
|
||||
labels: %Schema{type: :string, description: "Comma-separated list of labels"}
|
||||
},
|
||||
example: %{
|
||||
solar_system_name: "Jita",
|
||||
position_x: 101,
|
||||
position_y: 202,
|
||||
visible: false,
|
||||
status: 0,
|
||||
tag: "HQ",
|
||||
locked: true,
|
||||
labels: "market,hub"
|
||||
}
|
||||
}
|
||||
|
||||
@map_connection_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, description: "Connection UUID"},
|
||||
map_id: %Schema{type: :string, description: "Map UUID"},
|
||||
solar_system_source: %Schema{type: :integer},
|
||||
solar_system_target: %Schema{type: :integer},
|
||||
type: %Schema{type: :integer},
|
||||
mass_status: %Schema{type: :integer, nullable: true},
|
||||
time_status: %Schema{type: :integer, nullable: true},
|
||||
ship_size_type: %Schema{type: :integer, nullable: true},
|
||||
locked: %Schema{type: :boolean},
|
||||
custom_info: %Schema{type: :string, nullable: true},
|
||||
wormhole_type: %Schema{type: :string, nullable: true}
|
||||
},
|
||||
required: ~w(id map_id solar_system_source solar_system_target)a
|
||||
}
|
||||
|
||||
@list_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
systems: %Schema{type: :array, items: @map_system_schema},
|
||||
connections: %Schema{type: :array, items: @map_connection_schema}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: %{
|
||||
systems: [
|
||||
%{
|
||||
id: "sys-uuid-1",
|
||||
map_id: "map-uuid-1",
|
||||
solar_system_id: 30_000_142,
|
||||
solar_system_name: "Jita",
|
||||
region_name: "The Forge",
|
||||
position_x: 100.5,
|
||||
position_y: 200.3,
|
||||
status: "active",
|
||||
visible: true,
|
||||
description: "Trade hub",
|
||||
tag: "HQ",
|
||||
locked: false,
|
||||
temporary_name: nil,
|
||||
labels: ["market", "hub"]
|
||||
}
|
||||
],
|
||||
connections: [
|
||||
%{
|
||||
id: "conn-uuid-1",
|
||||
map_id: "map-uuid-1",
|
||||
solar_system_source: 30_000_142,
|
||||
solar_system_target: 30_000_144,
|
||||
type: 0,
|
||||
mass_status: 1,
|
||||
time_status: 2,
|
||||
ship_size_type: 1,
|
||||
locked: false,
|
||||
custom_info: "Frigate only",
|
||||
wormhole_type: "C2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@detail_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: @map_system_schema
|
||||
},
|
||||
example: %{
|
||||
data: %{
|
||||
id: "sys-uuid-1",
|
||||
map_id: "map-uuid-1",
|
||||
solar_system_id: 30_000_142,
|
||||
solar_system_name: "Jita",
|
||||
region_name: "The Forge",
|
||||
position_x: 100.5,
|
||||
position_y: 200.3,
|
||||
status: "active",
|
||||
visible: true,
|
||||
description: "Trade hub",
|
||||
tag: "HQ",
|
||||
locked: false,
|
||||
temporary_name: nil,
|
||||
labels: ["market", "hub"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@delete_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{deleted: %Schema{type: :boolean, description: "Deleted flag"}},
|
||||
required: ["deleted"],
|
||||
example: %{deleted: true}
|
||||
}
|
||||
|
||||
@batch_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
systems: %Schema{
|
||||
type: :object,
|
||||
properties: %{created: %Schema{type: :integer}, updated: %Schema{type: :integer}},
|
||||
required: ~w(created updated)a
|
||||
},
|
||||
connections: %Schema{
|
||||
type: :object,
|
||||
properties: %{created: %Schema{type: :integer}, updated: %Schema{type: :integer}, deleted: %Schema{type: :integer}},
|
||||
required: ~w(created updated deleted)a
|
||||
}
|
||||
},
|
||||
required: ~w(systems connections)a
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: %{
|
||||
systems: %{created: 2, updated: 1},
|
||||
connections: %{created: 1, updated: 0, deleted: 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@batch_delete_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
system_ids: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :integer},
|
||||
description: "IDs to delete"
|
||||
},
|
||||
connection_ids: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
description: "Connection UUIDs to delete",
|
||||
nullable: true
|
||||
}
|
||||
},
|
||||
required: ["system_ids"],
|
||||
example: %{
|
||||
system_ids: [30_000_142, 30_000_143],
|
||||
connection_ids: ["conn-uuid-1", "conn-uuid-2"]
|
||||
}
|
||||
}
|
||||
|
||||
@batch_delete_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{deleted_count: %Schema{type: :integer, description: "Deleted count"}},
|
||||
required: ["deleted_count"],
|
||||
example: %{deleted_count: 2}
|
||||
}
|
||||
|
||||
@batch_request_schema ApiSchemas.data_wrapper(%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
systems: %Schema{type: :array, items: @system_request_schema},
|
||||
connections: %Schema{type: :array, items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_source: %Schema{type: :integer, description: "Source system ID"},
|
||||
solar_system_target: %Schema{type: :integer, description: "Target system ID"},
|
||||
type: %Schema{type: :integer, description: "Connection type (default 0)"},
|
||||
mass_status: %Schema{type: :integer, description: "Mass status (0-3)", nullable: true},
|
||||
time_status: %Schema{type: :integer, description: "Time decay status (0-3)", nullable: true},
|
||||
ship_size_type: %Schema{type: :integer, description: "Ship size limit (0-3)", nullable: true},
|
||||
locked: %Schema{type: :boolean, description: "Lock flag", nullable: true},
|
||||
custom_info: %Schema{type: :string, description: "Optional metadata", nullable: true}
|
||||
},
|
||||
required: ~w(solar_system_source solar_system_target)a
|
||||
}}
|
||||
},
|
||||
example: %{
|
||||
systems: [
|
||||
%{
|
||||
solar_system_id: 30_000_142,
|
||||
solar_system_name: "Jita",
|
||||
position_x: 100.5,
|
||||
position_y: 200.3,
|
||||
visible: true
|
||||
}
|
||||
],
|
||||
connections: [
|
||||
%{
|
||||
solar_system_source: 30_000_142,
|
||||
solar_system_target: 30_000_144,
|
||||
type: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
# -- Actions --
|
||||
|
||||
operation :index,
|
||||
summary: "List Map Systems and Connections",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
ok: {
|
||||
"List Map Systems and Connections",
|
||||
"application/json",
|
||||
@list_response_schema
|
||||
}
|
||||
]
|
||||
def index(%{assigns: %{map_id: map_id}} = conn, _params) do
|
||||
systems = Operations.list_systems(map_id) |> Enum.map(&APIUtils.map_system_to_json/1)
|
||||
connections = Operations.list_connections(map_id) |> Enum.map(&APIUtils.connection_to_json/1)
|
||||
APIUtils.respond_data(conn, %{systems: systems, connections: connections})
|
||||
end
|
||||
|
||||
operation :show,
|
||||
summary: "Show Map System",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
id: [
|
||||
in: :path,
|
||||
description: "System ID",
|
||||
type: :string,
|
||||
required: true
|
||||
]
|
||||
],
|
||||
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||
def show(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
|
||||
with {:ok, system_id} <- APIUtils.parse_int(id),
|
||||
{:ok, system} <- Operations.get_system(map_id, system_id) do
|
||||
APIUtils.respond_data(conn, APIUtils.map_system_to_json(system))
|
||||
end
|
||||
end
|
||||
|
||||
operation :create,
|
||||
summary: "Upsert Systems and Connections (batch or single)",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
]
|
||||
],
|
||||
request_body: {"Systems+Connections upsert", "application/json", @batch_request_schema},
|
||||
responses: ResponseSchemas.standard_responses(@batch_response_schema)
|
||||
def create(conn, params) do
|
||||
systems = Map.get(params, "systems", [])
|
||||
connections = Map.get(params, "connections", [])
|
||||
case Operations.upsert_systems_and_connections(conn, systems, connections) do
|
||||
{:ok, result} ->
|
||||
APIUtils.respond_data(conn, result)
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
operation :update,
|
||||
summary: "Update System",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
id: [
|
||||
in: :path,
|
||||
description: "System ID",
|
||||
type: :string,
|
||||
required: true
|
||||
]
|
||||
],
|
||||
request_body: {"System update request", "application/json", @system_update_schema},
|
||||
responses: ResponseSchemas.update_responses(@detail_response_schema)
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
with {:ok, sid} <- APIUtils.parse_int(id),
|
||||
{:ok, attrs} <- APIUtils.extract_update_params(params),
|
||||
update_attrs = Map.put(attrs, "solar_system_id", sid),
|
||||
{:ok, system} <- Operations.update_system(conn, sid, update_attrs) do
|
||||
APIUtils.respond_data(conn, APIUtils.map_system_to_json(system))
|
||||
end
|
||||
end
|
||||
|
||||
operation :delete,
|
||||
summary: "Batch Delete Systems and Connections",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
]
|
||||
],
|
||||
request_body: {"Batch delete", "application/json", @batch_delete_schema},
|
||||
responses: ResponseSchemas.standard_responses(@batch_delete_response_schema)
|
||||
def delete(conn, params) do
|
||||
system_ids = Map.get(params, "system_ids", [])
|
||||
connection_ids = Map.get(params, "connection_ids", [])
|
||||
|
||||
deleted_systems = Enum.map(system_ids, &delete_system_id(conn, &1))
|
||||
deleted_connections = Enum.map(connection_ids, &delete_connection_id(conn, &1))
|
||||
|
||||
systems_deleted = Enum.count(deleted_systems, &match?({:ok, _}, &1))
|
||||
connections_deleted = Enum.count(deleted_connections, &match?({:ok, _}, &1))
|
||||
deleted_count = systems_deleted + connections_deleted
|
||||
|
||||
APIUtils.respond_data(conn, %{deleted_count: deleted_count})
|
||||
end
|
||||
|
||||
defp delete_system_id(conn, id) do
|
||||
case APIUtils.parse_int(id) do
|
||||
{:ok, sid} -> Operations.delete_system(conn, sid)
|
||||
_ -> {:error, :invalid_id}
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_connection_id(conn, id) do
|
||||
case Operations.get_connection(conn, id) do
|
||||
{:ok, conn_struct} ->
|
||||
source_id = conn_struct.solar_system_source
|
||||
target_id = conn_struct.solar_system_target
|
||||
case Operations.delete_connection(conn, source_id, target_id) do
|
||||
:ok -> {:ok, conn_struct}
|
||||
error -> error
|
||||
end
|
||||
_ -> {:error, :invalid_id}
|
||||
end
|
||||
end
|
||||
|
||||
operation :delete_single,
|
||||
summary: "Delete a single Map System",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
id: [
|
||||
in: :path,
|
||||
description: "System ID",
|
||||
type: :string,
|
||||
required: true
|
||||
]
|
||||
],
|
||||
responses: ResponseSchemas.standard_responses(@delete_response_schema)
|
||||
def delete_single(conn, %{"id" => id}) do
|
||||
with {:ok, sid} <- APIUtils.parse_int(id),
|
||||
{:ok, _} <- Operations.delete_system(conn, sid) do
|
||||
APIUtils.respond_data(conn, %{deleted: true})
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> APIUtils.respond_data(%{deleted: false, error: "System not found"})
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> APIUtils.respond_data(%{deleted: false, error: "Failed to delete system", reason: reason})
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> APIUtils.respond_data(%{deleted: false, error: "Invalid system ID format"})
|
||||
end
|
||||
end
|
||||
|
||||
# -- Legacy endpoints --
|
||||
|
||||
operation :list_systems,
|
||||
summary: "List Map Systems (Legacy)",
|
||||
deprecated: true,
|
||||
description: "Deprecated, use GET /api/maps/:map_identifier/systems instead",
|
||||
parameters: [
|
||||
map_id: [
|
||||
in: :query,
|
||||
description: "Map identifier (UUID) - Either map_id or slug must be provided, but not both",
|
||||
type: :string,
|
||||
required: false,
|
||||
],
|
||||
slug: [
|
||||
in: :query,
|
||||
description: "Map slug - Either map_id or slug must be provided, but not both",
|
||||
type: :string,
|
||||
required: false,
|
||||
]
|
||||
],
|
||||
responses: ResponseSchemas.standard_responses(@list_response_schema)
|
||||
defdelegate list_systems(conn, params), to: __MODULE__, as: :index
|
||||
|
||||
operation :show_system,
|
||||
summary: "Show Map System (Legacy)",
|
||||
deprecated: true,
|
||||
description: "Deprecated, use GET /api/maps/:map_identifier/systems/:id instead",
|
||||
parameters: [
|
||||
map_id: [
|
||||
in: :query,
|
||||
description: "Map identifier (UUID) - Either map_id or slug must be provided, but not both",
|
||||
type: :string,
|
||||
required: false,
|
||||
],
|
||||
slug: [
|
||||
in: :query,
|
||||
description: "Map slug - Either map_id or slug must be provided, but not both",
|
||||
type: :string,
|
||||
required: false,
|
||||
],
|
||||
id: [
|
||||
in: :query,
|
||||
description: "System ID",
|
||||
type: :string,
|
||||
required: true
|
||||
]
|
||||
],
|
||||
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||
defdelegate show_system(conn, params), to: __MODULE__, as: :show
|
||||
|
||||
end
|
||||
@@ -0,0 +1,169 @@
|
||||
defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias WandererApp.Api.MapSystemSignature
|
||||
alias WandererApp.Map.Operations, as: MapOperations
|
||||
|
||||
@moduledoc """
|
||||
API controller for managing map system signatures.
|
||||
"""
|
||||
|
||||
# Inlined OpenAPI schema for a map system signature
|
||||
@signature_schema %OpenApiSpex.Schema{
|
||||
title: "MapSystemSignature",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %OpenApiSpex.Schema{type: :string, format: :uuid},
|
||||
system_id: %OpenApiSpex.Schema{type: :string, format: :uuid},
|
||||
eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
character_eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
name: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
description: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
type: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
linked_system_id: %OpenApiSpex.Schema{type: :integer, nullable: true},
|
||||
kind: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
group: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
custom_info: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
updated: %OpenApiSpex.Schema{type: :integer, nullable: true},
|
||||
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
|
||||
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
|
||||
},
|
||||
required: [
|
||||
:id, :system_id, :eve_id, :character_eve_id
|
||||
],
|
||||
example: %{
|
||||
id: "sig-uuid-1",
|
||||
system_id: "sys-uuid-1",
|
||||
eve_id: "ABC-123",
|
||||
character_eve_id: "123456789",
|
||||
name: "Wormhole K162",
|
||||
description: "Leads to unknown space",
|
||||
type: "Wormhole",
|
||||
linked_system_id: 30000144,
|
||||
kind: "cosmic_signature",
|
||||
group: "wormhole",
|
||||
custom_info: "Fresh",
|
||||
updated: 1,
|
||||
inserted_at: "2025-04-30T10:00:00Z",
|
||||
updated_at: "2025-04-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
|
||||
@doc """
|
||||
List all signatures for a map.
|
||||
"""
|
||||
operation :index,
|
||||
summary: "List all signatures for a map",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true]
|
||||
],
|
||||
responses: [ok: {"List of signatures", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %OpenApiSpex.Schema{
|
||||
type: :array,
|
||||
items: @signature_schema
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: [@signature_schema.example]
|
||||
}
|
||||
}}]
|
||||
def index(conn, _params) do
|
||||
map_id = conn.assigns.map_id
|
||||
signatures = MapOperations.list_signatures(map_id)
|
||||
json(conn, %{data: signatures})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Show a single signature by ID.
|
||||
"""
|
||||
operation :show,
|
||||
summary: "Show a single signature by ID",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true],
|
||||
id: [in: :path, description: "Signature UUID", type: :string, required: true]
|
||||
],
|
||||
responses: [ok: {"Signature", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @signature_schema},
|
||||
example: %{data: @signature_schema.example}
|
||||
}}]
|
||||
def show(conn, %{"id" => id}) do
|
||||
map_id = conn.assigns.map_id
|
||||
case MapSystemSignature.by_id(id) do
|
||||
{:ok, signature} ->
|
||||
case WandererApp.Api.MapSystem.by_id(signature.system_id) do
|
||||
{:ok, system} when system.map_id == map_id ->
|
||||
json(conn, %{data: signature})
|
||||
_ ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Signature not found"})
|
||||
end
|
||||
_ -> conn |> put_status(:not_found) |> json(%{error: "Signature not found"})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a new signature.
|
||||
"""
|
||||
operation :create,
|
||||
summary: "Create a new signature",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true]
|
||||
],
|
||||
request_body: {"Signature", "application/json", @signature_schema},
|
||||
responses: [created: {"Created signature", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @signature_schema},
|
||||
example: %{data: @signature_schema.example}
|
||||
}}]
|
||||
def create(conn, params) do
|
||||
case MapOperations.create_signature(conn, params) do
|
||||
{:ok, sig} -> conn |> put_status(:created) |> json(%{data: sig})
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update a signature by ID.
|
||||
"""
|
||||
operation :update,
|
||||
summary: "Update a signature by ID",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true],
|
||||
id: [in: :path, description: "Signature UUID", type: :string, required: true]
|
||||
],
|
||||
request_body: {"Signature update", "application/json", @signature_schema},
|
||||
responses: [ok: {"Updated signature", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @signature_schema},
|
||||
example: %{data: @signature_schema.example}
|
||||
}}]
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
case MapOperations.update_signature(conn, id, params) do
|
||||
{:ok, sig} -> json(conn, %{data: sig})
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Delete a signature by ID.
|
||||
"""
|
||||
operation :delete,
|
||||
summary: "Delete a signature by ID",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true],
|
||||
id: [in: :path, description: "Signature UUID", type: :string, required: true]
|
||||
],
|
||||
responses: [no_content: {"Deleted", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
example: %{}
|
||||
}}]
|
||||
def delete(conn, %{"id" => id}) do
|
||||
case MapOperations.delete_signature(conn, id) do
|
||||
:ok -> send_resp(conn, :no_content, "")
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,200 @@
|
||||
defmodule WandererAppWeb.MapSystemStructureAPIController do
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias WandererApp.Api.MapSystemStructure
|
||||
alias OpenApiSpex.Schema
|
||||
alias WandererApp.Map.Operations, as: MapOperations
|
||||
|
||||
@moduledoc """
|
||||
API controller for managing map system structures.
|
||||
"""
|
||||
|
||||
# Inlined OpenAPI schema for a map system structure
|
||||
@structure_schema %Schema{
|
||||
title: "MapSystemStructure",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, format: :uuid},
|
||||
system_id: %Schema{type: :string, format: :uuid},
|
||||
solar_system_name: %Schema{type: :string},
|
||||
solar_system_id: %Schema{type: :integer},
|
||||
structure_type_id: %Schema{type: :string},
|
||||
structure_type: %Schema{type: :string},
|
||||
character_eve_id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
notes: %Schema{type: :string, nullable: true},
|
||||
owner_name: %Schema{type: :string, nullable: true},
|
||||
owner_ticker: %Schema{type: :string, nullable: true},
|
||||
owner_id: %Schema{type: :string, nullable: true},
|
||||
status: %Schema{type: :string, nullable: true},
|
||||
end_time: %Schema{type: :string, format: :date_time, nullable: true},
|
||||
inserted_at: %Schema{type: :string, format: :date_time},
|
||||
updated_at: %Schema{type: :string, format: :date_time}
|
||||
},
|
||||
required: [
|
||||
:id, :system_id, :solar_system_name, :solar_system_id, :structure_type_id, :structure_type, :character_eve_id, :name
|
||||
],
|
||||
example: %{
|
||||
id: "struct-uuid-1",
|
||||
system_id: "sys-uuid-1",
|
||||
solar_system_name: "Jita",
|
||||
solar_system_id: 30000142,
|
||||
structure_type_id: "35832",
|
||||
structure_type: "Astrahus",
|
||||
character_eve_id: "123456789",
|
||||
name: "Jita Trade Hub",
|
||||
notes: "Main market structure",
|
||||
owner_name: "Wanderer Corp",
|
||||
owner_ticker: "WANDR",
|
||||
owner_id: "corp-uuid-1",
|
||||
status: "anchoring",
|
||||
end_time: "2025-05-01T12:00:00Z",
|
||||
inserted_at: "2025-04-30T10:00:00Z",
|
||||
updated_at: "2025-04-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
|
||||
@doc """
|
||||
List all structures for a map.
|
||||
"""
|
||||
operation :index,
|
||||
summary: "List all structures for a map",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true]
|
||||
],
|
||||
responses: [ok: {"List of structures", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %OpenApiSpex.Schema{
|
||||
type: :array,
|
||||
items: @structure_schema
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: [@structure_schema.example]
|
||||
}
|
||||
}}]
|
||||
def index(conn, _params) do
|
||||
map_id = conn.assigns.map_id
|
||||
structures = MapOperations.list_structures(map_id)
|
||||
json(conn, %{data: structures})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Show a single structure by ID.
|
||||
"""
|
||||
operation :show,
|
||||
summary: "Show a single structure by ID",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true],
|
||||
id: [in: :path, description: "Structure UUID", type: :string, required: true]
|
||||
],
|
||||
responses: [ok: {"Structure", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @structure_schema},
|
||||
example: %{data: @structure_schema.example}
|
||||
}}]
|
||||
def show(conn, %{"id" => id}) do
|
||||
map_id = conn.assigns.map_id
|
||||
case MapSystemStructure.by_id(id) do
|
||||
{:ok, structure} ->
|
||||
case WandererApp.Api.MapSystem.by_id(structure.system_id) do
|
||||
{:ok, system} when system.map_id == map_id ->
|
||||
json(conn, %{data: structure})
|
||||
_ ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Structure not found"})
|
||||
end
|
||||
_ -> conn |> put_status(:not_found) |> json(%{error: "Structure not found"})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a new structure.
|
||||
"""
|
||||
operation :create,
|
||||
summary: "Create a new structure",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true]
|
||||
],
|
||||
request_body: {"Structure", "application/json", @structure_schema},
|
||||
responses: [created: {"Created structure", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @structure_schema},
|
||||
example: %{data: @structure_schema.example}
|
||||
}}]
|
||||
def create(conn, params) do
|
||||
case MapOperations.create_structure(conn, params) do
|
||||
{:ok, struct} -> conn |> put_status(:created) |> json(%{data: struct})
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update a structure by ID.
|
||||
"""
|
||||
operation :update,
|
||||
summary: "Update a structure by ID",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true],
|
||||
id: [in: :path, description: "Structure UUID", type: :string, required: true]
|
||||
],
|
||||
request_body: {"Structure update", "application/json", @structure_schema},
|
||||
responses: [ok: {"Updated structure", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @structure_schema},
|
||||
example: %{data: @structure_schema.example}
|
||||
}}]
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
case MapOperations.update_structure(conn, id, params) do
|
||||
{:ok, struct} -> json(conn, %{data: struct})
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Delete a structure by ID.
|
||||
"""
|
||||
operation :delete,
|
||||
summary: "Delete a structure by ID",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true],
|
||||
id: [in: :path, description: "Structure UUID", type: :string, required: true]
|
||||
],
|
||||
responses: [no_content: {"Deleted", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
example: %{}
|
||||
}}]
|
||||
def delete(conn, %{"id" => id}) do
|
||||
case MapOperations.delete_structure(conn, id) do
|
||||
:ok -> send_resp(conn, :no_content, "")
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get structure timers for a map.
|
||||
"""
|
||||
operation :structure_timers,
|
||||
summary: "Get structure timers for a map",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true]
|
||||
],
|
||||
responses: [ok: {"Structure timers", "application/json", %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{
|
||||
type: :array,
|
||||
items: @structure_schema
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: [@structure_schema.example]
|
||||
}
|
||||
}}]
|
||||
def structure_timers(conn, _params) do
|
||||
map_id = conn.assigns.map_id
|
||||
structures = MapOperations.list_structures(map_id)
|
||||
json(conn, %{data: structures})
|
||||
end
|
||||
end
|
||||
21
lib/wanderer_app_web/controllers/plugs/assign_map_owner.ex
Normal file
21
lib/wanderer_app_web/controllers/plugs/assign_map_owner.ex
Normal file
@@ -0,0 +1,21 @@
|
||||
defmodule WandererAppWeb.Plugs.AssignMapOwner do
|
||||
import Plug.Conn
|
||||
|
||||
alias WandererApp.Map.Operations
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, _opts) do
|
||||
map_id = conn.assigns[:map_id]
|
||||
case Operations.get_owner_character_id(map_id) do
|
||||
{:ok, %{id: char_id, user_id: user_id}} ->
|
||||
conn
|
||||
|> assign(:owner_character_id, char_id)
|
||||
|> assign(:owner_user_id, user_id)
|
||||
_ ->
|
||||
conn
|
||||
|> assign(:owner_character_id, nil)
|
||||
|> assign(:owner_user_id, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,52 +1,116 @@
|
||||
defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
||||
@moduledoc """
|
||||
A plug that checks the "Authorization: Bearer <token>" header
|
||||
against the map's stored public_api_key. Halts with 401 if invalid.
|
||||
"""
|
||||
@behaviour Plug
|
||||
|
||||
import Plug.Conn
|
||||
alias WandererAppWeb.UtilAPIController, as: Util
|
||||
alias Plug.Crypto
|
||||
alias WandererApp.Api.Map, as: ApiMap
|
||||
alias WandererAppWeb.Schemas.ResponseSchemas, as: R
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
def init(opts), do: opts
|
||||
|
||||
@impl true
|
||||
def call(conn, _opts) do
|
||||
header = get_req_header(conn, "authorization") |> List.first()
|
||||
with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
|
||||
{:ok, map_id} <- fetch_map_id(conn),
|
||||
{:ok, map} <- ApiMap.by_id(map_id),
|
||||
true <- is_binary(map.public_api_key) &&
|
||||
Crypto.secure_compare(map.public_api_key, token)
|
||||
do
|
||||
conn
|
||||
|> assign(:map, map)
|
||||
|> assign(:map_id, map.id)
|
||||
else
|
||||
[] ->
|
||||
Logger.warning("Missing or invalid 'Bearer' token")
|
||||
conn |> respond(401, "Missing or invalid 'Bearer' token") |> halt()
|
||||
|
||||
case header do
|
||||
"Bearer " <> incoming_token ->
|
||||
case fetch_map(conn.query_params) do
|
||||
{:ok, map} ->
|
||||
if map.public_api_key == incoming_token do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(401, Jason.encode!(%{error: "Unauthorized (invalid token for map)"}))
|
||||
|> halt()
|
||||
end
|
||||
{:error, :bad_request, msg} ->
|
||||
Logger.warning("Bad request: #{msg}")
|
||||
conn |> respond(400, msg) |> halt()
|
||||
|
||||
{:error, _reason} ->
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(404, Jason.encode!(%{error: "Map not found"}))
|
||||
|> halt()
|
||||
end
|
||||
{:error, :not_found, msg} ->
|
||||
Logger.warning("Not found: #{msg}")
|
||||
conn |> respond(404, msg) |> halt()
|
||||
|
||||
_ ->
|
||||
{:error, _} ->
|
||||
Logger.warning("Map identifier required")
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(401, Jason.encode!(%{error: "Missing or invalid 'Bearer' token"}))
|
||||
|> respond(400, "Map identifier required. Provide `map_identifier` in the path or `map_id`/`slug` in query.")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map(query_params) do
|
||||
case Util.fetch_map_id(query_params) do
|
||||
{:ok, map_id} ->
|
||||
WandererApp.Api.Map.by_id(map_id)
|
||||
false ->
|
||||
Logger.warning("Unauthorized: invalid token for map #{inspect(conn.params["map_identifier"])}")
|
||||
conn |> respond(401, "Unauthorized (invalid token for map)") |> halt()
|
||||
|
||||
error ->
|
||||
error
|
||||
Logger.error("Unexpected error: #{inspect(error)}")
|
||||
conn |> respond(500, "Unexpected error") |> halt()
|
||||
end
|
||||
end
|
||||
|
||||
# Try unified path param first, then fall back to legacy query params
|
||||
defp fetch_map_id(%Plug.Conn{params: %{"map_identifier" => id}}) when is_binary(id) and id != "" do
|
||||
resolve_identifier(id)
|
||||
end
|
||||
defp fetch_map_id(conn), do: legacy_fetch(conn)
|
||||
|
||||
# Try ID lookup first, then slug lookup
|
||||
defp resolve_identifier(id) do
|
||||
case ApiMap.by_id(id) do
|
||||
{:ok, %{id: map_id}} ->
|
||||
{:ok, map_id}
|
||||
|
||||
_ ->
|
||||
case ApiMap.get_map_by_slug(id) do
|
||||
{:ok, %{id: map_id}} ->
|
||||
{:ok, map_id}
|
||||
|
||||
_ ->
|
||||
{:error, :not_found, "Map not found for identifier: #{id}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Legacy: check assigns, then params["map_id"], then params["slug"]
|
||||
defp legacy_fetch(conn) do
|
||||
map_id_from_assign = conn.assigns[:map_id]
|
||||
map_id_param = conn.params["map_id"]
|
||||
slug_param = conn.params["slug"]
|
||||
|
||||
cond do
|
||||
is_binary(map_id_from_assign) and map_id_from_assign != "" ->
|
||||
{:ok, map_id_from_assign}
|
||||
|
||||
is_binary(map_id_param) and map_id_param != "" ->
|
||||
{:ok, map_id_param}
|
||||
|
||||
is_binary(slug_param) and slug_param != "" ->
|
||||
case ApiMap.get_map_by_slug(slug_param) do
|
||||
{:ok, %{id: map_id}} -> {:ok, map_id}
|
||||
_ -> {:error, :not_found, "Map not found for slug: #{slug_param}"}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:error, :bad_request,
|
||||
"Map identifier required. Provide `map_identifier` in the path or `map_id`/`slug` in query."}
|
||||
end
|
||||
end
|
||||
|
||||
# Pick the right shared schema and send JSON
|
||||
defp respond(conn, status, msg) do
|
||||
{_desc, content_type, _schema} =
|
||||
case status do
|
||||
400 -> R.bad_request(msg)
|
||||
401 -> R.unauthorized(msg)
|
||||
404 -> R.not_found(msg)
|
||||
500 -> R.internal_server_error(msg)
|
||||
_ -> R.internal_server_error("Unexpected error")
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_resp_content_type(content_type)
|
||||
|> send_resp(status, Jason.encode!(%{error: msg}))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,11 +5,13 @@ defmodule WandererAppWeb.Plugs.CheckMapSubscription do
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
require Logger
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, _opts) do
|
||||
case fetch_map_id(conn.query_params) do
|
||||
# First check if map_id is already in conn.assigns (from CheckMapApiKey)
|
||||
case get_map_id_from_assigns_or_params(conn) do
|
||||
{:ok, map_id} ->
|
||||
{:ok, is_subscription_active} = map_id |> WandererApp.Map.is_subscription_active?()
|
||||
|
||||
@@ -28,6 +30,17 @@ defmodule WandererAppWeb.Plugs.CheckMapSubscription do
|
||||
end
|
||||
end
|
||||
|
||||
# First try to get map_id from conn.assigns
|
||||
defp get_map_id_from_assigns_or_params(conn) do
|
||||
if Map.has_key?(conn.assigns, :map_id) do
|
||||
Logger.debug("Found map_id in conn.assigns: #{conn.assigns.map_id}")
|
||||
{:ok, conn.assigns.map_id}
|
||||
else
|
||||
# Fall back to query params if not in assigns
|
||||
fetch_map_id(conn.query_params)
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
|
||||
{:ok, mid}
|
||||
end
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
defmodule WandererAppWeb.UtilAPIController do
|
||||
@moduledoc """
|
||||
Utility functions for parameter handling, fetch helpers, etc.
|
||||
"""
|
||||
|
||||
alias WandererApp.Api
|
||||
|
||||
def fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
|
||||
{:ok, mid}
|
||||
end
|
||||
|
||||
def fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
|
||||
case Api.Map.get_map_by_slug(slug) do
|
||||
{:ok, map} ->
|
||||
{:ok, map.id}
|
||||
|
||||
{:error, _reason} ->
|
||||
{:error, "No map found for slug=#{slug}"}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_map_id(_),
|
||||
do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
|
||||
|
||||
# Require a given param to be present and non-empty
|
||||
def require_param(params, key) do
|
||||
case params[key] do
|
||||
nil -> {:error, "Missing required param: #{key}"}
|
||||
"" -> {:error, "Param #{key} cannot be empty"}
|
||||
val -> {:ok, val}
|
||||
end
|
||||
end
|
||||
|
||||
# Parse a string into an integer
|
||||
def parse_int(str) do
|
||||
case Integer.parse(str) do
|
||||
{num, ""} -> {:ok, num}
|
||||
_ -> {:error, "Invalid integer for param id=#{str}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
301
lib/wanderer_app_web/helpers/api_utils.ex
Normal file
301
lib/wanderer_app_web/helpers/api_utils.ex
Normal file
@@ -0,0 +1,301 @@
|
||||
defmodule WandererAppWeb.Helpers.APIUtils do
|
||||
@moduledoc """
|
||||
Unified helper module for API operations:
|
||||
- Parameter parsing and validation
|
||||
- Map ID resolution
|
||||
- Standardized responses
|
||||
- JSON serialization
|
||||
"""
|
||||
|
||||
# Explicit imports to avoid unnecessary dependencies
|
||||
import Plug.Conn, only: [put_status: 2]
|
||||
import Phoenix.Controller, only: [json: 2]
|
||||
|
||||
alias WandererApp.Api.Map, as: MapApi
|
||||
alias WandererApp.Api.MapSolarSystem
|
||||
require Logger
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Map ID Resolution
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@spec fetch_map_id(map()) :: {:ok, String.t()} | {:error, String.t()}
|
||||
def fetch_map_id(%{"map_id" => id}) when is_binary(id) do
|
||||
case Ecto.UUID.cast(id) do
|
||||
{:ok, _} -> {:ok, id}
|
||||
:error -> {:error, "Invalid UUID format for map_id: #{id}"}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_map_id(%{"slug" => slug}) when is_binary(slug) do
|
||||
case MapApi.get_map_by_slug(slug) do
|
||||
{:ok, %{id: id}} -> {:ok, id}
|
||||
_ -> {:error, "No map found for slug=#{slug}"}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Parameter Validators and Parsers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@spec require_param(map(), String.t()) :: {:ok, any()} | {:error, String.t()}
|
||||
def require_param(params, key) do
|
||||
case Map.fetch(params, key) do
|
||||
{:ok, val} when is_binary(val) ->
|
||||
trimmed = String.trim(val)
|
||||
if trimmed == "" do
|
||||
{:error, "Param #{key} cannot be empty"}
|
||||
else
|
||||
{:ok, trimmed}
|
||||
end
|
||||
|
||||
{:ok, val} ->
|
||||
{:ok, val}
|
||||
|
||||
:error ->
|
||||
{:error, "Missing required param: #{key}"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_int(binary() | integer()) :: {:ok, integer()} | {:error, String.t()}
|
||||
def parse_int(str) when is_binary(str) do
|
||||
Logger.debug("Parsing integer from: #{inspect(str)}")
|
||||
|
||||
case Integer.parse(str) do
|
||||
{num, ""} -> {:ok, num}
|
||||
_ -> {:error, "Invalid integer format: #{str}"}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_int(num) when is_integer(num), do: {:ok, num}
|
||||
def parse_int(other), do: {:error, "Expected integer or string, got: #{inspect(other)}"}
|
||||
|
||||
@spec parse_int!(binary() | integer()) :: integer()
|
||||
def parse_int!(str) do
|
||||
case parse_int(str) do
|
||||
{:ok, num} -> num
|
||||
{:error, msg} -> raise ArgumentError, msg
|
||||
end
|
||||
end
|
||||
|
||||
@spec validate_uuid(any()) :: {:ok, String.t()} | {:error, String.t()}
|
||||
def validate_uuid(id) when is_binary(id) do
|
||||
case Ecto.UUID.cast(id) do
|
||||
{:ok, uuid} -> {:ok, uuid}
|
||||
:error -> {:error, "Invalid UUID format: #{id}"}
|
||||
end
|
||||
end
|
||||
|
||||
def validate_uuid(_), do: {:error, "ID must be a UUID string"}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Parameter Extraction
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@doc """
|
||||
Extract and validate parameters for upserting a system.
|
||||
Returns {:ok, attrs} or {:error, error_message}.
|
||||
"""
|
||||
@spec extract_upsert_params(map()) :: {:ok, map()} | {:error, String.t()}
|
||||
def extract_upsert_params(params) when is_map(params) do
|
||||
required = ["solar_system_id"]
|
||||
optional = [
|
||||
"solar_system_name", "position_x", "position_y", "coordinates",
|
||||
"status", "visible", "description", "tag",
|
||||
"locked", "temporary_name", "labels"
|
||||
]
|
||||
|
||||
case Map.fetch(params, "solar_system_id") do
|
||||
:error -> {:error, "Missing solar_system_id in request body"}
|
||||
{:ok, _} ->
|
||||
params
|
||||
|> Map.take(required ++ optional)
|
||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||
|> Enum.into(%{})
|
||||
|> then(&{:ok, &1})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extract and validate parameters for updating a system.
|
||||
Returns {:ok, attrs} or {:error, error_message}.
|
||||
"""
|
||||
@spec extract_update_params(map()) :: {:ok, map()} | {:error, String.t()}
|
||||
def extract_update_params(params) when is_map(params) do
|
||||
allowed = [
|
||||
"solar_system_name", "position_x", "position_y", "coordinates",
|
||||
"status", "visible", "description", "tag",
|
||||
"locked", "temporary_name", "labels"
|
||||
]
|
||||
|
||||
attrs =
|
||||
params
|
||||
|> Map.take(allowed)
|
||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
{:ok, attrs}
|
||||
end
|
||||
|
||||
@spec normalize_connection_params(map()) :: {:ok, map()} | {:error, String.t()}
|
||||
def normalize_connection_params(params) do
|
||||
# Convert all keys to strings for consistent access
|
||||
string_params = for {k, v} <- params, into: %{} do
|
||||
{to_string(k), v}
|
||||
end
|
||||
|
||||
# Define parameter mappings for normalization
|
||||
aliases = %{
|
||||
"source" => "solar_system_source",
|
||||
"source_id" => "solar_system_source",
|
||||
"target" => "solar_system_target",
|
||||
"target_id" => "solar_system_target"
|
||||
}
|
||||
|
||||
# Normalize parameters using aliases
|
||||
normalized_params = Enum.reduce(aliases, string_params, fn {alias_key, std_key}, acc ->
|
||||
if Map.has_key?(acc, alias_key) && !Map.has_key?(acc, std_key) do
|
||||
Map.put(acc, std_key, acc[alias_key])
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
# Handle required parameters
|
||||
with {:ok, src} <- parse_to_int(normalized_params["solar_system_source"], "solar_system_source"),
|
||||
{:ok, tgt} <- parse_to_int(normalized_params["solar_system_target"], "solar_system_target") do
|
||||
|
||||
# Handle optional parameters with sane defaults
|
||||
type = normalized_params["type"] || 0
|
||||
mass_status = normalized_params["mass_status"] || 0
|
||||
time_status = normalized_params["time_status"] || 0
|
||||
ship_size_type = normalized_params["ship_size_type"] || 0
|
||||
# Coerce to boolean; accept "true"/"false", 1/0, etc.
|
||||
locked =
|
||||
case normalized_params["locked"] do
|
||||
val when val in [true, "true", 1, "1"] -> true
|
||||
val when val in [false, "false", 0, "0"] -> false
|
||||
nil -> false
|
||||
other -> other # keep unknowns for caller-side validation
|
||||
end
|
||||
custom_info = normalized_params["custom_info"]
|
||||
wormhole_type = normalized_params["wormhole_type"]
|
||||
|
||||
# Build standardized attrs map
|
||||
attrs = %{
|
||||
"solar_system_source" => src,
|
||||
"solar_system_target" => tgt,
|
||||
"type" => parse_optional_int(type, 0),
|
||||
"mass_status" => parse_optional_int(mass_status, 0),
|
||||
"time_status" => parse_optional_int(time_status, 0),
|
||||
"ship_size_type" => parse_optional_int(ship_size_type, 0)
|
||||
}
|
||||
|
||||
# Add non-nil optional attributes
|
||||
attrs = if is_nil(locked), do: attrs, else: Map.put(attrs, "locked", locked)
|
||||
attrs = if is_nil(custom_info), do: attrs, else: Map.put(attrs, "custom_info", custom_info)
|
||||
attrs = if is_nil(wormhole_type), do: attrs, else: Map.put(attrs, "wormhole_type", wormhole_type)
|
||||
|
||||
{:ok, attrs}
|
||||
else
|
||||
{:error, msg} -> {:error, msg}
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to handle various input formats
|
||||
defp parse_to_int(nil, field), do: {:error, "Missing #{field}"}
|
||||
defp parse_to_int(val, _field) when is_integer(val), do: {:ok, val}
|
||||
defp parse_to_int(val, field) when is_binary(val) do
|
||||
case Integer.parse(val) do
|
||||
{i, ""} -> {:ok, i}
|
||||
:error -> {:error, "Invalid #{field}: #{val}"}
|
||||
_ -> {:error, "Invalid #{field}: #{val}"}
|
||||
end
|
||||
end
|
||||
defp parse_to_int(val, field), do: {:error, "Invalid #{field} type: #{inspect(val)}"}
|
||||
|
||||
defp parse_optional_int(nil, default), do: default
|
||||
defp parse_optional_int(i, _default) when is_integer(i), do: i
|
||||
defp parse_optional_int(s, default) when is_binary(s) do
|
||||
case Integer.parse(s) do
|
||||
{i, _} -> i
|
||||
:error -> default
|
||||
end
|
||||
end
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Standardized JSON Responses
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@spec respond_data(Plug.Conn.t(), any(), atom() | integer()) :: Plug.Conn.t()
|
||||
def respond_data(conn, data, status \\ :ok) do
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> json(%{data: data})
|
||||
end
|
||||
|
||||
@spec error_response(Plug.Conn.t(), atom() | integer(), String.t(), map() | nil) :: Plug.Conn.t()
|
||||
def error_response(conn, status, message, details \\ nil) do
|
||||
body = if details, do: %{error: message, details: details}, else: %{error: message}
|
||||
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> json(body)
|
||||
end
|
||||
|
||||
@spec error_not_found(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
|
||||
def error_not_found(conn, message), do: error_response(conn, :not_found, message)
|
||||
|
||||
@doc """
|
||||
Formats error messages for consistent display.
|
||||
"""
|
||||
@spec format_error(any()) :: String.t()
|
||||
def format_error(error) when is_binary(error), do: error
|
||||
def format_error(error) when is_atom(error), do: Atom.to_string(error)
|
||||
def format_error(error), do: inspect(error)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# JSON Serialization
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@spec map_system_to_json(struct()) :: map()
|
||||
def map_system_to_json(system) do
|
||||
base =
|
||||
Map.take(system, ~w(
|
||||
id map_id solar_system_id custom_name temporary_name description tag labels
|
||||
locked visible status position_x position_y inserted_at updated_at
|
||||
)a)
|
||||
|
||||
original = get_original_name(system.solar_system_id)
|
||||
name = pick_name(system)
|
||||
|
||||
base
|
||||
|> Map.put(:original_name, original)
|
||||
|> Map.put(:name, name)
|
||||
end
|
||||
|
||||
defp get_original_name(id) do
|
||||
case MapSolarSystem.by_solar_system_id(id) do
|
||||
{:ok, sys} -> sys.solar_system_name
|
||||
_ -> "System #{id}"
|
||||
end
|
||||
end
|
||||
|
||||
defp pick_name(%{temporary_name: t, custom_name: c, solar_system_id: id}) do
|
||||
cond do
|
||||
t not in [nil, ""] -> t
|
||||
c not in [nil, ""] -> c
|
||||
true -> get_original_name(id)
|
||||
end
|
||||
end
|
||||
|
||||
@spec connection_to_json(struct()) :: map()
|
||||
def connection_to_json(conn) do
|
||||
Map.take(conn, ~w(
|
||||
id map_id solar_system_source solar_system_target mass_status
|
||||
time_status ship_size_type type wormhole_type inserted_at updated_at
|
||||
)a)
|
||||
end
|
||||
end
|
||||
@@ -17,24 +17,26 @@ defmodule WandererAppWeb.MapCharacters do
|
||||
|> handle_info_or_assign(assigns)}
|
||||
end
|
||||
|
||||
# attr(:groups, :any, required: true)
|
||||
# attr(:character_settings, :any, required: true)
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={@id}>
|
||||
<ul :for={group <- @groups} class="space-y-4 border-t border-b border-gray-200 py-4">
|
||||
<ul :for={group <- @groups} class="border-t border-b border-gray-200 py-0">
|
||||
<li :for={character <- group.characters}>
|
||||
<div class="flex items-center justify-between w-full space-x-2 p-1 hover:bg-gray-900">
|
||||
<.character_entry character={character} character_settings={@character_settings} />
|
||||
<.character_entry character={character} />
|
||||
<button
|
||||
:if={character.tracked}
|
||||
phx-click="untrack"
|
||||
phx-value-event-data={character.id}
|
||||
class="btn btn-sm btn-icon"
|
||||
class="btn btn-sm btn-icon py-1"
|
||||
>
|
||||
<.icon name="hero-eye-slash" class="h-5 w-5" /> Untrack
|
||||
</button>
|
||||
|
||||
<span :if={not character.tracked} class="text-white rounded-full px-2">
|
||||
Viewer
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -43,31 +45,43 @@ defmodule WandererAppWeb.MapCharacters do
|
||||
end
|
||||
|
||||
attr(:character, :any, required: true)
|
||||
attr(:character_settings, :any, required: true)
|
||||
|
||||
defp character_entry(assigns) do
|
||||
~H"""
|
||||
<div class="flex items-center gap-3 text-sm w-[450px]">
|
||||
<span
|
||||
:if={is_tracked?(@character.id, @character_settings)}
|
||||
class="text-green-500 rounded-full px-2 py-1"
|
||||
>
|
||||
Tracked
|
||||
<div class="flex flex-col p-4 items-center gap-2 tooltip tooltip-top" data-tip="Active from">
|
||||
<span class="text-green-500 rounded-full px-2 py-1 whitespace-nowrap">
|
||||
<.local_time id={@character.id} at={@character.from} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="avatar">
|
||||
<div class="rounded-md w-8 h-8">
|
||||
<img src={member_icon_url(@character.eve_id)} alt={@character.name} />
|
||||
</div>
|
||||
</div>
|
||||
<span class="whitespace-nowrap">{@character.name}</span>
|
||||
<span :if={@character.alliance_ticker} class="whitespace-nowrap">
|
||||
[{@character.alliance_ticker}]
|
||||
</span>
|
||||
<span :if={@character.corporation_ticker} class="whitespace-nowrap">
|
||||
[{@character.corporation_ticker}]
|
||||
</span>
|
||||
|
||||
<span :if={is_online?(@character.id)} class="text-green-500 rounded-full px-2 py-1">
|
||||
Online
|
||||
</span>
|
||||
<span :if={not is_online?(@character.id)} class="text-red-500 rounded-full px-2 py-1">
|
||||
Offline
|
||||
</span>
|
||||
<div class="avatar">
|
||||
<div class="rounded-md w-8 h-8">
|
||||
<img src={member_icon_url(@character.eve_id)} alt={@character.name} />
|
||||
</div>
|
||||
</div>
|
||||
<span>{@character.name}</span>
|
||||
<span :if={@character.alliance_ticker}>[{@character.alliance_ticker}]</span>
|
||||
<span :if={@character.corporation_ticker}>[{@character.corporation_ticker}]</span>
|
||||
|
||||
<span :if={@character.tracked} class="text-green-500 rounded-full px-2 py-1">
|
||||
Tracked
|
||||
</span>
|
||||
|
||||
<span :if={not @character.tracked} class="text-red-500 rounded-full px-2 py-1 whitespace-nowrap">
|
||||
Not Tracked
|
||||
</span>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
@@ -79,12 +93,6 @@ defmodule WandererAppWeb.MapCharacters do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp is_tracked?(character_id, character_settings) do
|
||||
Enum.any?(character_settings, fn setting ->
|
||||
setting.character_id == character_id && setting.tracked
|
||||
end)
|
||||
end
|
||||
|
||||
defp is_online?(character_id) do
|
||||
{:ok, state} = WandererApp.Character.get_character_state(character_id)
|
||||
state.is_online
|
||||
|
||||
@@ -56,13 +56,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
|
||||
case track_character do
|
||||
false ->
|
||||
:ok =
|
||||
WandererApp.Character.TrackingUtils.untrack(
|
||||
map_characters,
|
||||
map_id,
|
||||
self()
|
||||
)
|
||||
|
||||
:ok = WandererApp.Character.TrackingUtils.untrack(map_characters, map_id, self())
|
||||
:ok = WandererApp.Character.TrackingUtils.remove_characters(map_characters, map_id)
|
||||
|
||||
_ ->
|
||||
@@ -232,7 +226,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
def handle_ui_event(
|
||||
_event,
|
||||
_body,
|
||||
%{assigns: %{has_tracked_characters?: false}} =
|
||||
%{assigns: %{has_tracked_characters?: false, can_track?: true}} =
|
||||
socket
|
||||
) do
|
||||
Process.send_after(self(), %{event: :show_tracking}, 10)
|
||||
@@ -248,7 +242,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
def handle_ui_event(
|
||||
event,
|
||||
body,
|
||||
%{assigns: %{main_character_id: main_character_id}} =
|
||||
%{assigns: %{main_character_id: main_character_id, can_track?: true}} =
|
||||
socket
|
||||
)
|
||||
when is_nil(main_character_id) do
|
||||
@@ -266,7 +260,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
{:ok, map_server_started} = WandererApp.Cache.lookup("map_#{map_id}:started", false)
|
||||
|
||||
if map_server_started do
|
||||
Process.send_after(self(), %{event: :map_server_started}, 10)
|
||||
Process.send_after(self(), %{event: :map_server_started}, 50)
|
||||
else
|
||||
WandererApp.Map.Manager.start_map(map_id)
|
||||
end
|
||||
@@ -439,6 +433,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
assigns: %{
|
||||
current_user: current_user,
|
||||
map_id: map_id,
|
||||
main_character_id: main_character_id,
|
||||
tracked_characters: tracked_characters,
|
||||
has_tracked_characters?: has_tracked_characters?,
|
||||
user_permissions:
|
||||
@@ -460,7 +455,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
end
|
||||
|
||||
events =
|
||||
case not has_tracked_characters? do
|
||||
case track_character && not has_tracked_characters? do
|
||||
true ->
|
||||
events ++ [:empty_tracked_characters]
|
||||
|
||||
@@ -468,13 +463,28 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
events
|
||||
end
|
||||
|
||||
character_limit_reached? = present_character_ids |> Enum.count() >= characters_limit
|
||||
|
||||
events =
|
||||
case present_character_ids |> Enum.count() < characters_limit do
|
||||
true ->
|
||||
cond do
|
||||
# in case user has not tracked any character track his main character as viewer
|
||||
track_character && not has_tracked_characters? ->
|
||||
main_character = Enum.find(current_user.characters, &(&1.id == main_character_id))
|
||||
events ++ [{:track_characters, [main_character], false}]
|
||||
|
||||
track_character && not character_limit_reached? ->
|
||||
events ++ [{:track_characters, tracked_characters, track_character}]
|
||||
|
||||
_ ->
|
||||
track_character && character_limit_reached? ->
|
||||
events ++ [:map_character_limit]
|
||||
|
||||
# in case user has view only permissions track his main character as viewer
|
||||
not track_character ->
|
||||
main_character = Enum.find(current_user.characters, &(&1.id == main_character_id))
|
||||
events ++ [{:track_characters, [main_character], track_character}]
|
||||
|
||||
true ->
|
||||
events
|
||||
end
|
||||
|
||||
initial_data =
|
||||
|
||||
@@ -51,21 +51,36 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
|
||||
%{"system_id" => solar_system_id, "routes_settings" => routes_settings} = _event,
|
||||
%{assigns: %{map_id: map_id, map_loaded?: true}} = socket
|
||||
) do
|
||||
Task.async(fn ->
|
||||
{:ok, hubs} = map_id |> WandererApp.Map.list_hubs()
|
||||
{:ok, map} = map_id |> WandererApp.Map.get_map()
|
||||
hubs_limit = map |> Map.get(:hubs_limit, 20)
|
||||
|
||||
{:ok, hubs} = map_id |> WandererApp.Map.list_hubs()
|
||||
|
||||
is_hubs_limit_reached = hubs |> Enum.count() > hubs_limit
|
||||
|
||||
Task.async(fn ->
|
||||
{:ok, routes} =
|
||||
WandererApp.Maps.find_routes(
|
||||
map_id,
|
||||
hubs,
|
||||
solar_system_id,
|
||||
get_routes_settings(routes_settings)
|
||||
get_routes_settings(routes_settings),
|
||||
is_hubs_limit_reached
|
||||
)
|
||||
|
||||
{:routes, {solar_system_id, routes}}
|
||||
end)
|
||||
|
||||
{:noreply, socket}
|
||||
if is_hubs_limit_reached do
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(
|
||||
:warning,
|
||||
"The Map hubs limit has been reached, please try to remove some hubs first, or contact the map administrators."
|
||||
)}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
@@ -80,16 +95,22 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
{:ok, map} = map_id |> WandererApp.Map.get_map()
|
||||
hubs_limit = map |> Map.get(:hubs_limit, 20)
|
||||
|
||||
{:ok, hubs} = WandererApp.MapUserSettingsRepo.get_hubs(map_id, current_user.id)
|
||||
|
||||
is_hubs_limit_reached = hubs |> Enum.count() > hubs_limit
|
||||
|
||||
Task.async(fn ->
|
||||
if is_subscription_active? do
|
||||
{:ok, hubs} = WandererApp.MapUserSettingsRepo.get_hubs(map_id, current_user.id)
|
||||
|
||||
{:ok, routes} =
|
||||
WandererApp.Maps.find_routes(
|
||||
map_id,
|
||||
hubs,
|
||||
solar_system_id,
|
||||
get_routes_settings(routes_settings)
|
||||
get_routes_settings(routes_settings),
|
||||
is_hubs_limit_reached
|
||||
)
|
||||
|
||||
{:user_routes, {solar_system_id, routes}}
|
||||
@@ -98,9 +119,197 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
|
||||
end
|
||||
end)
|
||||
|
||||
if is_hubs_limit_reached do
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(
|
||||
:warning,
|
||||
"The user hubs limit has been reached, please try to remove some hubs first, or contact the map administrators."
|
||||
)}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"add_hub",
|
||||
%{"system_id" => solar_system_id} = _event,
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
current_user: current_user,
|
||||
main_character_id: main_character_id,
|
||||
has_tracked_characters?: true,
|
||||
user_permissions: %{update_system: true}
|
||||
}
|
||||
} =
|
||||
socket
|
||||
)
|
||||
when not is_nil(main_character_id) do
|
||||
{:ok, map} = map_id |> WandererApp.Map.get_map()
|
||||
hubs_limit = map |> Map.get(:hubs_limit, 20)
|
||||
|
||||
{:ok, hubs} = map_id |> WandererApp.Map.list_hubs()
|
||||
|
||||
if hubs |> Enum.count() < hubs_limit do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.add_hub(%{
|
||||
solar_system_id: solar_system_id
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
WandererApp.User.ActivityTracker.track_map_event(:hub_added, %{
|
||||
character_id: main_character_id,
|
||||
user_id: current_user.id,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id
|
||||
})
|
||||
|
||||
{:noreply, socket}
|
||||
else
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(
|
||||
:warning,
|
||||
"The Map hubs limit has been reached, please try to remove some hubs first, or contact the map administrators."
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"delete_hub",
|
||||
%{"system_id" => solar_system_id} = _event,
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
current_user: current_user,
|
||||
main_character_id: main_character_id,
|
||||
has_tracked_characters?: true,
|
||||
user_permissions: %{update_system: true}
|
||||
}
|
||||
} =
|
||||
socket
|
||||
)
|
||||
when not is_nil(main_character_id) do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.remove_hub(%{
|
||||
solar_system_id: solar_system_id
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
WandererApp.User.ActivityTracker.track_map_event(:hub_removed, %{
|
||||
character_id: main_character_id,
|
||||
user_id: current_user.id,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id
|
||||
})
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"get_user_hubs",
|
||||
_event,
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
current_user: current_user
|
||||
}
|
||||
} =
|
||||
socket
|
||||
) do
|
||||
{:ok, hubs} = WandererApp.MapUserSettingsRepo.get_hubs(map_id, current_user.id)
|
||||
|
||||
{:reply, %{hubs: hubs}, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"add_user_hub",
|
||||
%{"system_id" => solar_system_id} = _event,
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
current_user: current_user
|
||||
}
|
||||
} =
|
||||
socket
|
||||
) do
|
||||
{:ok, map} = map_id |> WandererApp.Map.get_map()
|
||||
hubs_limit = map |> Map.get(:hubs_limit, 20)
|
||||
|
||||
{:ok, hubs} = WandererApp.MapUserSettingsRepo.get_hubs(map_id, current_user.id)
|
||||
|
||||
if hubs |> Enum.count() < hubs_limit do
|
||||
hubs = hubs ++ ["#{solar_system_id}"]
|
||||
|
||||
{:ok, _} =
|
||||
WandererApp.MapUserSettingsRepo.update_hubs(
|
||||
map_id,
|
||||
current_user.id,
|
||||
hubs
|
||||
)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"map_updated",
|
||||
%{user_hubs: hubs}
|
||||
)}
|
||||
else
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"map_updated",
|
||||
%{user_hubs: hubs}
|
||||
)
|
||||
|> put_flash(
|
||||
:warning,
|
||||
"The user hubs limit has been reached, please try to remove some user hubs first, or contact the map administrators."
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"delete_user_hub",
|
||||
%{"system_id" => solar_system_id} = _event,
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
current_user: current_user
|
||||
}
|
||||
} =
|
||||
socket
|
||||
) do
|
||||
{:ok, hubs} = WandererApp.MapUserSettingsRepo.get_hubs(map_id, current_user.id)
|
||||
|
||||
case hubs |> Enum.member?("#{solar_system_id}") do
|
||||
true ->
|
||||
hubs = hubs |> Enum.reject(fn hub -> hub == "#{solar_system_id}" end)
|
||||
|
||||
{:ok, _} =
|
||||
WandererApp.MapUserSettingsRepo.update_hubs(
|
||||
map_id,
|
||||
current_user.id,
|
||||
hubs
|
||||
)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"map_updated",
|
||||
%{user_hubs: hubs}
|
||||
)}
|
||||
|
||||
_ ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"map_updated",
|
||||
%{user_hubs: hubs}
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"set_autopilot_waypoint",
|
||||
%{
|
||||
|
||||
@@ -211,10 +211,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
})
|
||||
end
|
||||
|
||||
Phoenix.PubSub.broadcast!(WandererApp.PubSub, map_id, %{
|
||||
event: :signatures_updated,
|
||||
payload: solar_system_source
|
||||
})
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :signatures_updated, solar_system_source)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
@@ -261,10 +258,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
})
|
||||
end)
|
||||
|
||||
Phoenix.PubSub.broadcast!(WandererApp.PubSub, map_id, %{
|
||||
event: :signatures_updated,
|
||||
payload: solar_system_source
|
||||
})
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :signatures_updated, solar_system_source)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
@@ -273,6 +267,39 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"undo_delete_signatures",
|
||||
%{"system_id" => solar_system_id, "eve_ids" => eve_ids} = payload,
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
main_character_id: main_character_id,
|
||||
user_permissions: %{update_system: true}
|
||||
}
|
||||
} = socket
|
||||
)
|
||||
when not is_nil(main_character_id) do
|
||||
case WandererApp.Api.MapSystem.read_by_map_and_solar_system(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: get_integer(solar_system_id)
|
||||
}) do
|
||||
{:ok, system} ->
|
||||
restored =
|
||||
WandererApp.Api.MapSystemSignature.by_system_id_all!(system.id)
|
||||
|> Enum.filter(fn s -> s.eve_id in eve_ids end)
|
||||
|> Enum.map(fn s ->
|
||||
s |> WandererApp.Api.MapSystemSignature.update!(%{deleted: false})
|
||||
end)
|
||||
Phoenix.PubSub.broadcast!(WandererApp.PubSub, map_id, %{
|
||||
event: :signatures_updated,
|
||||
payload: system.solar_system_id
|
||||
})
|
||||
{:noreply, socket}
|
||||
_ ->
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(event, body, socket),
|
||||
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule WandererAppWeb.MapStructuresEventHandler do
|
||||
alias WandererApp.Api.MapSystem
|
||||
alias WandererApp.Structure
|
||||
|
||||
alias WandererAppWeb.{MapEventHandler, MapCoreEventHandler}
|
||||
alias WandererAppWeb. MapCoreEventHandler
|
||||
|
||||
def handle_server_event(%{event: :structures_updated, payload: _solar_system_id}, socket) do
|
||||
socket
|
||||
|
||||
@@ -5,6 +5,8 @@ defmodule WandererAppWeb.MapCharactersLive do
|
||||
|
||||
alias WandererAppWeb.MapCharacters
|
||||
|
||||
@refresh_interval :timer.seconds(30)
|
||||
|
||||
def mount(
|
||||
%{"slug" => map_slug} = _params,
|
||||
_session,
|
||||
@@ -44,6 +46,15 @@ defmodule WandererAppWeb.MapCharactersLive do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
:refresh_tracking_data,
|
||||
socket
|
||||
) do
|
||||
Process.send_after(self(), :refresh_tracking_data, @refresh_interval)
|
||||
{:noreply, socket |> load_characters()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
_event,
|
||||
@@ -101,17 +112,35 @@ defmodule WandererAppWeb.MapCharactersLive do
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
Process.send_after(self(), :refresh_tracking_data, @refresh_interval)
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :map_characters)
|
||||
|> assign(:page_title, "Map - Characters")
|
||||
|> load_characters()
|
||||
end
|
||||
|
||||
defp get_all_characters(map_id) do
|
||||
{:ok, present_characters} =
|
||||
WandererApp.Cache.lookup(
|
||||
"map_#{map_id}:presence_data",
|
||||
[]
|
||||
)
|
||||
|
||||
present_characters =
|
||||
present_characters
|
||||
|> Enum.map(fn character ->
|
||||
character |> Map.merge(WandererApp.Character.get_character!(character.character_id))
|
||||
end)
|
||||
|
||||
present_characters
|
||||
end
|
||||
|
||||
defp load_characters(%{assigns: %{map_id: map_id}} = socket) do
|
||||
map_characters =
|
||||
map_id
|
||||
|> WandererApp.Map.list_characters()
|
||||
|> Enum.map(&map_ui_character/1)
|
||||
|> get_all_characters()
|
||||
|> Enum.map(fn character -> map_ui_character(map_id, character) end)
|
||||
|
||||
groups =
|
||||
map_characters
|
||||
@@ -132,20 +161,22 @@ defmodule WandererAppWeb.MapCharactersLive do
|
||||
|> assign(:groups, groups)
|
||||
end
|
||||
|
||||
defp map_ui_character(character),
|
||||
do:
|
||||
character
|
||||
|> Map.take([
|
||||
:id,
|
||||
:user_id,
|
||||
:eve_id,
|
||||
:name,
|
||||
:online,
|
||||
:corporation_id,
|
||||
:corporation_name,
|
||||
:corporation_ticker,
|
||||
:alliance_id,
|
||||
:alliance_name,
|
||||
:alliance_ticker
|
||||
])
|
||||
defp map_ui_character(map_id, character) do
|
||||
character
|
||||
|> Map.take([
|
||||
:id,
|
||||
:user_id,
|
||||
:eve_id,
|
||||
:name,
|
||||
:online,
|
||||
:corporation_id,
|
||||
:corporation_name,
|
||||
:corporation_ticker,
|
||||
:alliance_id,
|
||||
:alliance_name,
|
||||
:alliance_ticker,
|
||||
:from,
|
||||
:tracked
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<.link navigate={~p"/#{@map_slug}"} class="text-neutral-100">
|
||||
<%= @map_name %>
|
||||
</.link>
|
||||
- Characters [<%= @characters_count %>]
|
||||
- Active Characters [<%= @characters_count %>]
|
||||
</span>
|
||||
</nav>
|
||||
<main
|
||||
@@ -24,7 +24,6 @@
|
||||
id="map-characters"
|
||||
notify_to={self()}
|
||||
groups={@groups}
|
||||
character_settings={@character_settings}
|
||||
event_name="character_event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -42,8 +42,6 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
]
|
||||
|
||||
@map_system_ui_events [
|
||||
"add_hub",
|
||||
"delete_hub",
|
||||
"delete_systems",
|
||||
"get_system_static_infos",
|
||||
"manual_add_system",
|
||||
@@ -56,10 +54,7 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
"update_system_locked",
|
||||
"update_system_tag",
|
||||
"update_system_temporary_name",
|
||||
"update_system_status",
|
||||
"get_user_hubs",
|
||||
"add_user_hub",
|
||||
"delete_user_hub"
|
||||
"update_system_status"
|
||||
]
|
||||
|
||||
@map_system_comments_events [
|
||||
@@ -108,7 +103,12 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
@map_routes_ui_events [
|
||||
"get_routes",
|
||||
"get_user_routes",
|
||||
"set_autopilot_waypoint"
|
||||
"set_autopilot_waypoint",
|
||||
"add_hub",
|
||||
"delete_hub",
|
||||
"get_user_hubs",
|
||||
"add_user_hub",
|
||||
"delete_user_hub"
|
||||
]
|
||||
|
||||
@map_signatures_events [
|
||||
@@ -121,7 +121,8 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
"update_signatures",
|
||||
"get_signatures",
|
||||
"link_signature_to_system",
|
||||
"unlink_signature"
|
||||
"unlink_signature",
|
||||
"undo_delete_signatures"
|
||||
]
|
||||
|
||||
@map_structures_events [
|
||||
|
||||
@@ -4,6 +4,8 @@ defmodule WandererAppWeb.MapLive do
|
||||
|
||||
require Logger
|
||||
|
||||
@server_event_unsync_timeout :timer.minutes(2)
|
||||
|
||||
@impl true
|
||||
def mount(%{"slug" => map_slug} = _params, _session, socket) when is_connected?(socket) do
|
||||
Process.send_after(self(), %{event: :load_map}, Enum.random(10..800))
|
||||
@@ -93,6 +95,19 @@ defmodule WandererAppWeb.MapLive do
|
||||
)
|
||||
|> push_navigate(to: ~p"/tracking/#{map_slug}")}
|
||||
|
||||
@impl true
|
||||
def handle_info(%{timestamp: timestamp} = info, %{assigns: %{map_slug: map_slug}} = socket) do
|
||||
duration = DateTime.diff(DateTime.utc_now(), timestamp, :millisecond)
|
||||
|
||||
if duration > @server_event_unsync_timeout do
|
||||
{:noreply, socket |> push_navigate(to: ~p"/#{map_slug}")}
|
||||
else
|
||||
{:noreply,
|
||||
socket
|
||||
|> WandererAppWeb.MapEventHandler.handle_event(info)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(info, socket),
|
||||
do:
|
||||
|
||||
@@ -10,19 +10,39 @@ defmodule WandererAppWeb.Presence do
|
||||
end
|
||||
|
||||
def handle_metas(map_id, %{joins: _joins, leaves: _leaves}, presences, state) do
|
||||
presence_character_ids =
|
||||
presence_data =
|
||||
presences
|
||||
|> Enum.map(fn {character_id, _} -> character_id end)
|
||||
|> Enum.map(fn {character_id, meta} ->
|
||||
from =
|
||||
meta
|
||||
|> Enum.map(& &1.from)
|
||||
|> Enum.sort(&(DateTime.compare(&1, &2) != :gt))
|
||||
|> List.first()
|
||||
|
||||
any_tracked = Enum.any?(meta, fn %{tracked: tracked} -> tracked end)
|
||||
|
||||
%{character_id: character_id, tracked: any_tracked, from: from}
|
||||
end)
|
||||
|
||||
presence_tracked_character_ids =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{tracked: tracked} -> tracked end)
|
||||
|> Enum.map(fn %{character_id: character_id} ->
|
||||
character_id
|
||||
end)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_character_ids", presence_character_ids)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_character_ids",
|
||||
presence_tracked_character_ids
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_data",
|
||||
presence_data
|
||||
)
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def presence_character_ids(map_id) do
|
||||
map_id
|
||||
|> list()
|
||||
|> Enum.map(fn {character_id, _} -> character_id end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -169,6 +169,7 @@ defmodule WandererAppWeb.Router do
|
||||
pipeline :api_map do
|
||||
plug WandererAppWeb.Plugs.CheckMapApiKey
|
||||
plug WandererAppWeb.Plugs.CheckMapSubscription
|
||||
plug WandererAppWeb.Plugs.AssignMapOwner
|
||||
end
|
||||
|
||||
pipeline :api_kills do
|
||||
@@ -206,17 +207,42 @@ defmodule WandererAppWeb.Router do
|
||||
scope "/api/map", WandererAppWeb do
|
||||
pipe_through [:api, :api_map]
|
||||
get "/audit", MapAuditAPIController, :index
|
||||
get "/systems", MapAPIController, :list_systems
|
||||
get "/system", MapAPIController, :show_system
|
||||
get "/connections", MapAPIController, :list_connections
|
||||
get "/characters", MapAPIController, :tracked_characters_with_info
|
||||
# Deprecated routes - use /api/maps/:map_identifier/systems instead
|
||||
get "/systems", MapSystemAPIController, :list_systems
|
||||
get "/system", MapSystemAPIController, :show_system
|
||||
get "/connections", MapConnectionAPIController, :list_all_connections
|
||||
get "/characters", MapAPIController, :list_tracked_characters
|
||||
get "/structure-timers", MapAPIController, :show_structure_timers
|
||||
get "/character-activity", MapAPIController, :character_activity
|
||||
get "/user_characters", MapAPIController, :user_characters
|
||||
|
||||
get "/acls", MapAccessListAPIController, :index
|
||||
post "/acls", MapAccessListAPIController, :create
|
||||
end
|
||||
|
||||
#
|
||||
# Unified RESTful routes for systems & connections by slug or ID
|
||||
#
|
||||
scope "/api/maps/:map_identifier", WandererAppWeb do
|
||||
pipe_through [:api, :api_map]
|
||||
|
||||
patch "/connections", MapConnectionAPIController, :update
|
||||
delete "/connections", MapConnectionAPIController, :delete
|
||||
delete "/systems", MapSystemAPIController, :delete
|
||||
resources "/systems", MapSystemAPIController, only: [:index, :show, :create, :update, :delete]
|
||||
resources "/connections", MapConnectionAPIController, only: [:index, :show, :create, :update, :delete], param: "id"
|
||||
resources "/structures", MapSystemStructureAPIController, except: [:new, :edit]
|
||||
get "/structure-timers", MapSystemStructureAPIController, :structure_timers
|
||||
resources "/signatures", MapSystemSignatureAPIController, except: [:new, :edit]
|
||||
get "/user-characters", MapAPIController, :show_user_characters
|
||||
get "/tracked-characters", MapAPIController, :show_tracked_characters
|
||||
end
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Other API routes
|
||||
#
|
||||
scope "/api/characters", WandererAppWeb do
|
||||
pipe_through [:api, :api_character]
|
||||
get "/", CharactersAPIController, :index
|
||||
|
||||
165
lib/wanderer_app_web/schemas/api_schemas.ex
Normal file
165
lib/wanderer_app_web/schemas/api_schemas.ex
Normal file
@@ -0,0 +1,165 @@
|
||||
defmodule WandererAppWeb.Schemas.ApiSchemas do
|
||||
@moduledoc """
|
||||
Shared OpenAPI schema definitions for the Wanderer API.
|
||||
|
||||
This module defines common schema components that can be reused
|
||||
across different controller specifications.
|
||||
"""
|
||||
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
# Standard response wrappers
|
||||
def data_wrapper(schema) do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: schema
|
||||
},
|
||||
required: ["data"]
|
||||
}
|
||||
end
|
||||
|
||||
# Standard error responses
|
||||
def error_response(description \\ "Error") do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %Schema{type: :string, description: "Brief error message"},
|
||||
details: %Schema{type: :string, description: "Detailed explanation", nullable: true},
|
||||
code: %Schema{type: :string, description: "Optional error code", nullable: true}
|
||||
},
|
||||
required: ["error"],
|
||||
example: %{"error" => description, "details" => "Additional information about the error"}
|
||||
}
|
||||
end
|
||||
|
||||
# Common entity schemas
|
||||
def character_schema do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, description: "Character UUID"},
|
||||
eve_id: %Schema{type: :string, description: "EVE Online character ID"},
|
||||
name: %Schema{type: :string, description: "Character name"},
|
||||
online: %Schema{type: :boolean, description: "Online status"},
|
||||
corporation_id: %Schema{type: :integer, description: "Corporation ID"},
|
||||
corporation_name: %Schema{type: :string, description: "Corporation name"},
|
||||
corporation_ticker: %Schema{type: :string, description: "Corporation ticker"},
|
||||
alliance_id: %Schema{type: :integer, description: "Alliance ID"},
|
||||
alliance_name: %Schema{type: :string, description: "Alliance name"},
|
||||
alliance_ticker: %Schema{type: :string, description: "Alliance ticker"},
|
||||
solar_system_id: %Schema{type: :integer, description: "Current solar system ID"},
|
||||
ship: %Schema{type: :integer, description: "Current ship type ID"},
|
||||
ship_name: %Schema{type: :string, description: "Current ship name"},
|
||||
inserted_at: %Schema{type: :string, format: :date_time, description: "Creation timestamp"},
|
||||
updated_at: %Schema{type: :string, format: :date_time, description: "Last update timestamp"}
|
||||
},
|
||||
required: ~w(eve_id name)a
|
||||
}
|
||||
end
|
||||
|
||||
# Common system schema based on what we've seen in controllers
|
||||
def solar_system_basic_schema do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_id: %Schema{type: :integer},
|
||||
solar_system_name: %Schema{type: :string},
|
||||
region_id: %Schema{type: :integer},
|
||||
region_name: %Schema{type: :string},
|
||||
constellation_id: %Schema{type: :integer},
|
||||
constellation_name: %Schema{type: :string},
|
||||
security: %Schema{type: :string}
|
||||
},
|
||||
required: ["solar_system_id", "solar_system_name"]
|
||||
}
|
||||
end
|
||||
|
||||
# Map schema with common fields
|
||||
def map_basic_schema do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
slug: %Schema{type: :string},
|
||||
description: %Schema{type: :string},
|
||||
owner_id: %Schema{type: :string},
|
||||
inserted_at: %Schema{type: :string, format: :date_time},
|
||||
updated_at: %Schema{type: :string, format: :date_time}
|
||||
},
|
||||
required: ["id", "name", "slug"]
|
||||
}
|
||||
end
|
||||
|
||||
# License schema
|
||||
def license_schema do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
license_key: %Schema{type: :string},
|
||||
is_valid: %Schema{type: :boolean},
|
||||
expire_at: %Schema{type: :string, format: :date_time},
|
||||
map_id: %Schema{type: :string}
|
||||
},
|
||||
required: ["id", "license_key", "is_valid", "map_id"]
|
||||
}
|
||||
end
|
||||
|
||||
# Access list schema
|
||||
def access_list_schema do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
description: %Schema{type: :string},
|
||||
owner_id: %Schema{type: :string},
|
||||
api_key: %Schema{type: :string},
|
||||
inserted_at: %Schema{type: :string, format: :date_time},
|
||||
updated_at: %Schema{type: :string, format: :date_time}
|
||||
},
|
||||
required: ["id", "name"]
|
||||
}
|
||||
end
|
||||
|
||||
# Access list member schema
|
||||
def access_list_member_schema do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
role: %Schema{type: :string},
|
||||
eve_character_id: %Schema{type: :string},
|
||||
eve_corporation_id: %Schema{type: :string},
|
||||
eve_alliance_id: %Schema{type: :string},
|
||||
inserted_at: %Schema{type: :string, format: :date_time},
|
||||
updated_at: %Schema{type: :string, format: :date_time}
|
||||
},
|
||||
required: ["id", "name", "role"]
|
||||
}
|
||||
end
|
||||
|
||||
# Common paginated response wrapper
|
||||
def paginated_response(items_schema) do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: items_schema,
|
||||
pagination: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
page: %Schema{type: :integer},
|
||||
page_size: %Schema{type: :integer},
|
||||
total_pages: %Schema{type: :integer},
|
||||
total_count: %Schema{type: :integer}
|
||||
},
|
||||
required: ["page", "page_size", "total_count"]
|
||||
}
|
||||
},
|
||||
required: ["data", "pagination"]
|
||||
}
|
||||
end
|
||||
end
|
||||
114
lib/wanderer_app_web/schemas/response_schemas.ex
Normal file
114
lib/wanderer_app_web/schemas/response_schemas.ex
Normal file
@@ -0,0 +1,114 @@
|
||||
defmodule WandererAppWeb.Schemas.ResponseSchemas do
|
||||
@moduledoc """
|
||||
Standard response schema definitions for API responses.
|
||||
|
||||
This module provides helper functions to create standardized
|
||||
HTTP response schemas for OpenAPI documentation.
|
||||
"""
|
||||
|
||||
alias WandererAppWeb.Schemas.ApiSchemas
|
||||
|
||||
# Standard response status codes
|
||||
def ok(schema, description \\ "Successful operation") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
schema
|
||||
}
|
||||
end
|
||||
|
||||
def created(schema, description \\ "Resource created") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
schema
|
||||
}
|
||||
end
|
||||
|
||||
def bad_request(description \\ "Bad request") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
ApiSchemas.error_response(description)
|
||||
}
|
||||
end
|
||||
|
||||
def not_found(description \\ "Resource not found") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
ApiSchemas.error_response(description)
|
||||
}
|
||||
end
|
||||
|
||||
def internal_server_error(description \\ "Internal server error") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
ApiSchemas.error_response(description)
|
||||
}
|
||||
end
|
||||
|
||||
def unauthorized(description \\ "Unauthorized") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
ApiSchemas.error_response(description)
|
||||
}
|
||||
end
|
||||
|
||||
def forbidden(description \\ "Forbidden") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
ApiSchemas.error_response(description)
|
||||
}
|
||||
end
|
||||
|
||||
# Helper for common response patterns
|
||||
def standard_responses(success_schema, success_description \\ "Successful operation") do
|
||||
[
|
||||
ok: ok(success_schema, success_description),
|
||||
bad_request: bad_request(),
|
||||
not_found: not_found(),
|
||||
internal_server_error: internal_server_error()
|
||||
]
|
||||
end
|
||||
|
||||
# Helper for create operation responses
|
||||
def create_responses(created_schema, created_description \\ "Resource created") do
|
||||
[
|
||||
created: created(created_schema, created_description),
|
||||
bad_request: bad_request(),
|
||||
internal_server_error: internal_server_error()
|
||||
]
|
||||
end
|
||||
|
||||
# Helper for update operation responses
|
||||
def update_responses(updated_schema, updated_description \\ "Resource updated") do
|
||||
[
|
||||
ok: ok(updated_schema, updated_description),
|
||||
bad_request: bad_request(),
|
||||
not_found: not_found(),
|
||||
internal_server_error: internal_server_error()
|
||||
]
|
||||
end
|
||||
|
||||
# Helper for delete operation responses
|
||||
def delete_responses(deleted_schema \\ nil, deleted_description \\ "Resource deleted") do
|
||||
if deleted_schema do
|
||||
[
|
||||
ok: ok(deleted_schema, deleted_description),
|
||||
not_found: not_found(),
|
||||
internal_server_error: internal_server_error()
|
||||
]
|
||||
else
|
||||
[
|
||||
no_content:
|
||||
{deleted_description <> " (no content)", nil, nil},
|
||||
not_found: not_found(),
|
||||
internal_server_error: internal_server_error()
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.62.3"
|
||||
@version "1.65.3"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
915
priv/posts/2025/05-07-systems-connections-api.md
Normal file
915
priv/posts/2025/05-07-systems-connections-api.md
Normal file
@@ -0,0 +1,915 @@
|
||||
%{
|
||||
title: "Guide: Systems and Connections API",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/03-06-systems/api-endpoints.png",
|
||||
tags: ~w(api map systems connections documentation),
|
||||
description: "Detailed guide for Wanderer's systems and connections API endpoints, including batch operations, updates, and deletions."
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
# Guide to Wanderer's Systems and Connections API
|
||||
|
||||
## Introduction
|
||||
|
||||
This guide covers Wanderer's dedicated API endpoints for managing systems and connections on your maps. These endpoints provide fine-grained control over individual systems and connections, as well as batch operations for efficient updates.
|
||||
|
||||
With these APIs, you can:
|
||||
|
||||
- Create, update, and delete individual systems
|
||||
- Create, update, and delete individual connections
|
||||
- Perform batch operations on systems and connections
|
||||
- Query system and connection details
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints require a Map API Token, which you can generate in your map settings. Pass the token in the Authorization header:
|
||||
|
||||
```bash
|
||||
Authorization: Bearer <YOUR_MAP_TOKEN>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Systems Endpoints
|
||||
|
||||
### 1. List Systems
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/systems
|
||||
```
|
||||
|
||||
- **Description:** Retrieves all systems and their connections for the specified map.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"systems": [
|
||||
{
|
||||
"id": "<SYSTEM_UUID>",
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"position_x": 100.5,
|
||||
"position_y": 200.3,
|
||||
"status": "clear",
|
||||
"visible": true,
|
||||
"description": "Trade hub",
|
||||
"tag": "TRADE",
|
||||
"locked": false,
|
||||
"labels": ["market", "highsec"],
|
||||
"map_id": "<MAP_UUID>"
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"id": "<CONNECTION_UUID>",
|
||||
"solar_system_source": 30000142,
|
||||
"solar_system_target": 30000144,
|
||||
"type": 0,
|
||||
"mass_status": 0,
|
||||
"time_status": 0,
|
||||
"ship_size_type": 1,
|
||||
"wormhole_type": "K162",
|
||||
"count_of_passage": 0,
|
||||
"locked": false,
|
||||
"custom_info": "Fresh hole"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Show Single System
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/systems/:id
|
||||
```
|
||||
|
||||
- **Description:** Retrieves details for a specific system.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the system's solar_system_id.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems/30000142"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<SYSTEM_UUID>",
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"position_x": 100.5,
|
||||
"position_y": 200.3,
|
||||
"status": "clear",
|
||||
"visible": true,
|
||||
"description": "Trade hub",
|
||||
"tag": "TRADE",
|
||||
"locked": false,
|
||||
"labels": ["market", "highsec"],
|
||||
"map_id": "<MAP_UUID>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create/Update System
|
||||
|
||||
```bash
|
||||
POST /api/maps/:map_identifier/systems
|
||||
PUT /api/maps/:map_identifier/systems/:id
|
||||
```
|
||||
|
||||
- **Description:** Creates a new system or updates an existing one.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required for PUT) — the system's solar_system_id.
|
||||
|
||||
#### Example Create Request
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"position_x": 100.5,
|
||||
"position_y": 200.3,
|
||||
"status": "clear",
|
||||
"visible": true,
|
||||
"description": "Trade hub",
|
||||
"tag": "TRADE",
|
||||
"locked": false,
|
||||
"labels": ["market", "highsec"]
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||
```
|
||||
|
||||
#### Example Update Request
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"status": "hostile",
|
||||
"description": "Hostiles reported",
|
||||
"tag": "DANGER"
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems/30000142"
|
||||
```
|
||||
|
||||
### 4. Delete System
|
||||
|
||||
```bash
|
||||
DELETE /api/maps/:map_identifier/systems/:id
|
||||
```
|
||||
|
||||
- **Description:** Deletes a specific system and its associated connections.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the system's solar_system_id.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems/30000142"
|
||||
```
|
||||
|
||||
### 5. Batch Delete Systems
|
||||
|
||||
```bash
|
||||
DELETE /api/maps/:map_identifier/systems
|
||||
```
|
||||
|
||||
- **Description:** Deletes multiple systems and their connections in a single operation.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"system_ids": [30000142, 30000144, 30000145]
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connections Endpoints
|
||||
|
||||
### 1. List Connections
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/connections
|
||||
```
|
||||
|
||||
- **Description:** Retrieves all connections for the specified map.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/connections"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "<CONNECTION_UUID>",
|
||||
"solar_system_source": 30000142,
|
||||
"solar_system_target": 30000144,
|
||||
"type": 0,
|
||||
"mass_status": 0,
|
||||
"time_status": 0,
|
||||
"ship_size_type": 1,
|
||||
"wormhole_type": "K162",
|
||||
"count_of_passage": 0,
|
||||
"locked": false,
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create Connection
|
||||
|
||||
```bash
|
||||
POST /api/maps/:map_identifier/connections
|
||||
```
|
||||
|
||||
- **Description:** Creates a new connection between two systems.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"solar_system_source": 30000142,
|
||||
"solar_system_target": 30000144,
|
||||
"type": 0,
|
||||
"mass_status": 0,
|
||||
"time_status": 0,
|
||||
"ship_size_type": 1,
|
||||
"locked": false,
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/connections"
|
||||
```
|
||||
|
||||
### 3. Update Connection
|
||||
|
||||
```bash
|
||||
PATCH /api/maps/:map_identifier/connections
|
||||
```
|
||||
|
||||
- **Description:** Updates an existing connection's properties.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- Query parameters:
|
||||
- `solar_system_source` (required) — source system ID
|
||||
- `solar_system_target` (required) — target system ID
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X PATCH \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"mass_status": 1,
|
||||
"time_status": 1,
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/connections?solar_system_source=30000142&solar_system_target=30000144"
|
||||
```
|
||||
|
||||
### 4. Delete Connection
|
||||
|
||||
```bash
|
||||
DELETE /api/maps/:map_identifier/connections
|
||||
```
|
||||
|
||||
- **Description:** Deletes a connection between two systems.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- Query parameters:
|
||||
- `solar_system_source` (required) — source system ID
|
||||
- `solar_system_target` (required) — target system ID
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/connections?solar_system_source=30000142&solar_system_target=30000144"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Batch Operations
|
||||
|
||||
### 1. Batch Upsert Systems and Connections
|
||||
|
||||
```bash
|
||||
POST /api/maps/:map_identifier/systems
|
||||
```
|
||||
|
||||
- **Description:** Creates or updates multiple systems and connections in a single operation.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"systems": [
|
||||
{
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"position_x": 100.5,
|
||||
"position_y": 200.3,
|
||||
"status": "clear"
|
||||
},
|
||||
{
|
||||
"solar_system_id": 30000144,
|
||||
"solar_system_name": "Perimeter",
|
||||
"position_x": 150.5,
|
||||
"position_y": 250.3,
|
||||
"status": "clear"
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"solar_system_source": 30000142,
|
||||
"solar_system_target": 30000144,
|
||||
"type": 0,
|
||||
"mass_status": 0,
|
||||
"ship_size_type": 1
|
||||
}
|
||||
]
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"systems": {
|
||||
"created": 2,
|
||||
"updated": 0
|
||||
},
|
||||
"connections": {
|
||||
"created": 1,
|
||||
"updated": 0,
|
||||
"deleted": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The response includes counts for:
|
||||
- Systems created and updated
|
||||
- Connections created, updated, and deleted (if any)
|
||||
|
||||
Note: The `deleted` count in connections will be 0 for batch operations as deletion is handled through separate endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Backup and Restore Map State
|
||||
|
||||
We provide a utility script that demonstrates how to use these endpoints to backup and restore your map state:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup_restore_test.sh
|
||||
|
||||
# 1. Backup current state
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems" \
|
||||
> map_backup.json
|
||||
|
||||
# 2. Delete everything (after confirmation)
|
||||
read -p "Delete all systems? (y/N) " confirm
|
||||
if [[ "$confirm" =~ ^[Yy]$ ]]; then
|
||||
# Get system IDs
|
||||
systems=$(cat map_backup.json | jq -r '.data.systems[].solar_system_id')
|
||||
|
||||
# Create deletion payload
|
||||
payload=$(jq -n --argjson ids "$(echo "$systems" | jq -R . | jq -s .)" \
|
||||
'{system_ids: $ids}')
|
||||
|
||||
# Delete all systems
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||
fi
|
||||
|
||||
# 3. Restore from backup (after confirmation)
|
||||
read -p "Restore from backup? (y/N) " confirm
|
||||
if [[ "$confirm" =~ ^[Yy]$ ]]; then
|
||||
# Extract systems and connections
|
||||
backup_data=$(cat map_backup.json)
|
||||
systems=$(echo "$backup_data" | jq '.data.systems')
|
||||
connections=$(echo "$backup_data" | jq '.data.connections')
|
||||
|
||||
# Create restore payload
|
||||
payload="{\"systems\": $systems, \"connections\": $connections}"
|
||||
|
||||
# Restore everything
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||
fi
|
||||
```
|
||||
|
||||
This script demonstrates a practical application of the batch operations endpoints for backing up and restoring map data.
|
||||
|
||||
---
|
||||
|
||||
## Structures Endpoints
|
||||
|
||||
### 1. List Structures
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/structures
|
||||
```
|
||||
|
||||
- **Description:** Retrieves all structures for the specified map.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/structures"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "<STRUCTURE_UUID>",
|
||||
"system_id": "<SYSTEM_UUID>",
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"structure_type_id": "35832",
|
||||
"structure_type": "Astrahus",
|
||||
"character_eve_id": "123456789",
|
||||
"name": "Jita Trade Hub",
|
||||
"notes": "Main market structure",
|
||||
"owner_name": "Wanderer Corp",
|
||||
"owner_ticker": "WANDR",
|
||||
"owner_id": "corp-uuid-1",
|
||||
"status": "anchoring",
|
||||
"end_time": "2025-05-01T12:00:00Z",
|
||||
"inserted_at": "2025-04-30T10:00:00Z",
|
||||
"updated_at": "2025-04-30T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Show Structure
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/structures/:id
|
||||
```
|
||||
|
||||
- **Description:** Retrieves details for a specific structure.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the structure's UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/structures/<STRUCTURE_UUID>"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<STRUCTURE_UUID>",
|
||||
"system_id": "<SYSTEM_UUID>",
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"structure_type_id": "35832",
|
||||
"structure_type": "Astrahus",
|
||||
"character_eve_id": "123456789",
|
||||
"name": "Jita Trade Hub",
|
||||
"notes": "Main market structure",
|
||||
"owner_name": "Wanderer Corp",
|
||||
"owner_ticker": "WANDR",
|
||||
"owner_id": "corp-uuid-1",
|
||||
"status": "anchoring",
|
||||
"end_time": "2025-05-01T12:00:00Z",
|
||||
"inserted_at": "2025-04-30T10:00:00Z",
|
||||
"updated_at": "2025-04-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Structure
|
||||
|
||||
```bash
|
||||
POST /api/maps/:map_identifier/structures
|
||||
```
|
||||
|
||||
- **Description:** Creates a new structure.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"structure_type_id": "35832",
|
||||
"structure_type": "Astrahus",
|
||||
"character_eve_id": "123456789",
|
||||
"name": "Jita Trade Hub",
|
||||
"notes": "Main market structure",
|
||||
"owner_name": "Wanderer Corp",
|
||||
"owner_ticker": "WANDR",
|
||||
"owner_id": "corp-uuid-1",
|
||||
"status": "anchoring",
|
||||
"end_time": "2025-05-01T12:00:00Z"
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/structures"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<STRUCTURE_UUID>",
|
||||
"system_id": "<SYSTEM_UUID>",
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"structure_type_id": "35832",
|
||||
"structure_type": "Astrahus",
|
||||
"character_eve_id": "123456789",
|
||||
"name": "Jita Trade Hub",
|
||||
"notes": "Main market structure",
|
||||
"owner_name": "Wanderer Corp",
|
||||
"owner_ticker": "WANDR",
|
||||
"owner_id": "corp-uuid-1",
|
||||
"status": "anchoring",
|
||||
"end_time": "2025-05-01T12:00:00Z",
|
||||
"inserted_at": "2025-04-30T10:00:00Z",
|
||||
"updated_at": "2025-04-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update Structure
|
||||
|
||||
```bash
|
||||
PUT /api/maps/:map_identifier/structures/:id
|
||||
```
|
||||
|
||||
- **Description:** Updates an existing structure.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the structure's UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"status": "anchored",
|
||||
"notes": "Updated via API"
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/structures/<STRUCTURE_UUID>"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<STRUCTURE_UUID>",
|
||||
"status": "anchored",
|
||||
"notes": "Updated via API"
|
||||
// ... other fields ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Delete Structure
|
||||
|
||||
```bash
|
||||
DELETE /api/maps/:map_identifier/structures/:id
|
||||
```
|
||||
|
||||
- **Description:** Deletes a specific structure.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the structure's UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/structures/<STRUCTURE_UUID>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signatures Endpoints
|
||||
|
||||
### 1. List Signatures
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/signatures
|
||||
```
|
||||
|
||||
- **Description:** Retrieves all signatures for the specified map.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/signatures"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "<SIGNATURE_UUID>",
|
||||
"system_id": "<SYSTEM_UUID>",
|
||||
"eve_id": "ABC-123",
|
||||
"name": "Wormhole K162",
|
||||
"description": "Leads to unknown space",
|
||||
"type": "Wormhole",
|
||||
"linked_system_id": 30000144,
|
||||
"kind": "cosmic_signature",
|
||||
"group": "wormhole",
|
||||
"custom_info": "Fresh",
|
||||
"solar_system_id": 31001394,
|
||||
"solar_system_name": "J214811",
|
||||
"character_eve_id": "123456789",
|
||||
"inserted_at": "2025-04-30T10:00:00Z",
|
||||
"updated_at": "2025-04-30T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Show Signature
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/signatures/:id
|
||||
```
|
||||
|
||||
- **Description:** Retrieves details for a specific signature.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the signature's UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/signatures/<SIGNATURE_UUID>"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<SIGNATURE_UUID>",
|
||||
"system_id": "<SYSTEM_UUID>",
|
||||
"eve_id": "ABC-123",
|
||||
"name": "Wormhole K162",
|
||||
"description": "Leads to unknown space",
|
||||
"type": "Wormhole",
|
||||
"linked_system_id": 30000144,
|
||||
"kind": "cosmic_signature",
|
||||
"group": "wormhole",
|
||||
"custom_info": "Fresh",
|
||||
"solar_system_id": 31001394,
|
||||
"solar_system_name": "J214811",
|
||||
"character_eve_id": "123456789",
|
||||
"inserted_at": "2025-04-30T10:00:00Z",
|
||||
"updated_at": "2025-04-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Signature
|
||||
|
||||
```bash
|
||||
POST /api/maps/:map_identifier/signatures
|
||||
```
|
||||
|
||||
- **Description:** Creates a new signature.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"eve_id": "ABC-123",
|
||||
"name": "Wormhole K162",
|
||||
"description": "Leads to unknown space",
|
||||
"type": "Wormhole",
|
||||
"linked_system_id": 30000144,
|
||||
"kind": "cosmic_signature",
|
||||
"group": "wormhole",
|
||||
"custom_info": "Fresh",
|
||||
"solar_system_id": 31001394,
|
||||
"solar_system_name": "J214811"
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/signatures"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<SIGNATURE_UUID>",
|
||||
"eve_id": "ABC-123",
|
||||
"name": "Wormhole K162",
|
||||
"description": "Leads to unknown space",
|
||||
"type": "Wormhole",
|
||||
"linked_system_id": 30000144,
|
||||
"kind": "cosmic_signature",
|
||||
"group": "wormhole",
|
||||
"custom_info": "Fresh",
|
||||
"solar_system_id": 31001394,
|
||||
"solar_system_name": "J214811",
|
||||
"character_eve_id": "123456789",
|
||||
"inserted_at": "2025-04-30T10:00:00Z",
|
||||
"updated_at": "2025-04-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update Signature
|
||||
|
||||
```bash
|
||||
PUT /api/maps/:map_identifier/signatures/:id
|
||||
```
|
||||
|
||||
- **Description:** Updates an existing signature.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the signature's UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"description": "Updated via API",
|
||||
"custom_info": "Updated info"
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/signatures/<SIGNATURE_UUID>"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<SIGNATURE_UUID>",
|
||||
"description": "Updated via API",
|
||||
"custom_info": "Updated info"
|
||||
// ... other fields ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Delete Signature
|
||||
|
||||
```bash
|
||||
DELETE /api/maps/:map_identifier/signatures/:id
|
||||
```
|
||||
|
||||
- **Description:** Deletes a specific signature.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the signature's UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/signatures/<SIGNATURE_UUID>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
These endpoints provide powerful tools for managing your map's systems and connections programmatically. Key features include:
|
||||
|
||||
1. Individual system and connection management
|
||||
2. Efficient batch operations
|
||||
3. Flexible update options
|
||||
4. Robust error handling
|
||||
5. Consistent response formats
|
||||
|
||||
For the most up-to-date and interactive documentation, remember to check the Swagger UI at `/swaggerui`.
|
||||
|
||||
If you have questions about these endpoints or need assistance, please reach out to the Wanderer Team.
|
||||
|
||||
----
|
||||
|
||||
Fly safe,
|
||||
**The Wanderer Team**
|
||||
|
||||
----
|
||||
70
priv/posts/2025/05-08-signature-deletion-flow.md
Normal file
70
priv/posts/2025/05-08-signature-deletion-flow.md
Normal file
@@ -0,0 +1,70 @@
|
||||
%{
|
||||
title: "Instant Signature Deletion & Undo: A New Flow for Map Signatures",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/05-08-undo/undo.png",
|
||||
tags: ~w(signatures deletion undo map realtime guide),
|
||||
description: "Learn about the new instant signature deletion flow, real-time updates, and the ability to undo removals in Wanderer maps."
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### Introduction
|
||||
|
||||
Managing cosmic signatures is a core part of mapping and navigation in EVE Online. With our latest update, signature deletion is now **instant, real-time, and reversible**—making it easier than ever to keep your map up to date and error-free.
|
||||
|
||||
This guide covers the new signature deletion flow, how to use the undo feature, and what happens behind the scenes to keep your map clean and synchronized for all users.
|
||||
|
||||
---
|
||||
|
||||
### 1. The New User Flow: Instant, Real-Time, Reversible
|
||||
|
||||
- **Delete a signature:** When you remove a signature, it disappears from your map (and all other users' maps) instantly after a server roundtrip.
|
||||
- **Undo:** If you make a mistake, you have a window of up to 30s to undo the deletion
|
||||
|
||||
---
|
||||
|
||||
### 2. How to Use the New Signature Deletion Flow
|
||||
|
||||
1. **Select and Delete:**
|
||||
- Open the system signatures widget.
|
||||
- Select one or more signatures and click delete (or paste and use lazy delete).
|
||||
- The signatures will disappear for all users viewing the same system.
|
||||
|
||||
2. **Undo a Deletion:**
|
||||
- After deleting, an **Undo** button appears for you (the user who deleted the signature) and remains visible based on your timeout settings.
|
||||
- Click **Undo** to restore the removed signatures instantly for all users.
|
||||
- If you don't click Undo in time, the deletion becomes permanent
|
||||
|
||||
3. **Real-Time Updates:**
|
||||
- All users see signature changes (add, update, remove, undo) in real time—no need to refresh.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 4. FAQ & Troubleshooting
|
||||
|
||||
|
||||
**Q: Who sees the Undo button?**
|
||||
- Only the user who deleted the signature sees the Undo button
|
||||
|
||||
**Q: Do all users see the same signature list in real time?**
|
||||
- Yes! All changes are broadcast instantly to everyone viewing the same map.
|
||||
|
||||
**Q: Can I configure the undo timeout?**
|
||||
- Yes, in the user inteface settings for the signatures widget
|
||||
|
||||
**Q: What about performance?**
|
||||
- The new flow is optimized for real-time collaboration and efficient cleanup, ensuring your map stays fast and accurate.
|
||||
|
||||
---
|
||||
|
||||
### 5. Summary
|
||||
|
||||
The new signature deletion flow brings instant, real-time updates and a safety net for accidental removals. Enjoy a more collaborative, error-resistant mapping experience—now live for all Wanderer users!
|
||||
|
||||
---
|
||||
|
||||
Fly safe,
|
||||
**The Wanderer Team**
|
||||
|
||||
---
|
||||
77
priv/posts/2025/05-11-map-active-characters.md
Normal file
77
priv/posts/2025/05-11-map-active-characters.md
Normal file
@@ -0,0 +1,77 @@
|
||||
%{
|
||||
title: "Map Active Characters Page — Interface Guide",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/2025/05-11-map-active-characters/cover.png",
|
||||
tags: ~w(characters interface guide map security),
|
||||
description: "This interface is essential for managing access and tracking behavior on shared maps, especially in large organizations."
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### Introduction
|
||||
|
||||

|
||||

|
||||
|
||||
This page displays **only currently active characters** — those who have the map page open in an active browser tab or window.
|
||||
|
||||
### Key Use Cases:
|
||||
- Identify active pilots on your map
|
||||
- Monitor user activity and access level
|
||||
- Manage tracked status to stay within subscription limits
|
||||
|
||||
---
|
||||
|
||||
## 👤 Character Grouping by User
|
||||
|
||||
Each user may have multiple EVE Online characters authorized. On this page:
|
||||
- Characters are **grouped under their owning user**
|
||||
- Admins can easily see which characters belong to the same person
|
||||
- Useful for distinguishing between multiboxers or corp mates sharing access
|
||||
|
||||
---
|
||||
|
||||
## 📋 Character Info Displayed
|
||||
|
||||

|
||||
|
||||
Each tracked character on this page includes:
|
||||
|
||||
| Field | Description |
|
||||
|--------------------|-----------------------------------------------------------------------------|
|
||||
| **Active From** | Timestamp indicating when the character opened the map (based on real-time browser tab activity) |
|
||||
| **Character Info** | Character Name, Corporation, and Alliance |
|
||||
| **Tracked Status** | Whether the character is being actively tracked on the map |
|
||||
| **Online Status** | Online/offline status (in-game) |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Admin Actions
|
||||
|
||||
Map **owners and administrators** can:
|
||||
|
||||
- ✅ **See viewer-only access**: Identify characters who can view but are not being tracked.
|
||||
- 🚫 **Force Untrack** characters:
|
||||
- Stop tracking & remove characters from map.
|
||||
- Useful to stay within your character limit or reset tracking manually.
|
||||
- Note: The user should re-enable tracking later if needed manually in map tracking settings.
|
||||
|
||||
---
|
||||
|
||||
## 🛑 Notes & Recommendations
|
||||
|
||||
- A character is **counted toward the Characters Limit** of the map's subscription **only when tracked**.
|
||||
- Tracking begins **as soon as the character opens the map page** in any tab/browser.
|
||||
- Closing all tabs with the map **automatically stops tracking** for that character (after a small period about 15 minutes).
|
||||
- This system ensures you always have a live (tracking data automatically updated every 30 seconds), accurate picture of map usage across your team.
|
||||
|
||||
---
|
||||
|
||||
By using the **Map Active Characters** page, admins can efficiently manage map activity, maintain security, and optimize performance across their team or alliance.
|
||||
|
||||
---
|
||||
|
||||
Fly safe,
|
||||
**The Wanderer Team**
|
||||
|
||||
---
|
||||
@@ -340,7 +340,7 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
|
||||
471,23,Corporate Hangar Array,0,1,0,1,0,1
|
||||
472,7,System Scanner,0,0,0,0,0,1
|
||||
473,23,Tracking Array,0,1,0,1,0,1
|
||||
474,17,Acceleration Gate Keys,0,0,0,0,0,1
|
||||
474,17,Acceleration Gate Keys,0,1,0,0,0,1
|
||||
475,7,Microwarpdrive,96,0,0,0,0,0
|
||||
476,8,XL Torpedo,1349,0,0,0,1,1
|
||||
477,9,Mining Barge Blueprint,0,1,0,0,0,1
|
||||
@@ -1010,7 +1010,7 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
|
||||
1405,65,Laboratory,None,0,0,0,0,0
|
||||
1406,65,Refinery,None,0,1,0,0,1
|
||||
1407,65,Observatory Array,None,0,0,0,0,0
|
||||
1408,65,Upwell Jump Gate,None,0,1,0,0,1
|
||||
1408,65,Upwell Jump Bridge,None,0,1,0,0,1
|
||||
1409,65,Administration Hub,None,0,0,0,0,0
|
||||
1410,65,Advertisement Center,None,0,0,0,0,0
|
||||
1411,11,Amarr Navy Roaming Cruiser,None,0,0,0,0,0
|
||||
@@ -1295,7 +1295,7 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
|
||||
1924,65,♦ Stronghold,None,0,0,0,0,0
|
||||
1925,11,Irregular Industrial Command Ship,None,0,0,0,0,0
|
||||
1926,11,Irregular Freighter,None,0,0,0,0,0
|
||||
1927,11,Irregular Structure,None,0,0,0,0,0
|
||||
1927,11,Irregular Structure,None,0,1,0,0,0
|
||||
1928,11,Irregular Container,None,0,0,0,0,0
|
||||
1929,11,Irregular - Unidentified,None,0,0,0,0,0
|
||||
1933,66,Structure Composite Reactor Rig M - TE,None,0,0,0,0,1
|
||||
@@ -1531,6 +1531,10 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
|
||||
4821,17,Atavum,None,1,0,0,0,1
|
||||
4824,17,Infomorph Systems,None,1,0,0,0,1
|
||||
4825,2,Local Beacon,None,0,1,0,0,0
|
||||
4827,17,EDENCOM Data,None,1,0,0,0,1
|
||||
4828,2,Pirate Spawners,None,0,0,0,0,0
|
||||
4843,17,Limited Rarities,None,1,0,0,0,1
|
||||
4857,25,Tyranite,15,0,1,0,0,1
|
||||
350858,350001,Infantry Weapons,None,1,0,0,0,0
|
||||
351064,350001,Infantry Dropsuits,None,1,0,0,0,0
|
||||
351121,350001,Infantry Modules,None,1,0,0,0,0
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -5431,12 +5431,12 @@ regionID,constellationID,solarSystemID,solarSystemName,x,y,z,xMin,xMax,yMin,yMax
|
||||
10000069,20000782,30045353,Pynekastoh,-218294709389294016.0000000000,57890482798287904.0000000000,108746979052662000.0000000000,-215460829363407008.0000000000,-215443912092723008.0000000000,58478339404338496.0000000000,58495256675023104.0000000000,-107169521884028000.0000000000,-107152604613344000.0000000000,1.1560000000,0,0,0,0,0,0,None,0.2396328126,500001,710889342216.0000000000,45047,None
|
||||
10000069,20000782,30045354,Reitsato,-197894305085120992.0000000000,65621164096811400.0000000000,125071865404206000.0000000000,-192367367542551008.0000000000,-192346438189638016.0000000000,62054618448878800.0000000000,62075547801792096.0000000000,-129051005274330000.0000000000,-129030075921416992.0000000000,0.0106400000,0,0,0,0,0,0,None,0.1895104797,500001,1757422438016.0000000000,8,None
|
||||
10001000,20010000,30100000,Zarzakh,4732782451200000.0000000000,2722598544370000.0000000000,-1508346782640000.0000000000,4732781451210000.0000000000,4732783451210000.0000000000,2722597544370000.0000000000,2722599544370000.0000000000,-1508347782640000.0000000000,-1508345782640000.0000000000,0E-10,0,0,0,0,0,0,None,-1.0000000000,500029,3112788232450.0000000000,3796,None
|
||||
11000033,21000334,31000001,J055520,6824391110928870400.0000000000,1700713810475010048.0000000000,-9808807909109499904.0000000000,6824376151141799936.0000000000,6824406070715939840.0000000000,1700698850687940096.0000000000,1700728770262080000.0000000000,-9808822868896569344.0000000000,-9808792949322430464.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000002,J110145,6862328572493299712.0000000000,1716315417584930048.0000000000,-9835131116484100096.0000000000,6862313612706230272.0000000000,6862343532280380416.0000000000,1716300457797860096.0000000000,1716330377372000000.0000000000,-9835146076271169536.0000000000,-9835116156697030656.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000003,J164710,6800690715289709568.0000000000,1667864493217910016.0000000000,-9815994309006979072.0000000000,6800675755502640128.0000000000,6800705675076780032.0000000000,1667849533430840064.0000000000,1667879453004979968.0000000000,-9816009268794050560.0000000000,-9815979349219909632.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000004,J200727,6827372579272930304.0000000000,1725234432784110080.0000000000,-9821296571091089408.0000000000,6827357619485859840.0000000000,6827387539059999744.0000000000,1725219472997040128.0000000000,1725249392571180032.0000000000,-9821311530878160896.0000000000,-9821281611304019968.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000001,J055520,6824391110928870400.0000000000,1700713810475010048.0000000000,-9808807909109499904.0000000000,6824376151141799936.0000000000,6824406070715939840.0000000000,1700698850687940096.0000000000,1700728770262080000.0000000000,-9808822868896569344.0000000000,-9808792949322430464.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,500027,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000002,J110145,6862328572493299712.0000000000,1716315417584930048.0000000000,-9835131116484100096.0000000000,6862313612706230272.0000000000,6862343532280380416.0000000000,1716300457797860096.0000000000,1716330377372000000.0000000000,-9835146076271169536.0000000000,-9835116156697030656.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,500002,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000003,J164710,6800690715289709568.0000000000,1667864493217910016.0000000000,-9815994309006979072.0000000000,6800675755502640128.0000000000,6800705675076780032.0000000000,1667849533430840064.0000000000,1667879453004979968.0000000000,-9816009268794050560.0000000000,-9815979349219909632.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,500003,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000004,J200727,6827372579272930304.0000000000,1725234432784110080.0000000000,-9821296571091089408.0000000000,6827357619485859840.0000000000,6827387539059999744.0000000000,1725219472997040128.0000000000,1725249392571180032.0000000000,-9821311530878160896.0000000000,-9821281611304019968.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,500001,14959787070000.0000000000,34331,None
|
||||
11000031,21000324,31000005,Thera,7201177000000000000.0000000000,1534300000000000000.0000000000,-9501332482538399744.0000000000,7321174000000000000.0000000000,7321180000000000000.0000000000,1533300000000000000.0000000000,1535300000000000000.0000000000,-9394335000000000000.0000000000,-9394329000000000000.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,3112788232446.8598632812,34331,None
|
||||
11000033,21000334,31000006,J174618,6851740425709679616.0000000000,1679294718115650048.0000000000,-9788179393983350784.0000000000,6851725465922610176.0000000000,6851755385496760320.0000000000,1679279758328580096.0000000000,1679309677902720000.0000000000,-9788194353770420224.0000000000,-9788164434196279296.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000006,J174618,6851740425709679616.0000000000,1679294718115650048.0000000000,-9788179393983350784.0000000000,6851725465922610176.0000000000,6851755385496760320.0000000000,1679279758328580096.0000000000,1679309677902720000.0000000000,-9788194353770420224.0000000000,-9788164434196279296.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,500026,14959787070000.0000000000,34331,None
|
||||
11000001,21000311,31000007,J105443,7644843937233080320.0000000000,-5747794381266180.0000000000,-9482937421590790144.0000000000,7644840278395700224.0000000000,7644847596070469632.0000000000,-5751453218652410.0000000000,-5744135543879960.0000000000,9482933762753400832.0000000000,9482941080428179456.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,3658837386225.7099609375,45038,None
|
||||
11000001,21000311,31000008,J100744,7646115934669320192.0000000000,45486224544419904.0000000000,-9479841645153390592.0000000000,7646114732911429632.0000000000,7646117136427200512.0000000000,45485022786539904.0000000000,45487426302299800.0000000000,9479840443395510272.0000000000,9479842846911270912.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,1201757879938.6899414062,45033,None
|
||||
11000001,21000311,31000009,J225046,7639560557040199680.0000000000,-15009347794601900.0000000000,-9477365158238810112.0000000000,7639551529002940416.0000000000,7639569585077449728.0000000000,-15018375831855400.0000000000,-15000319757348400.0000000000,9477356130201550848.0000000000,9477374186276059136.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,9028037253510.2304687500,45038,None
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -0,0 +1,23 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddDeletedSignature do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:map_system_signatures_v1) do
|
||||
remove :updated
|
||||
add :deleted, :boolean, null: false, default: false
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:map_system_signatures_v1) do
|
||||
remove :deleted
|
||||
add :updated, :bigint
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,197 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "eve_id",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "character_eve_id",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "type",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "linked_system_id",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "kind",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "group",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "custom_info",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "deleted",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "map_system_signatures_v1_system_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "map_system_v1"
|
||||
},
|
||||
"size": null,
|
||||
"source": "system_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "63D26445C9E67459C4D41CF31D61C3EE2356BE664F0D44AB5BC04C2100B701F3",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "map_system_signatures_v1_uniq_system_eve_id_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "system_id"
|
||||
},
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "eve_id"
|
||||
}
|
||||
],
|
||||
"name": "uniq_system_eve_id",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "map_system_signatures_v1"
|
||||
}
|
||||
163
test/manual/api/map_api_backup_restore_test.sh
Executable file
163
test/manual/api/map_api_backup_restore_test.sh
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/bin/bash
|
||||
# test/manual/api/backup_restore_test.sh
|
||||
# ─── Backup and Restore Test for Map Systems and Connections ────────────────────────
|
||||
#
|
||||
# Usage:
|
||||
# ./backup_restore_test.sh # Run with default settings
|
||||
# ./backup_restore_test.sh -v # Run in verbose mode
|
||||
# ./backup_restore_test.sh -h # Show help
|
||||
#
|
||||
source "$(dirname "$0")/utils.sh"
|
||||
|
||||
# Set to "true" to see detailed output, "false" for minimal output
|
||||
VERBOSE=${VERBOSE:-false}
|
||||
|
||||
# Parse command line options
|
||||
while getopts "vh" opt; do
|
||||
case $opt in
|
||||
v)
|
||||
VERBOSE=true
|
||||
;;
|
||||
h)
|
||||
echo "Usage: $0 [-v] [-h]"
|
||||
echo " -v Verbose mode (show detailed output)"
|
||||
echo " -h Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
echo "Use -h for help"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
shift $((OPTIND-1))
|
||||
|
||||
# File to store backup data
|
||||
BACKUP_FILE="/tmp/wanderer_map_backup.json"
|
||||
|
||||
# ─── UTILITY FUNCTIONS ─────────────────────────────────────────────────────
|
||||
|
||||
# Function to backup current map state
|
||||
backup_map_state() {
|
||||
echo "==== Backing Up Map State ===="
|
||||
|
||||
echo "Fetching current map state..."
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local response=$(parse_response "$raw")
|
||||
echo "$response" > "$BACKUP_FILE"
|
||||
|
||||
local system_count=$(echo "$response" | jq '.data.systems | length')
|
||||
local conn_count=$(echo "$response" | jq '.data.connections | length')
|
||||
|
||||
echo "✅ Backed up $system_count systems and $conn_count connections to $BACKUP_FILE"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Backup data:" && cat "$BACKUP_FILE" | jq '.'
|
||||
return 0
|
||||
else
|
||||
echo "❌ Failed to backup map state. Status: $status"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to delete all systems (which will cascade to connections)
|
||||
delete_all() {
|
||||
echo "==== Deleting All Systems ===="
|
||||
|
||||
# Get current systems
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local response=$(parse_response "$raw")
|
||||
local system_ids=$(echo "$response" | jq -r '.data.systems[].solar_system_id')
|
||||
|
||||
if [ -z "$system_ids" ]; then
|
||||
echo "No systems to delete."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Convert system IDs to JSON array and create payload
|
||||
local system_ids_json=$(echo "$system_ids" | jq -R . | jq -s .)
|
||||
local payload=$(jq -n --argjson system_ids "$system_ids_json" '{system_ids: $system_ids}')
|
||||
|
||||
# Send batch delete request
|
||||
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Successfully deleted all systems and their connections"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Failed to delete systems. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Failed to fetch systems for deletion. Status: $status"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to restore map state from backup
|
||||
restore_map_state() {
|
||||
echo "==== Restoring Map State ===="
|
||||
|
||||
if [ ! -f "$BACKUP_FILE" ]; then
|
||||
echo "❌ No backup file found at $BACKUP_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local backup_data=$(cat "$BACKUP_FILE")
|
||||
local systems=$(echo "$backup_data" | jq '.data.systems')
|
||||
local connections=$(echo "$backup_data" | jq '.data.connections')
|
||||
|
||||
# Create payload for batch upsert
|
||||
local payload="{\"systems\": $systems, \"connections\": $connections}"
|
||||
|
||||
# Send batch upsert request
|
||||
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local response=$(parse_response "$raw")
|
||||
local systems_created=$(echo "$response" | jq '.data.systems.created')
|
||||
local systems_updated=$(echo "$response" | jq '.data.systems.updated')
|
||||
local conns_created=$(echo "$response" | jq '.data.connections.created')
|
||||
local conns_updated=$(echo "$response" | jq '.data.connections.updated')
|
||||
|
||||
echo "✅ Restore successful:"
|
||||
echo " Systems: $systems_created created, $systems_updated updated"
|
||||
echo " Connections: $conns_created created, $conns_updated updated"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Failed to restore map state. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── MAIN EXECUTION FLOW ─────────────────────────────────────────────────
|
||||
|
||||
echo "Starting backup/restore test sequence..."
|
||||
|
||||
# Step 1: Backup current state
|
||||
backup_map_state || { echo "Backup failed, aborting."; exit 1; }
|
||||
|
||||
echo -e "\nBackup complete. Press Enter to proceed with deletion..."
|
||||
read -r
|
||||
|
||||
# Step 2: Delete everything
|
||||
delete_all || { echo "Deletion failed, aborting."; exit 1; }
|
||||
|
||||
echo -e "\nDeletion complete. Press Enter to proceed with restore..."
|
||||
read -r
|
||||
|
||||
# Step 3: Restore from backup
|
||||
restore_map_state || { echo "Restore failed."; exit 1; }
|
||||
|
||||
echo -e "\nTest sequence completed."
|
||||
exit 0
|
||||
462
test/manual/api/structure_signature_api_tests.sh
Executable file
462
test/manual/api/structure_signature_api_tests.sh
Executable file
@@ -0,0 +1,462 @@
|
||||
#!/bin/bash
|
||||
# test/manual/api/structure_signature_api_tests.sh
|
||||
# ─── Manual API Tests for Map Structure and Signature APIs ────────────────
|
||||
#
|
||||
# Usage:
|
||||
# ./structure_signature_api_tests.sh # Run all tests with menu selection
|
||||
# ./structure_signature_api_tests.sh create # Run only creation tests
|
||||
# ./structure_signature_api_tests.sh update # Run only update tests
|
||||
# ./structure_signature_api_tests.sh delete # Run only deletion tests
|
||||
# ./structure_signature_api_tests.sh -v # Run in verbose mode
|
||||
#
|
||||
source "$(dirname "$0")/utils.sh"
|
||||
|
||||
echo "DEBUG: Script started"
|
||||
|
||||
#set -x # Enable shell debug output
|
||||
|
||||
VERBOSE=${VERBOSE:-false}
|
||||
|
||||
trap 'echo -e "\n❌ ERROR: Script failed at line $LINENO. Last command: $BASH_COMMAND" >&2' ERR
|
||||
|
||||
while getopts "vh" opt; do
|
||||
case $opt in
|
||||
v)
|
||||
VERBOSE=true
|
||||
;;
|
||||
h)
|
||||
echo "Usage: $0 [-v] [-h] [all|create|update|delete]"
|
||||
echo " -v Verbose mode (show detailed test output)"
|
||||
echo " -h Show this help message"
|
||||
echo " all Run all tests (default with menu)"
|
||||
echo " create Run only creation tests"
|
||||
echo " update Run only update tests"
|
||||
echo " delete Run only deletion tests"
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
echo "Use -h for help"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND-1))
|
||||
COMMAND=${1:-"all"}
|
||||
|
||||
STRUCTURES_FILE="/tmp/wanderer_test_structures.txt"
|
||||
SIGNATURES_FILE="/tmp/wanderer_test_signatures.txt"
|
||||
CREATED_STRUCTURE_IDS=""
|
||||
CREATED_SIGNATURE_IDS=""
|
||||
|
||||
save_structures() {
|
||||
echo "DEBUG: Entering save_structures"
|
||||
if ! echo "$CREATED_STRUCTURE_IDS" > "$STRUCTURES_FILE"; then
|
||||
echo "ERROR: Failed to write to $STRUCTURES_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "DEBUG: Successfully wrote to $STRUCTURES_FILE"
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Saved $(wc -w < "$STRUCTURES_FILE") structures to $STRUCTURES_FILE"; fi
|
||||
}
|
||||
load_structures() {
|
||||
if [ -f "$STRUCTURES_FILE" ]; then
|
||||
CREATED_STRUCTURE_IDS=$(cat "$STRUCTURES_FILE")
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Loaded $(wc -w < "$STRUCTURES_FILE") structures from $STRUCTURES_FILE"; fi
|
||||
else
|
||||
CREATED_STRUCTURE_IDS=""
|
||||
fi
|
||||
}
|
||||
save_signatures() {
|
||||
echo "$CREATED_SIGNATURE_IDS" > "$SIGNATURES_FILE"
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Saved $(wc -w < "$SIGNATURES_FILE") signatures to $SIGNATURES_FILE"; fi
|
||||
}
|
||||
load_signatures() {
|
||||
if [ -f "$SIGNATURES_FILE" ]; then
|
||||
CREATED_SIGNATURE_IDS=$(cat "$SIGNATURES_FILE")
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Loaded $(wc -w < "$SIGNATURES_FILE") signatures from $SIGNATURES_FILE"; fi
|
||||
else
|
||||
CREATED_SIGNATURE_IDS=""
|
||||
fi
|
||||
}
|
||||
add_to_list() {
|
||||
local list="$1"
|
||||
local item="$2"
|
||||
if [ -z "$list" ]; then
|
||||
echo "$item"
|
||||
else
|
||||
echo "$list $item"
|
||||
fi
|
||||
}
|
||||
|
||||
# Fetch the first available system (ID and name) from the API
|
||||
get_first_system() {
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local response=$(parse_response "$raw")
|
||||
# Try .data as array
|
||||
local count=$(echo "$response" | jq -er 'if (.data | type == "array") then (.data | length) else 0 end' 2>/dev/null)
|
||||
for i in $(seq 0 $((count-1))); do
|
||||
local uuid=$(echo "$response" | jq -er ".data[$i].id // empty" 2>/dev/null)
|
||||
local eve_id=$(echo "$response" | jq -er ".data[$i].solar_system_id // empty" 2>/dev/null)
|
||||
local name=$(echo "$response" | jq -er ".data[$i].name // .data[$i].solar_system_name // empty" 2>/dev/null)
|
||||
if [[ -n "$uuid" && -n "$eve_id" && -n "$name" ]]; then
|
||||
echo "$uuid:$eve_id:$name"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
# Try .data.systems as array
|
||||
local count2=$(echo "$response" | jq -er 'if (.data.systems | type == "array") then (.data.systems | length) else 0 end' 2>/dev/null)
|
||||
for i in $(seq 0 $((count2-1))); do
|
||||
local uuid=$(echo "$response" | jq -er ".data.systems[$i].id // empty" 2>/dev/null)
|
||||
local eve_id=$(echo "$response" | jq -er ".data.systems[$i].solar_system_id // empty" 2>/dev/null)
|
||||
local name=$(echo "$response" | jq -er ".data.systems[$i].name // .data.systems[$i].solar_system_name // empty" 2>/dev/null)
|
||||
if [[ -n "$uuid" && -n "$eve_id" && -n "$name" ]]; then
|
||||
echo "$uuid:$eve_id:$name"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo "ERROR: No valid system found in API response. Available systems:" >&2
|
||||
echo "$response" | jq '.' >&2
|
||||
exit 1
|
||||
else
|
||||
echo "ERROR: Failed to fetch systems (status $status)" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── STRUCTURE TESTS ─────────────────────────────────────────────
|
||||
create_structure() {
|
||||
local sys_info=$(get_first_system)
|
||||
local system_uuid=$(echo "$sys_info" | cut -d: -f1)
|
||||
local eve_system_id=$(echo "$sys_info" | cut -d: -f2)
|
||||
local system_name=$(echo "$sys_info" | cut -d: -f3-)
|
||||
echo "==== Creating Structure in system $system_name ($eve_system_id, $system_uuid) ===="
|
||||
local payload=$(jq -n --arg sid "$eve_system_id" --arg name "$system_name" '{
|
||||
system_id: "sys-uuid-1",
|
||||
solar_system_name: $name,
|
||||
solar_system_id: ($sid|tonumber),
|
||||
structure_type_id: "35832",
|
||||
structure_type: "Astrahus",
|
||||
character_eve_id: "123456789",
|
||||
name: "Jita Trade Hub",
|
||||
notes: "Main market structure",
|
||||
owner_name: "Wanderer Corp",
|
||||
owner_ticker: "WANDR",
|
||||
owner_id: "corp-uuid-1",
|
||||
status: "anchoring",
|
||||
end_time: "2025-05-05T12:00:00Z"
|
||||
}')
|
||||
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/structures" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local id=$(parse_response "$raw" | jq -r '.data.id')
|
||||
CREATED_STRUCTURE_IDS=$(add_to_list "$CREATED_STRUCTURE_IDS" "$id")
|
||||
echo "✅ Created structure with ID: $id"
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to create structure. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
save_structures
|
||||
echo "DEBUG: End of create_structure, about to return"
|
||||
}
|
||||
|
||||
list_structures() {
|
||||
echo "==== Listing Structures ===="
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/structures")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local count=$(parse_response "$raw" | jq '.data | length')
|
||||
echo "✅ Listed $count structures"
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "$(parse_response "$raw")" | jq '.'; fi
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to list structures. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
show_structure() {
|
||||
load_structures
|
||||
local id=$(echo "$CREATED_STRUCTURE_IDS" | awk '{print $1}')
|
||||
if [ -z "$id" ]; then
|
||||
echo -e "\n❌ ERROR: No structure ID found. Run creation first." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==== Show Structure $id ===="
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/structures/$id")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local data=$(parse_response "$raw")
|
||||
local name=$(echo "$data" | jq -r '.data.name')
|
||||
local status_val=$(echo "$data" | jq -r '.data.status')
|
||||
local notes=$(echo "$data" | jq -r '.data.notes')
|
||||
echo "✅ Showed structure $id: name='$name', status='$status_val', notes='$notes'"
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "$data" | jq '.'; fi
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to show structure $id. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
update_structure() {
|
||||
load_structures
|
||||
local id=$(echo "$CREATED_STRUCTURE_IDS" | awk '{print $1}')
|
||||
if [ -z "$id" ]; then
|
||||
echo -e "\n❌ ERROR: No structure ID found. Run creation first." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==== Updating Structure $id ===="
|
||||
local payload=$(jq -n '{status: "anchored", notes: "Updated via test"}')
|
||||
local raw=$(make_request PUT "$API_BASE_URL/api/maps/$MAP_SLUG/structures/$id" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Updated structure $id"
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to update structure $id. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
delete_structure() {
|
||||
load_structures
|
||||
local id=$(echo "$CREATED_STRUCTURE_IDS" | awk '{print $1}')
|
||||
if [ -z "$id" ]; then
|
||||
echo -e "\n❌ ERROR: No structure ID found. Run creation first." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==== Deleting Structure $id ===="
|
||||
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/structures/$id")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Deleted structure $id"
|
||||
CREATED_STRUCTURE_IDS=""
|
||||
save_structures
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to delete structure $id. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── SIGNATURE TESTS ─────────────────────────────────────────────
|
||||
create_signature() {
|
||||
local sys_info=$(get_first_system)
|
||||
echo "DEBUG: sys_info='$sys_info'"
|
||||
local system_uuid=$(echo "$sys_info" | cut -d: -f1)
|
||||
local system_id=$(echo "$sys_info" | cut -d: -f2)
|
||||
local system_name=$(echo "$sys_info" | cut -d: -f3-)
|
||||
echo "DEBUG: system_id='$system_id' (should be a number like 31001394)"
|
||||
if [[ -z "$system_id" ]]; then
|
||||
echo "ERROR: system_id is empty. sys_info='$sys_info'" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Generate a unique, valid-looking eve_id (e.g., ABC-123)
|
||||
local eve_id=$(cat /dev/urandom | tr -dc 'A-Z' | fold -w 3 | head -n 1)-$(shuf -i 100-999 -n 1)
|
||||
echo "==== Creating Signature in system $system_name ($system_id, $system_uuid) with eve_id $eve_id ===="
|
||||
local payload=$(jq -n --arg sid "$system_id" --arg name "$system_name" --arg eve_id "$eve_id" '{
|
||||
eve_id: $eve_id,
|
||||
name: "Wormhole K162",
|
||||
description: "Leads to unknown space",
|
||||
type: "Wormhole",
|
||||
linked_system_id: 30000144,
|
||||
kind: "cosmic_signature",
|
||||
group: "wormhole",
|
||||
custom_info: "Fresh",
|
||||
solar_system_id: ($sid|tonumber),
|
||||
solar_system_name: $name
|
||||
}')
|
||||
echo "DEBUG: payload=$payload"
|
||||
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/signatures" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
# Now list signatures and find the one with this eve_id
|
||||
local list_raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/signatures")
|
||||
local id=$(parse_response "$list_raw" | jq -r --arg eve_id "$eve_id" '.data[] | select(.eve_id == $eve_id) | .id' | head -n 1)
|
||||
if [[ -z "$id" ]]; then
|
||||
echo "❌ ERROR: Created signature not found in list (eve_id: $eve_id)" >&2
|
||||
exit 1
|
||||
fi
|
||||
CREATED_SIGNATURE_IDS=$(add_to_list "$CREATED_SIGNATURE_IDS" "$id")
|
||||
save_signatures
|
||||
echo "✅ Created signature with eve_id: $eve_id and ID: $id"
|
||||
else
|
||||
echo "❌ ERROR: Failed to create signature (status $status)" >&2
|
||||
echo "$raw" | parse_response | jq . >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
list_signatures() {
|
||||
echo "==== Listing Signatures ===="
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/signatures")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local count=$(parse_response "$raw" | jq '.data | length')
|
||||
echo "✅ Listed $count signatures"
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "$(parse_response "$raw")" | jq '.'; fi
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to list signatures. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
show_signature() {
|
||||
load_signatures
|
||||
local id=$(echo "$CREATED_SIGNATURE_IDS" | awk '{print $1}')
|
||||
if [ -z "$id" ]; then
|
||||
echo -e "\n❌ ERROR: No signature ID found. Run creation first." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==== Show Signature $id ===="
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/signatures/$id")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local data=$(parse_response "$raw")
|
||||
local eve_id=$(echo "$data" | jq -r '.data.eve_id')
|
||||
local name=$(echo "$data" | jq -r '.data.name')
|
||||
local description=$(echo "$data" | jq -r '.data.description')
|
||||
local custom_info=$(echo "$data" | jq -r '.data.custom_info')
|
||||
echo "✅ Showed signature $id: eve_id='$eve_id', name='$name', description='$description', custom_info='$custom_info'"
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "$data" | jq '.'; fi
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to show signature $id. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
update_signature() {
|
||||
load_signatures
|
||||
local id=$(echo "$CREATED_SIGNATURE_IDS" | awk '{print $1}')
|
||||
if [ -z "$id" ]; then
|
||||
echo -e "\n❌ ERROR: No signature ID found. Run creation first." >&2
|
||||
exit 1
|
||||
fi
|
||||
# Get the EVE system ID for the update payload
|
||||
local sys_info=$(get_first_system)
|
||||
local system_id=$(echo "$sys_info" | cut -d: -f2)
|
||||
echo "==== Updating Signature $id ===="
|
||||
local payload=$(jq -n --arg sid "$system_id" '{description: "Updated via test", custom_info: "Updated info", solar_system_id: ($sid|tonumber) }')
|
||||
local raw=$(make_request PUT "$API_BASE_URL/api/maps/$MAP_SLUG/signatures/$id" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Updated signature $id"
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to update signature $id. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
delete_signature() {
|
||||
load_signatures
|
||||
local id=$(echo "$CREATED_SIGNATURE_IDS" | awk '{print $1}')
|
||||
if [ -z "$id" ]; then
|
||||
echo -e "\n❌ ERROR: No signature ID found. Run creation first." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==== Deleting Signature $id ===="
|
||||
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/signatures/$id")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Deleted signature $id"
|
||||
CREATED_SIGNATURE_IDS=""
|
||||
save_signatures
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to delete signature $id. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
show_menu() {
|
||||
echo "===== Map Structure & Signature API Tests ====="
|
||||
echo "1. Run all tests in sequence (with pauses)"
|
||||
echo "2. Create structure"
|
||||
echo "3. List structures"
|
||||
echo "4. Show structure"
|
||||
echo "5. Update structure"
|
||||
echo "6. Delete structure"
|
||||
echo "7. Create signature"
|
||||
echo "8. List signatures"
|
||||
echo "9. Show signature"
|
||||
echo "10. Update signature"
|
||||
echo "11. Delete signature"
|
||||
echo "12. Exit"
|
||||
echo "==============================================="
|
||||
echo "Enter your choice [1-12]: "
|
||||
}
|
||||
|
||||
case "$COMMAND" in
|
||||
"all")
|
||||
if [ -t 0 ]; then
|
||||
while true; do
|
||||
show_menu
|
||||
read -r choice
|
||||
case $choice in
|
||||
1)
|
||||
create_structure
|
||||
echo "DEBUG: After calling create_structure in menu, exit code $?"
|
||||
echo "DEBUG: After create_structure, exit code $?"; read -p "Press Enter to continue..."
|
||||
list_structures; echo "DEBUG: After list_structures, exit code $?"; read -p "Press Enter to continue..."
|
||||
show_structure; echo "DEBUG: After show_structure, exit code $?"; read -p "Press Enter to continue..."
|
||||
update_structure; echo "DEBUG: After update_structure, exit code $?"; read -p "Press Enter to continue..."
|
||||
show_structure; echo "DEBUG: After show_structure (post-update), exit code $?"; read -p "Press Enter to continue..."
|
||||
delete_structure; echo "DEBUG: After delete_structure, exit code $?"; read -p "Press Enter to continue..."
|
||||
create_signature; echo "DEBUG: After create_signature, exit code $?"; read -p "Press Enter to continue..."
|
||||
list_signatures; echo "DEBUG: After list_signatures, exit code $?"; read -p "Press Enter to continue..."
|
||||
show_signature; echo "DEBUG: After show_signature, exit code $?"; read -p "Press Enter to continue..."
|
||||
update_signature; echo "DEBUG: After update_signature, exit code $?"; read -p "Press Enter to continue..."
|
||||
show_signature; echo "DEBUG: After show_signature (post-update), exit code $?"; read -p "Press Enter to continue..."
|
||||
delete_signature; echo "DEBUG: After delete_signature, exit code $?"; read -p "Press Enter to continue..."
|
||||
echo "All tests completed."
|
||||
show_menu
|
||||
read -r choice
|
||||
continue
|
||||
;;
|
||||
2) create_structure ;;
|
||||
3) list_structures ;;
|
||||
4) show_structure ;;
|
||||
5) update_structure ;;
|
||||
6) delete_structure ;;
|
||||
7) create_signature ;;
|
||||
8) list_signatures ;;
|
||||
9) show_signature ;;
|
||||
10) update_signature ;;
|
||||
11) delete_signature ;;
|
||||
12)
|
||||
read -p "Clean up any remaining test data before exiting? (y/n): " confirm
|
||||
if [[ "$confirm" =~ ^[Yy] ]]; then
|
||||
delete_structure
|
||||
delete_signature
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Invalid option. Please try again." ;;
|
||||
esac
|
||||
done
|
||||
else
|
||||
create_structure; list_structures; show_structure; update_structure; show_structure; delete_structure
|
||||
create_signature; list_signatures; show_signature; update_signature; show_signature; delete_signature
|
||||
fi
|
||||
;;
|
||||
"create")
|
||||
create_structure; create_signature ;;
|
||||
"update")
|
||||
update_structure; update_signature ;;
|
||||
"delete")
|
||||
delete_structure; delete_signature ;;
|
||||
*)
|
||||
echo "Invalid command: $COMMAND"
|
||||
echo "Use -h for help"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
echo "DEBUG: End of script reached"
|
||||
531
test/manual/api/system_api_legacy_tests.sh
Executable file
531
test/manual/api/system_api_legacy_tests.sh
Executable file
@@ -0,0 +1,531 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─── Legacy Map endpoint tests ───────────────────────────────────────────────────────
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/utils.sh"
|
||||
|
||||
# Track created IDs for cleanup - use space-delimited strings to match utils.sh
|
||||
CREATED_SYSTEM_IDS=""
|
||||
CREATED_CONNECTION_IDS=""
|
||||
|
||||
# Optional environment variables to control verbosity:
|
||||
# VERBOSE_LOGGING=1 - Show full API responses
|
||||
QUIET_MODE=1 # Show minimal output (just test names and results)
|
||||
|
||||
# DUMP RESPONSE - Call this to see the complete raw API response
|
||||
dump_complete_response() {
|
||||
local url="$1"
|
||||
|
||||
# Only show full response dumps if VERBOSE_LOGGING is set
|
||||
if [ "${VERBOSE_LOGGING:-0}" -eq 1 ]; then
|
||||
echo ""
|
||||
echo "🔍 DUMPING COMPLETE RESPONSE FOR: $url"
|
||||
echo "────────────────────────────────────────────────────────────────────────────────"
|
||||
curl -s -H "Authorization: Bearer $API_TOKEN" "$url"
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────────────────────────────────────────"
|
||||
echo ""
|
||||
else
|
||||
# In non-verbose mode, just do the curl but don't show output
|
||||
curl -s -H "Authorization: Bearer $API_TOKEN" "$url" > /dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Initial test to show raw API response structure for system endpoint
|
||||
test_dump_system_response() {
|
||||
# If verbose logging is not enabled, skip this test
|
||||
if [ "${VERBOSE_LOGGING:-0}" -ne 1 ]; then
|
||||
#echo "Skipping raw response dump (enable with VERBOSE_LOGGING=1)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local id="30000142" # Jita
|
||||
echo "Getting complete raw API response for system ID $id..."
|
||||
dump_complete_response "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$id"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Helper function to add element to space-delimited string list
|
||||
add_to_list() {
|
||||
local list="$1"
|
||||
local item="$2"
|
||||
if [ -z "$list" ]; then
|
||||
echo "$item"
|
||||
else
|
||||
echo "$list $item"
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to count items in a space-delimited list
|
||||
count_items() {
|
||||
local list="$1"
|
||||
if [ -z "$list" ]; then
|
||||
echo "0"
|
||||
else
|
||||
echo "$list" | wc -w
|
||||
fi
|
||||
}
|
||||
|
||||
# Parse JSON response with error handling
|
||||
parse_response() {
|
||||
local raw="$1"
|
||||
|
||||
# Skip HTTP headers and get the JSON body
|
||||
local json_body=$(echo "$raw" | sed '1,/^\s*$/d')
|
||||
|
||||
# If JSON is valid, return it. Otherwise, return empty object
|
||||
if echo "$json_body" | jq . >/dev/null 2>&1; then
|
||||
echo "$json_body"
|
||||
else
|
||||
echo "{}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get and display detailed system information including visibility
|
||||
fetch_system_details() {
|
||||
local system_id=$1
|
||||
local verbose=${2:-0} # Default to non-verbose mode
|
||||
|
||||
# Skip detailed output in quiet mode
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Fetching system details for ID $system_id..."
|
||||
fi
|
||||
|
||||
# Get the complete raw response
|
||||
local raw
|
||||
raw=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$system_id")
|
||||
|
||||
# Only show raw response in verbose mode
|
||||
if [ "$verbose" -eq 1 ] && [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Raw response from curl:"
|
||||
echo "$raw" | jq '.' 2>/dev/null || echo "$raw"
|
||||
fi
|
||||
|
||||
# Extract key information
|
||||
local name=""
|
||||
local visible=""
|
||||
|
||||
# First attempt to extract from data wrapper
|
||||
if echo "$raw" | jq -e '.data' >/dev/null 2>&1; then
|
||||
name=$(echo "$raw" | jq -r '.data.name // .data.solar_system_name // ""')
|
||||
visible=$(echo "$raw" | jq -r '.data.visible // ""')
|
||||
else
|
||||
# Use grep as a last resort
|
||||
if echo "$raw" | grep -q '"visible":true'; then
|
||||
visible="true"
|
||||
elif echo "$raw" | grep -q '"visible":false'; then
|
||||
visible="false"
|
||||
fi
|
||||
|
||||
if echo "$raw" | grep -q '"name":"[^"]*"'; then
|
||||
name=$(echo "$raw" | grep -o '"name":"[^"]*"' | head -1 | cut -d':' -f2 | tr -d '"')
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show results only if not in quiet mode
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "SYSTEM NAME: $name"
|
||||
echo "VISIBILITY: $visible"
|
||||
fi
|
||||
|
||||
# Return success if we found both name and visibility
|
||||
if [ ! -z "$name" ] && [ ! -z "$visible" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_direct_api_access() {
|
||||
local raw status
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=$MAP_SLUG")
|
||||
status=$(parse_status "$raw")
|
||||
[[ "$status" =~ ^2[0-9]{2}$ ]]
|
||||
}
|
||||
|
||||
test_missing_params() {
|
||||
local raw status
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/systems")
|
||||
status=$(parse_status "$raw")
|
||||
[[ "$status" =~ ^4[0-9]{2}$ ]]
|
||||
}
|
||||
|
||||
test_invalid_auth() {
|
||||
local old="$API_TOKEN" raw status
|
||||
API_TOKEN="invalid-token"
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=$MAP_SLUG")
|
||||
status=$(parse_status "$raw")
|
||||
API_TOKEN="$old"
|
||||
[[ "$status" == "401" || "$status" == "403" ]]
|
||||
}
|
||||
|
||||
test_invalid_slug() {
|
||||
local raw status
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=nonexistent")
|
||||
status=$(parse_status "$raw")
|
||||
[[ "$status" =~ ^4[0-9]{2}$ ]]
|
||||
}
|
||||
|
||||
# Create and then show systems for legacy API
|
||||
test_show_systems() {
|
||||
# Use two well-known systems (use actual EVE IDs for clarity)
|
||||
local jita_id=30000142 # Jita
|
||||
local amarr_id=30002187 # Amarr
|
||||
local success_count=0
|
||||
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Creating and verifying systems: Jita and Amarr"
|
||||
fi
|
||||
|
||||
# Create first system - Jita with coordinates
|
||||
local payload raw status response
|
||||
payload=$(jq -n \
|
||||
--argjson sid "$jita_id" \
|
||||
--argjson visible true \
|
||||
'{solar_system_id:$sid,solar_system_name:"Jita",coordinates:{"x":100,"y":200},visible:$visible}')
|
||||
|
||||
# Create the system using the RESTful API
|
||||
raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" == "201" || "$status" == "200" ]]; then
|
||||
success_count=$((success_count + 1))
|
||||
CREATED_SYSTEM_IDS=$(add_to_list "$CREATED_SYSTEM_IDS" "$jita_id")
|
||||
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ Created Jita system (ID: $jita_id)"
|
||||
echo "Verifying system $jita_id is visible after creation..."
|
||||
fi
|
||||
|
||||
# Allow a moment for system to be registered
|
||||
sleep 1
|
||||
|
||||
# Verify the system is visible
|
||||
fetch_system_details "$jita_id"
|
||||
else
|
||||
echo "Warning: Couldn't create Jita system, status: $status"
|
||||
fi
|
||||
|
||||
# Create second system - Amarr with coordinates
|
||||
payload=$(jq -n \
|
||||
--argjson sid "$amarr_id" \
|
||||
--argjson visible true \
|
||||
'{solar_system_id:$sid,solar_system_name:"Amarr",coordinates:{"x":300,"y":400},visible:$visible}')
|
||||
|
||||
# Create the system using the RESTful API
|
||||
raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" == "201" || "$status" == "200" ]]; then
|
||||
success_count=$((success_count + 1))
|
||||
CREATED_SYSTEM_IDS=$(add_to_list "$CREATED_SYSTEM_IDS" "$amarr_id")
|
||||
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ Created Amarr system (ID: $amarr_id)"
|
||||
echo "Verifying system $amarr_id is visible after creation..."
|
||||
fi
|
||||
|
||||
# Allow a moment for system to be registered
|
||||
sleep 1
|
||||
|
||||
# Verify the system is visible
|
||||
fetch_system_details "$amarr_id"
|
||||
else
|
||||
echo "Warning: Couldn't create Amarr system, status: $status"
|
||||
fi
|
||||
|
||||
# If we couldn't create any systems, test fails
|
||||
if [ $success_count -eq 0 ]; then
|
||||
echo "Couldn't create any test systems for legacy API"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify systems are in the list API
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Checking if systems appear in the list API after creation..."
|
||||
fi
|
||||
|
||||
raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
status=$(parse_status "$raw")
|
||||
response_body=$(echo "$raw" | sed '1,/^\s*$/d')
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
# Parse the response appropriately depending on structure
|
||||
local data_array=""
|
||||
|
||||
# Check if the response has data array structure
|
||||
if echo "$response_body" | jq -e '.data' >/dev/null 2>&1; then
|
||||
data_array=$(echo "$response_body" | jq '.data')
|
||||
else
|
||||
data_array="$response_body"
|
||||
fi
|
||||
|
||||
# Check each created system
|
||||
local all_systems_in_list=true
|
||||
for sid in $CREATED_SYSTEM_IDS; do
|
||||
if echo "$data_array" | jq -e ".[] | select(.solar_system_id == $sid)" >/dev/null 2>&1; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ System $sid appears in list API after creation"
|
||||
fi
|
||||
else
|
||||
all_systems_in_list=false
|
||||
echo "⚠ WARNING: System $sid does not appear in list API after creation"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "ERROR: Failed to get systems list: status $status"
|
||||
fi
|
||||
|
||||
# Now test the legacy API endpoint for each created system
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Verifying systems are accessible via legacy API..."
|
||||
fi
|
||||
|
||||
local legacy_success=true
|
||||
|
||||
for sid in $CREATED_SYSTEM_IDS; do
|
||||
local raw status
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/system?id=$sid&slug=$MAP_SLUG")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ ! "$status" =~ ^2[0-9]{2}$ ]]; then
|
||||
echo "Failed to retrieve system $sid via legacy API: status $status"
|
||||
legacy_success=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$legacy_success" = "true" ] && [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ All systems accessible via legacy API"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
test_verify_connections() {
|
||||
# Even if we don't have systems, we can still test the legacy connections API endpoint
|
||||
# by checking that it returns a valid response
|
||||
local raw status response
|
||||
|
||||
# Try to check all connections via legacy API
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/connections?slug=$MAP_SLUG")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
# If the endpoint exists and returns a success status, the test passes
|
||||
if [[ "$status" =~ ^2[0-9]{2}$ ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
test_delete_systems() {
|
||||
# If we don't have system IDs, skip the test
|
||||
if [ $(count_items "$CREATED_SYSTEM_IDS") -eq 0 ]; then
|
||||
echo "No systems to delete, skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local success_count=0
|
||||
local total_systems=$(count_items "$CREATED_SYSTEM_IDS")
|
||||
local deleted_ids=""
|
||||
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "TEST: Delete Systems API"
|
||||
echo "------------------------"
|
||||
echo "Testing system deletion for existing systems in map $MAP_SLUG"
|
||||
echo "Systems to delete: $CREATED_SYSTEM_IDS"
|
||||
fi
|
||||
|
||||
# Try batch delete first
|
||||
if [ $(count_items "$CREATED_SYSTEM_IDS") -gt 1 ]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Attempting batch delete of systems: $CREATED_SYSTEM_IDS"
|
||||
fi
|
||||
|
||||
local payload=$(echo "$CREATED_SYSTEM_IDS" | tr ' ' '\n' | jq -R . | jq -s '{system_ids: .}')
|
||||
local raw status
|
||||
|
||||
raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems/batch_delete" "$payload")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ Batch delete successful"
|
||||
fi
|
||||
|
||||
# Verify systems are gone from the list
|
||||
sleep 1
|
||||
local list_response
|
||||
list_response=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
|
||||
# Check if all systems are gone
|
||||
local all_deleted=1
|
||||
for system_id in $CREATED_SYSTEM_IDS; do
|
||||
if echo "$list_response" | jq -e --arg id "$system_id" '.data[] | select(.solar_system_id == ($id|tonumber) and .visible == true)' >/dev/null 2>&1; then
|
||||
all_deleted=0
|
||||
else
|
||||
success_count=$((success_count + 1))
|
||||
deleted_ids=$(add_to_list "$deleted_ids" "$system_id")
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ System $system_id no longer visible in list API after batch deletion"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $all_deleted -eq 1 ]; then
|
||||
# Update the list of created systems to remove successfully deleted ones
|
||||
for id in $deleted_ids; do
|
||||
CREATED_SYSTEM_IDS=$(echo "$CREATED_SYSTEM_IDS" | sed "s/\b$id\b//g" | tr -s ' ' | sed 's/^ //g' | sed 's/ $//g')
|
||||
done
|
||||
|
||||
# If batch delete worked for all systems, we're done
|
||||
if [ $success_count -eq $total_systems ]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✅ All systems successfully deleted via batch delete"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Batch delete failed with status $status, trying individual deletes"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# If batch delete didn't work, try individual deletes
|
||||
for system_id in $CREATED_SYSTEM_IDS; do
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Attempting to delete system with ID: $system_id"
|
||||
fi
|
||||
|
||||
local raw status
|
||||
|
||||
# Use the RESTful DELETE endpoint
|
||||
raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$system_id")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ Delete API call successful for system $system_id"
|
||||
fi
|
||||
|
||||
# Allow time for change to propagate
|
||||
sleep 1
|
||||
|
||||
# Get the complete system list after deletion
|
||||
local list_response
|
||||
list_response=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
|
||||
# Check if the system appears in the list (deleted systems shouldn't appear or should be invisible)
|
||||
local system_still_visible=0
|
||||
|
||||
if echo "$list_response" | jq -e --arg id "$system_id" '.data[] | select(.solar_system_id == ($id|tonumber) and .visible == true)' >/dev/null 2>&1; then
|
||||
system_still_visible=1
|
||||
fi
|
||||
|
||||
if [ $system_still_visible -eq 0 ]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ System $system_id no longer visible in list API after deletion"
|
||||
fi
|
||||
success_count=$((success_count + 1))
|
||||
deleted_ids=$(add_to_list "$deleted_ids" "$system_id")
|
||||
fi
|
||||
else
|
||||
echo "❌ Failed to delete system $system_id: status $status"
|
||||
fi
|
||||
done
|
||||
|
||||
# Update the list of created systems to remove successfully deleted ones
|
||||
for id in $deleted_ids; do
|
||||
CREATED_SYSTEM_IDS=$(echo "$CREATED_SYSTEM_IDS" | sed "s/\b$id\b//g" | tr -s ' ' | sed 's/^ //g' | sed 's/ $//g')
|
||||
done
|
||||
|
||||
# Report results
|
||||
if [ $success_count -eq $total_systems ]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✅ All systems successfully deleted (no longer visible in list API): $success_count / $total_systems"
|
||||
fi
|
||||
return 0
|
||||
else
|
||||
echo "⚠ Some systems still appear visible in list API after deletion: $success_count / $total_systems deleted"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test the system list API endpoint
|
||||
test_system_list() {
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Testing system list API endpoint..."
|
||||
fi
|
||||
|
||||
local raw status
|
||||
raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" != "200" ]]; then
|
||||
echo "ERROR: Failed to get system list: status $status"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Test legacy system list endpoint too
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Testing legacy system list API endpoint..."
|
||||
fi
|
||||
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=$MAP_SLUG")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" != "200" ]]; then
|
||||
echo "ERROR: Failed to get legacy system list: status $status"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check that both APIs return the same number of systems
|
||||
local restful_count=$(echo "$raw" | sed '1,/^\s*$/d' | jq '.data | length // length')
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=$MAP_SLUG")
|
||||
local legacy_count=$(echo "$raw" | sed '1,/^\s*$/d' | jq '.data | length // length')
|
||||
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "RESTful API returned $restful_count systems, Legacy API returned $legacy_count systems"
|
||||
fi
|
||||
|
||||
if [[ "$restful_count" == "$legacy_count" ]]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ Both APIs return the same number of systems"
|
||||
fi
|
||||
else
|
||||
echo "WARNING: APIs return different numbers of systems"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─── Execute Tests ────────────────────────────────────────────────────────────
|
||||
# Function to run a test and report success/failure
|
||||
run_test() {
|
||||
local name="$1"
|
||||
local func="$2"
|
||||
|
||||
# Only print test name if not in quiet mode
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo -n "Testing: $name... "
|
||||
fi
|
||||
|
||||
# Run the test function
|
||||
if $func; then
|
||||
echo "✅ $name"
|
||||
return 0
|
||||
else
|
||||
echo "❌ $name"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_test "Dump Raw API Response" test_dump_system_response
|
||||
run_test "Direct API access" test_direct_api_access
|
||||
run_test "Missing params (4xx)" test_missing_params
|
||||
run_test "Invalid auth (401/403)" test_invalid_auth
|
||||
run_test "Invalid slug on GET" test_invalid_slug
|
||||
run_test "Show systems" test_show_systems
|
||||
run_test "System list" test_system_list
|
||||
run_test "Verify connections" test_verify_connections
|
||||
run_test "Delete systems" test_delete_systems
|
||||
682
test/manual/api/system_api_tests.sh
Executable file
682
test/manual/api/system_api_tests.sh
Executable file
@@ -0,0 +1,682 @@
|
||||
#!/bin/bash
|
||||
# test/manual/api/improved_api_tests.sh
|
||||
# ─── Improved API Tests for Map System and Connection APIs ────────────────────────
|
||||
#
|
||||
# Usage:
|
||||
# ./improved_api_tests.sh # Run all tests with menu selection
|
||||
# ./improved_api_tests.sh create # Run only creation tests
|
||||
# ./improved_api_tests.sh update # Run only update tests
|
||||
# ./improved_api_tests.sh delete # Run only deletion tests
|
||||
# ./improved_api_tests.sh -v # Run in verbose mode
|
||||
#
|
||||
source "$(dirname "$0")/utils.sh"
|
||||
|
||||
# Set to "true" to see detailed output, "false" for minimal output
|
||||
VERBOSE=${VERBOSE:-false}
|
||||
|
||||
# Parse command line options
|
||||
while getopts "vh" opt; do
|
||||
case $opt in
|
||||
v)
|
||||
VERBOSE=true
|
||||
;;
|
||||
h)
|
||||
echo "Usage: $0 [-v] [-h] [all|create|update|delete]"
|
||||
echo " -v Verbose mode (show detailed test output)"
|
||||
echo " -h Show this help message"
|
||||
echo " all Run all tests (default with menu)"
|
||||
echo " create Run only creation tests"
|
||||
echo " update Run only update tests"
|
||||
echo " delete Run only deletion tests"
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
echo "Use -h for help"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
shift $((OPTIND-1))
|
||||
COMMAND=${1:-"all"}
|
||||
|
||||
# File to store system and connection IDs for persistence between command runs
|
||||
SYSTEMS_FILE="/tmp/wanderer_test_systems.txt"
|
||||
CONNECTIONS_FILE="/tmp/wanderer_test_connections.txt"
|
||||
|
||||
# Track created IDs for cleanup
|
||||
CREATED_SYSTEM_IDS=""
|
||||
CREATED_CONNECTION_IDS=""
|
||||
|
||||
# Array of valid EVE system IDs and names (first 5 for individual creation)
|
||||
declare -a EVE_SYSTEMS=(
|
||||
"30005304:Alentene"
|
||||
"30003380:Alf"
|
||||
"30003811:Algasienan"
|
||||
"30004972:Algogille"
|
||||
"30002698:Aliette"
|
||||
)
|
||||
|
||||
# Next 5 for batch upsert
|
||||
declare -a BATCH_EVE_SYSTEMS=(
|
||||
"30002754:Alikara"
|
||||
"30002712:Alillere"
|
||||
"30003521:Alkabsi"
|
||||
"30000034:Alkez"
|
||||
"30004995:Allamotte"
|
||||
)
|
||||
|
||||
# ─── UTILITY FUNCTIONS ─────────────────────────────────────────────────────
|
||||
|
||||
# Function to save created system IDs to file
|
||||
save_systems() {
|
||||
echo "$CREATED_SYSTEM_IDS" > "$SYSTEMS_FILE"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Saved $(wc -w < "$SYSTEMS_FILE") systems to $SYSTEMS_FILE"
|
||||
}
|
||||
|
||||
# Function to load system IDs from file
|
||||
load_systems() {
|
||||
if [ -f "$SYSTEMS_FILE" ]; then
|
||||
CREATED_SYSTEM_IDS=$(cat "$SYSTEMS_FILE")
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Loaded $(wc -w < "$SYSTEMS_FILE") systems from $SYSTEMS_FILE"
|
||||
else
|
||||
echo "No systems file found at $SYSTEMS_FILE. Run creation tests first."
|
||||
CREATED_SYSTEM_IDS=""
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to save created connection IDs to file
|
||||
save_connections() {
|
||||
echo "$CREATED_CONNECTION_IDS" > "$CONNECTIONS_FILE"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Saved $(wc -w < "$CONNECTIONS_FILE") connections to $CONNECTIONS_FILE"
|
||||
}
|
||||
|
||||
# Function to load connection IDs from file
|
||||
load_connections() {
|
||||
if [ -f "$CONNECTIONS_FILE" ]; then
|
||||
CREATED_CONNECTION_IDS=$(cat "$CONNECTIONS_FILE")
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Loaded $(wc -w < "$CONNECTIONS_FILE") connections from $CONNECTIONS_FILE"
|
||||
else
|
||||
echo "No connections file found at $CONNECTIONS_FILE. Run creation tests first."
|
||||
CREATED_CONNECTION_IDS=""
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to add item to space-delimited list
|
||||
add_to_list() {
|
||||
local list="$1"
|
||||
local item="$2"
|
||||
if [ -z "$list" ]; then
|
||||
echo "$item"
|
||||
else
|
||||
echo "$list $item"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── TEST FUNCTIONS ─────────────────────────────────────────────────────
|
||||
|
||||
# FUNCTION: Create systems
|
||||
create_systems() {
|
||||
echo "==== Creating Systems ===="
|
||||
local system_count=0
|
||||
local center_x=500
|
||||
local center_y=500
|
||||
local radius=250
|
||||
|
||||
# Only clear the systems file if we're starting fresh
|
||||
> "$SYSTEMS_FILE"
|
||||
CREATED_SYSTEM_IDS=""
|
||||
|
||||
# Build all system payloads as a JSON array
|
||||
local systems_payload="["
|
||||
local num_systems=${#EVE_SYSTEMS[@]}
|
||||
for i in $(seq 0 $((num_systems-1))); do
|
||||
IFS=':' read -r system_id system_name <<< "${EVE_SYSTEMS[$i]}"
|
||||
local angle=$(echo "scale=6; $i * 6.28318 / $num_systems" | bc -l)
|
||||
local x=$(echo "scale=2; $center_x + $radius * c($angle)" | bc -l)
|
||||
local y=$(echo "scale=2; $center_y + $radius * s($angle)" | bc -l)
|
||||
local system_json=$(jq -n \
|
||||
--argjson sid "$system_id" \
|
||||
--arg name "$system_name" \
|
||||
--argjson x "$x" \
|
||||
--argjson y "$y" \
|
||||
'{
|
||||
solar_system_id: $sid,
|
||||
solar_system_name: $name,
|
||||
position_x: $x,
|
||||
position_y: $y,
|
||||
status: "clear",
|
||||
visible: true,
|
||||
description: "Test system",
|
||||
tag: "TEST",
|
||||
locked: false
|
||||
}')
|
||||
systems_payload+="$system_json"
|
||||
if [ $i -lt $((num_systems-1)) ]; then
|
||||
systems_payload+=","
|
||||
fi
|
||||
done
|
||||
systems_payload+="]"
|
||||
|
||||
# Wrap in the 'systems' key
|
||||
local payload="{\"systems\": $systems_payload}"
|
||||
|
||||
# Send the batch create request
|
||||
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Created all systems in batch"
|
||||
# Track the system IDs for later cleanup
|
||||
for i in $(seq 0 $((num_systems-1))); do
|
||||
IFS=':' read -r system_id _ <<< "${EVE_SYSTEMS[$i]}"
|
||||
CREATED_SYSTEM_IDS=$(add_to_list "$CREATED_SYSTEM_IDS" "$system_id")
|
||||
system_count=$((system_count+1))
|
||||
done
|
||||
else
|
||||
echo "❌ Failed to create systems in batch. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
|
||||
echo "Total systems created: $system_count/$num_systems"
|
||||
save_systems
|
||||
|
||||
# Validate actual state after creation
|
||||
echo "Validating systems after dedicated creation:"
|
||||
list_systems_and_connections
|
||||
}
|
||||
|
||||
# FUNCTION: Create connections
|
||||
create_connections() {
|
||||
echo "==== Creating Connections ===="
|
||||
load_systems
|
||||
if [ -z "$CREATED_SYSTEM_IDS" ]; then
|
||||
echo "No systems available. Run system creation first."
|
||||
return 1
|
||||
fi
|
||||
> "$CONNECTIONS_FILE"
|
||||
CREATED_CONNECTION_IDS=""
|
||||
local connection_count=0
|
||||
local total_connections=0
|
||||
local system_array=($CREATED_SYSTEM_IDS)
|
||||
|
||||
echo "Testing dedicated connection endpoints..."
|
||||
# Create connections one by one using the dedicated endpoint
|
||||
for i in $(seq 0 $((${#system_array[@]}-1))); do
|
||||
local source=${system_array[$i]}
|
||||
local target=${system_array[$(( (i+1) % ${#system_array[@]} ))]}
|
||||
total_connections=$((total_connections+1))
|
||||
|
||||
# Create single connection payload
|
||||
local payload=$(jq -n \
|
||||
--argjson source "$source" \
|
||||
--argjson target "$target" \
|
||||
'{
|
||||
solar_system_source: $source,
|
||||
solar_system_target: $target,
|
||||
type: 0,
|
||||
mass_status: 0,
|
||||
time_status: 0,
|
||||
ship_size_type: 1,
|
||||
wormhole_type: "K162",
|
||||
count_of_passage: 0
|
||||
}')
|
||||
|
||||
# Send create request to dedicated endpoint
|
||||
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/connections" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Created connection from $source to $target"
|
||||
local response=$(parse_response "$raw")
|
||||
# Store source and target for later use
|
||||
CREATED_CONNECTION_IDS=$(add_to_list "$CREATED_CONNECTION_IDS" "${source}:${target}")
|
||||
connection_count=$((connection_count+1))
|
||||
else
|
||||
echo "❌ Failed to create connection from $source to $target. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Total connections created via dedicated endpoint: $connection_count/$total_connections"
|
||||
save_connections
|
||||
|
||||
# Always validate actual state after connection creation
|
||||
echo "Validating connections after dedicated creation:"
|
||||
list_systems_and_connections
|
||||
|
||||
echo -e "\nTesting batch upsert functionality..."
|
||||
# Build batch upsert payload using BATCH_EVE_SYSTEMS
|
||||
local batch_systems_json="["
|
||||
local batch_connections_json="["
|
||||
local num_batch_systems=${#BATCH_EVE_SYSTEMS[@]}
|
||||
for i in $(seq 0 $((num_batch_systems-1))); do
|
||||
IFS=':' read -r system_id system_name <<< "${BATCH_EVE_SYSTEMS[$i]}"
|
||||
local angle=$(echo "scale=6; $i * 6.28318 / $num_batch_systems" | bc -l)
|
||||
local x=$(echo "scale=2; 500 + 250 * c($angle)" | bc -l)
|
||||
local y=$(echo "scale=2; 500 + 250 * s($angle)" | bc -l)
|
||||
local system_json=$(jq -n \
|
||||
--argjson sid "$system_id" \
|
||||
--arg name "$system_name" \
|
||||
--argjson x "$x" \
|
||||
--argjson y "$y" \
|
||||
'{
|
||||
solar_system_id: $sid,
|
||||
solar_system_name: $name,
|
||||
position_x: $x,
|
||||
position_y: $y,
|
||||
status: "clear",
|
||||
visible: true,
|
||||
description: "Test system (batch)",
|
||||
tag: "BATCH",
|
||||
locked: false
|
||||
}')
|
||||
batch_systems_json+="$system_json"
|
||||
if [ $i -lt $((num_batch_systems-1)) ]; then
|
||||
batch_systems_json+=","
|
||||
fi
|
||||
# Build connections in a ring
|
||||
local source=$system_id
|
||||
local next_index=$(( (i+1) % num_batch_systems ))
|
||||
IFS=':' read -r target_id _ <<< "${BATCH_EVE_SYSTEMS[$next_index]}"
|
||||
batch_connections_json+="{\"solar_system_source\":$source,\"solar_system_target\":$target_id,\"mass_status\":0,\"ship_size_type\":1,\"type\":0}"
|
||||
if [ $i -lt $((num_batch_systems-1)) ]; then
|
||||
batch_connections_json+=","
|
||||
fi
|
||||
done
|
||||
batch_systems_json+="]"
|
||||
batch_connections_json+="]"
|
||||
|
||||
echo "[SCRIPT] Batch upsert systems: $batch_systems_json"
|
||||
echo "[SCRIPT] Batch upsert connections: $batch_connections_json"
|
||||
|
||||
# Check for API_TOKEN
|
||||
if [ -z "$API_TOKEN" ]; then
|
||||
echo "❌ API_TOKEN is not set. Please export API_TOKEN before running the script."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Send batch upsert request
|
||||
local response=$(curl -s -X POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-d "{\"systems\":$batch_systems_json,\"connections\":$batch_connections_json}")
|
||||
|
||||
echo "[SCRIPT] Batch upsert response: $response"
|
||||
|
||||
# Debug: List all connections after batch upsert
|
||||
echo "[SCRIPT] Listing all connections after batch upsert:"
|
||||
local list_raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
local list_status=$(parse_status "$list_raw")
|
||||
if [[ "$list_status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local list_response=$(parse_response "$list_raw")
|
||||
echo "$list_response" | jq -c '.data.connections[] | {id: .id, source: .solar_system_source, target: .solar_system_target, mass_status: .mass_status, ship_size_type: .ship_size_type, type: .type}'
|
||||
else
|
||||
echo "[SCRIPT] Failed to list connections after batch upsert. Status: $list_status"
|
||||
fi
|
||||
|
||||
# Add batch system IDs to CREATED_SYSTEM_IDS
|
||||
for i in $(seq 0 $((num_batch_systems-1))); do
|
||||
IFS=':' read -r system_id _ <<< "${BATCH_EVE_SYSTEMS[$i]}"
|
||||
CREATED_SYSTEM_IDS=$(add_to_list "$CREATED_SYSTEM_IDS" "$system_id")
|
||||
done
|
||||
|
||||
# Add batch connection pairs to CREATED_CONNECTION_IDS
|
||||
for i in $(seq 0 $((num_batch_systems-1))); do
|
||||
IFS=':' read -r source _ <<< "${BATCH_EVE_SYSTEMS[$i]}"
|
||||
next_index=$(( (i+1) % num_batch_systems ))
|
||||
IFS=':' read -r target _ <<< "${BATCH_EVE_SYSTEMS[$next_index]}"
|
||||
CREATED_CONNECTION_IDS=$(add_to_list "$CREATED_CONNECTION_IDS" "${source}:${target}")
|
||||
done
|
||||
save_systems
|
||||
save_connections
|
||||
|
||||
list_systems_and_connections
|
||||
|
||||
echo "Total connections updated: $connection_count/${#system_array[@]}"
|
||||
}
|
||||
|
||||
# FUNCTION: Update systems
|
||||
update_systems() {
|
||||
echo "==== Updating Systems ===="
|
||||
load_systems
|
||||
|
||||
if [ -z "$CREATED_SYSTEM_IDS" ]; then
|
||||
echo "No systems available. Run system creation first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local update_count=0
|
||||
local system_array=($CREATED_SYSTEM_IDS)
|
||||
local num_systems=${#system_array[@]}
|
||||
|
||||
for i in $(seq 0 $((num_systems-1))); do
|
||||
local system_id=${system_array[$i]}
|
||||
|
||||
# Get system name from EVE_SYSTEMS array if available
|
||||
local system_name="System $system_id"
|
||||
for j in $(seq 0 $((${#EVE_SYSTEMS[@]}-1))); do
|
||||
IFS=':' read -r curr_id curr_name <<< "${EVE_SYSTEMS[$j]}"
|
||||
if [ "$curr_id" = "$system_id" ]; then
|
||||
system_name=$curr_name
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Updating system $((i+1))/$num_systems: $system_name (ID: $system_id)"
|
||||
|
||||
# Create update payload with new values
|
||||
local status_values=("clear" "friendly" "hostile" "occupied")
|
||||
local status=${status_values[$((RANDOM % 4))]}
|
||||
local desc="Updated description for $system_name"
|
||||
local tag="UPDATED"
|
||||
|
||||
local payload=$(jq -n \
|
||||
--arg status "$status" \
|
||||
--arg desc "$desc" \
|
||||
--arg tag "$tag" \
|
||||
'{
|
||||
status: $status,
|
||||
description: $desc,
|
||||
tag: $tag,
|
||||
locked: false
|
||||
}')
|
||||
|
||||
# Send the update request
|
||||
local raw=$(make_request PUT "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$system_id" "$payload")
|
||||
local status_code=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status_code" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Updated system $system_name with status: $status"
|
||||
update_count=$((update_count+1))
|
||||
else
|
||||
echo "❌ Failed to update system $system_name. Status: $status_code"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Total systems updated: $update_count/$num_systems"
|
||||
}
|
||||
|
||||
# FUNCTION: Update connections
|
||||
update_connections() {
|
||||
echo "==== Updating Connections ===="
|
||||
load_systems
|
||||
load_connections
|
||||
|
||||
if [ -z "$CREATED_SYSTEM_IDS" ] || [ -z "$CREATED_CONNECTION_IDS" ]; then
|
||||
echo "No systems or connections available. Run creation tests first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Testing connection updates..."
|
||||
local update_count=0
|
||||
local conn_array=($CREATED_CONNECTION_IDS)
|
||||
|
||||
for triple in "${conn_array[@]}"; do
|
||||
local source=$(echo $triple | cut -d: -f1)
|
||||
local target=$(echo $triple | cut -d: -f2)
|
||||
|
||||
# Create update payload
|
||||
local mass_values=(0 1 2)
|
||||
local ship_values=(0 1 2 3)
|
||||
local mass=${mass_values[$((RANDOM % 3))]}
|
||||
local ship=${ship_values[$((RANDOM % 4))]}
|
||||
local payload=$(jq -n \
|
||||
--argjson mass "$mass" \
|
||||
--argjson ship "$ship" \
|
||||
'{
|
||||
mass_status: $mass,
|
||||
ship_size_type: $ship
|
||||
}')
|
||||
|
||||
# Try source/target update
|
||||
local raw=$(make_request PATCH "$API_BASE_URL/api/maps/$MAP_SLUG/connections?solar_system_source=$source&solar_system_target=$target" "$payload")
|
||||
local status_code=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status_code" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Updated connection $source->$target"
|
||||
update_count=$((update_count+1))
|
||||
else
|
||||
echo "❌ Failed to update connection $source->$target. Status: $status_code"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Total connections updated: $update_count/${#conn_array[@]}"
|
||||
|
||||
echo -e "\nTesting batch connection updates..."
|
||||
# Create batch update payload for all connections
|
||||
local batch_connections="["
|
||||
local first=true
|
||||
for triple in "${conn_array[@]}"; do
|
||||
local source=$(echo $triple | cut -d: -f1)
|
||||
local target=$(echo $triple | cut -d: -f2)
|
||||
|
||||
local mass=${mass_values[$((RANDOM % 3))]}
|
||||
local ship=${ship_values[$((RANDOM % 4))]}
|
||||
|
||||
if [ "$first" = true ]; then
|
||||
first=false
|
||||
else
|
||||
batch_connections+=","
|
||||
fi
|
||||
|
||||
batch_connections+=$(jq -n \
|
||||
--argjson source "$source" \
|
||||
--argjson target "$target" \
|
||||
--argjson mass "$mass" \
|
||||
--argjson ship "$ship" \
|
||||
'{
|
||||
solar_system_source: $source,
|
||||
solar_system_target: $target,
|
||||
mass_status: $mass,
|
||||
ship_size_type: $ship
|
||||
}')
|
||||
done
|
||||
batch_connections+="]"
|
||||
|
||||
local batch_payload="{\"connections\": $batch_connections}"
|
||||
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$batch_payload")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local response=$(parse_response "$raw")
|
||||
local updated_count=$(echo "$response" | jq '.data.connections.updated')
|
||||
if [ "$updated_count" != "null" ]; then
|
||||
echo "✅ Batch update successful - Updated connections: $updated_count"
|
||||
else
|
||||
echo "❌ Batch update returned null for updated count"
|
||||
fi
|
||||
else
|
||||
echo "❌ Batch update failed. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
}
|
||||
|
||||
# FUNCTION: List systems and connections
|
||||
list_systems_and_connections() {
|
||||
echo "==== Listing Systems and Connections ===="
|
||||
load_systems
|
||||
if [ -z "$CREATED_SYSTEM_IDS" ]; then
|
||||
echo "No systems available. Run system creation first."
|
||||
return 1
|
||||
fi
|
||||
echo "Testing list all systems and connections endpoint"
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local response=$(parse_response "$raw")
|
||||
local system_count=$(echo "$response" | jq '.data.systems | length')
|
||||
local conn_count=$(echo "$response" | jq '.data.connections | length')
|
||||
echo "✅ Listed $system_count systems and $conn_count connections"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "$response" | jq '.'
|
||||
return 0
|
||||
else
|
||||
echo "❌ Failed to list systems and connections. Status: $status"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# FUNCTION: Delete connections and systems
|
||||
delete_everything() {
|
||||
echo "==== Deleting Connections and Systems ===="
|
||||
load_connections
|
||||
load_systems
|
||||
|
||||
echo "Cleaning up connections..."
|
||||
# Delete connections using source/target pairs
|
||||
local conn_array=($CREATED_CONNECTION_IDS)
|
||||
for triple in "${conn_array[@]}"; do
|
||||
local source=$(echo $triple | cut -d: -f1)
|
||||
local target=$(echo $triple | cut -d: -f2)
|
||||
|
||||
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/connections?solar_system_source=$source&solar_system_target=$target")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Deleted connection $source->$target"
|
||||
else
|
||||
echo "❌ Failed to delete connection $source->$target. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Cleaning up systems..."
|
||||
# Use batch delete for systems
|
||||
local system_array=($CREATED_SYSTEM_IDS)
|
||||
echo "Attempting batch delete of systems..."
|
||||
echo "System ${system_array[@]}"
|
||||
|
||||
local system_ids_json=$(printf '%s\n' "${system_array[@]}" | jq -R . | jq -s .)
|
||||
local payload=$(jq -n --argjson system_ids "$system_ids_json" '{system_ids: $system_ids}')
|
||||
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Batch delete successful for all systems"
|
||||
> "$SYSTEMS_FILE"
|
||||
> "$CONNECTIONS_FILE"
|
||||
CREATED_SYSTEM_IDS=""
|
||||
CREATED_CONNECTION_IDS=""
|
||||
else
|
||||
echo "❌ Batch delete failed. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── MENU AND INTERACTION LOGIC ─────────────────────────────────────────
|
||||
|
||||
show_menu() {
|
||||
echo "===== Map System and Connection API Tests ====="
|
||||
echo "1. Run all tests in sequence (with pauses)"
|
||||
echo "2. Create systems"
|
||||
echo "3. Create connections"
|
||||
echo "4. Update systems"
|
||||
echo "5. Update connections"
|
||||
echo "6. List systems and connections"
|
||||
echo "7. Delete everything"
|
||||
echo "8. Exit"
|
||||
echo "================================================"
|
||||
echo "Enter your choice [1-8]: "
|
||||
}
|
||||
|
||||
# ─── MAIN EXECUTION FLOW ─────────────────────────────────────────────────
|
||||
|
||||
# Main execution based on command
|
||||
case "$COMMAND" in
|
||||
"all")
|
||||
# If no specific command was provided, show the menu
|
||||
if [ -t 0 ]; then # Only show menu if running interactively
|
||||
# Interactive mode with menu
|
||||
while true; do
|
||||
show_menu
|
||||
read -r choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
# Run all tests in sequence with pauses
|
||||
create_systems || echo "System creation failed/skipped"
|
||||
echo "Press Enter to continue with connection creation..."
|
||||
read -r
|
||||
|
||||
create_connections || echo "Connection creation failed/skipped"
|
||||
echo "Press Enter to continue with system updates..."
|
||||
read -r
|
||||
|
||||
update_systems || echo "System update failed/skipped"
|
||||
echo "Press Enter to continue with connection updates..."
|
||||
read -r
|
||||
|
||||
update_connections || echo "Connection update failed/skipped"
|
||||
echo "Press Enter to continue with listing tests..."
|
||||
read -r
|
||||
|
||||
list_systems_and_connections || echo "Listing failed/skipped"
|
||||
echo "Press Enter to continue with deletion..."
|
||||
read -r
|
||||
|
||||
delete_everything || echo "Cleanup failed/skipped"
|
||||
echo "All tests completed."
|
||||
;;
|
||||
2)
|
||||
create_systems
|
||||
;;
|
||||
3)
|
||||
create_connections
|
||||
;;
|
||||
4)
|
||||
update_systems
|
||||
;;
|
||||
5)
|
||||
update_connections
|
||||
;;
|
||||
6)
|
||||
list_systems_and_connections
|
||||
;;
|
||||
7)
|
||||
delete_everything
|
||||
;;
|
||||
8)
|
||||
# Offer to clean up before exiting
|
||||
read -p "Clean up any remaining test data before exiting? (y/n): " confirm
|
||||
if [[ "$confirm" =~ ^[Yy] ]]; then
|
||||
delete_everything
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Invalid option. Please try again."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
else
|
||||
# Non-interactive mode, run all tests in sequence
|
||||
create_systems || echo "System creation failed/skipped"
|
||||
create_connections || echo "Connection creation failed/skipped"
|
||||
update_systems || echo "System update failed/skipped"
|
||||
update_connections || echo "Connection update failed/skipped"
|
||||
list_systems_and_connections || echo "Listing failed/skipped"
|
||||
delete_everything || echo "Cleanup failed/skipped"
|
||||
fi
|
||||
;;
|
||||
"create")
|
||||
create_systems
|
||||
create_connections
|
||||
;;
|
||||
"update")
|
||||
update_systems
|
||||
update_connections
|
||||
list_systems_and_connections
|
||||
;;
|
||||
"delete")
|
||||
delete_everything
|
||||
;;
|
||||
*)
|
||||
echo "Invalid command: $COMMAND"
|
||||
echo "Use -h for help"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
167
test/manual/api/utils.sh
Executable file
167
test/manual/api/utils.sh
Executable file
@@ -0,0 +1,167 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
# ─── Dependencies ─────────────────────────────────────────────────────────────
|
||||
for cmd in curl jq; do
|
||||
if ! command -v "$cmd" > /dev/null 2>&1; then
|
||||
echo "Error: '$cmd' is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
# ─── Load .env if present ─────────────────────────────────────────────────────
|
||||
load_env_file() {
|
||||
echo "📄 Loading env file: $1"
|
||||
set -o allexport
|
||||
source "$1"
|
||||
set +o allexport
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
if [ -f "$SCRIPT_DIR/.env" ]; then
|
||||
load_env_file "$SCRIPT_DIR/.env"
|
||||
fi
|
||||
|
||||
# Check if API_TOKEN is set
|
||||
: "${API_TOKEN:?Error: API_TOKEN environment variable not set}"
|
||||
|
||||
# ─── HTTP Request Helper ──────────────────────────────────────────────────────
|
||||
make_request() {
|
||||
local method=$1 url=$2 data=${3:-}
|
||||
local curl_cmd=(curl -s -w $'\n%{http_code}' -H "Authorization: Bearer $API_TOKEN")
|
||||
|
||||
if [ "$method" != "GET" ]; then
|
||||
curl_cmd+=(-X "$method" -H "Content-Type: application/json")
|
||||
fi
|
||||
|
||||
if [ -n "$data" ]; then
|
||||
curl_cmd+=(-d "$data")
|
||||
fi
|
||||
|
||||
"${curl_cmd[@]}" "$url"
|
||||
}
|
||||
|
||||
# ─── Response Parsers ─────────────────────────────────────────────────────────
|
||||
parse_response() { # strips the final newline+status line
|
||||
local raw="$1"
|
||||
echo "${raw%$'\n'*}"
|
||||
}
|
||||
|
||||
parse_status() { # returns only the status code (last line)
|
||||
local raw="$1"
|
||||
echo "${raw##*$'\n'}"
|
||||
}
|
||||
|
||||
# ─── Assertion Helper ─────────────────────────────────────────────────────────
|
||||
verify_http_code() {
|
||||
local got=$1 want=$2 label=$3
|
||||
if [ "$got" -eq "$want" ]; then
|
||||
return 0
|
||||
else
|
||||
echo "🚫 $label: expected HTTP $want, got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Test Runner & Summary ────────────────────────────────────────────────────
|
||||
# Only initialize counters once to accumulate across multiple suite sources
|
||||
if [ -z "${TOTAL_TESTS+x}" ]; then
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
FAILED_LIST=""
|
||||
fi
|
||||
|
||||
run_test() {
|
||||
local label=$1 fn=$2
|
||||
TOTAL_TESTS=$((TOTAL_TESTS+1))
|
||||
if "$fn"; then
|
||||
echo "✅ $label"
|
||||
PASSED_TESTS=$((PASSED_TESTS+1))
|
||||
else
|
||||
echo "❌ $label"
|
||||
FAILED_TESTS=$((FAILED_TESTS+1))
|
||||
FAILED_LIST="$FAILED_LIST $label"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Cleanup on Exit ──────────────────────────────────────────────────────────
|
||||
CREATED_SYSTEM_IDS=""
|
||||
CREATED_CONNECTION_IDS=""
|
||||
|
||||
cleanup_map_systems() {
|
||||
# First delete connections
|
||||
if [ -n "$CREATED_CONNECTION_IDS" ]; then
|
||||
echo "Cleaning up connections..."
|
||||
for conn_id in $CREATED_CONNECTION_IDS; do
|
||||
# Try with a direct DELETE request to the connection endpoint
|
||||
make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/connections/$conn_id" > /dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
|
||||
# Then delete systems
|
||||
if [ -n "$CREATED_SYSTEM_IDS" ]; then
|
||||
echo "Cleaning up systems..."
|
||||
|
||||
# First try batch delete if we have multiple systems
|
||||
if [ $(echo "$CREATED_SYSTEM_IDS" | wc -w) -gt 1 ]; then
|
||||
echo "Attempting batch delete of systems..."
|
||||
|
||||
# Use the official batch_delete endpoint
|
||||
local payload=$(echo "$CREATED_SYSTEM_IDS" | tr ' ' '\n' | jq -R . | jq -s '{system_ids: .}')
|
||||
local raw
|
||||
raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems/batch_delete" "$payload" 2>/dev/null) || true
|
||||
|
||||
# Check if batch delete was successful by looking for systems
|
||||
sleep 1
|
||||
local success=1
|
||||
|
||||
for sys_id in $CREATED_SYSTEM_IDS; do
|
||||
# Check if system still exists and is visible
|
||||
local check=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
if echo "$check" | grep -q "\"solar_system_id\":$sys_id"; then
|
||||
if echo "$check" | grep -q "\"solar_system_id\":$sys_id.*\"visible\":true"; then
|
||||
success=0
|
||||
else
|
||||
echo "System $sys_id exists but is not visible (batch delete worked)"
|
||||
fi
|
||||
else
|
||||
echo "System $sys_id no longer found (batch delete worked)"
|
||||
fi
|
||||
done
|
||||
|
||||
# If batch delete was successful for all systems, we're done
|
||||
if [ $success -eq 1 ]; then
|
||||
echo "✅ Batch delete successful for all systems"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# If batch delete failed or we have only one system, try individual deletes
|
||||
echo "Performing individual system deletions..."
|
||||
|
||||
for sys_id in $CREATED_SYSTEM_IDS; do
|
||||
echo "Deleting system $sys_id..."
|
||||
|
||||
# Try standard DELETE request
|
||||
make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$sys_id" > /dev/null 2>&1 || true
|
||||
|
||||
# Verify the system was deleted or at least made invisible
|
||||
sleep 1
|
||||
local check=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
|
||||
if echo "$check" | grep -q "\"solar_system_id\":$sys_id"; then
|
||||
if echo "$check" | grep -q "\"solar_system_id\":$sys_id.*\"visible\":true"; then
|
||||
echo "⚠️ System $sys_id is still visible after all deletion attempts"
|
||||
else
|
||||
echo "System $sys_id exists but is not visible (deletion worked)"
|
||||
fi
|
||||
else
|
||||
echo "System $sys_id no longer found (deletion worked)"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
#trap cleanup_map_systems EXIT
|
||||
Reference in New Issue
Block a user