mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-01 05:23:22 +00:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40672f6a47 | ||
|
|
6d66ae3f50 | ||
|
|
94c89e0325 | ||
|
|
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 | ||
|
|
cb318aa6c6 | ||
|
|
1a27b21efe | ||
|
|
e57f565812 | ||
|
|
da2605ee03 |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -157,6 +157,7 @@ jobs:
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
|
||||
115
CHANGELOG.md
115
CHANGELOG.md
@@ -2,6 +2,121 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.65.4](https://github.com/wanderer-industries/wanderer/compare/v1.65.3...v1.65.4) (2025-05-24)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Signature: Force signature update even if there are no any changes
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -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,14 +54,30 @@ 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);
|
||||
|
||||
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, true);
|
||||
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, false);
|
||||
|
||||
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 |
@@ -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
|
||||
|
||||
@@ -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,19 @@ 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 +48,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:description,
|
||||
:kind,
|
||||
:group,
|
||||
:type
|
||||
:type,
|
||||
:deleted
|
||||
]
|
||||
|
||||
defaults [:read, :destroy]
|
||||
@@ -64,7 +77,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:kind,
|
||||
:group,
|
||||
:type,
|
||||
:custom_info
|
||||
:custom_info,
|
||||
:deleted
|
||||
]
|
||||
|
||||
argument :system_id, :uuid, allow_nil?: false
|
||||
@@ -83,7 +97,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:group,
|
||||
:type,
|
||||
:custom_info,
|
||||
:updated
|
||||
:deleted,
|
||||
:update_forced_at
|
||||
]
|
||||
|
||||
primary? true
|
||||
@@ -105,14 +120,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 +182,14 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :updated, :integer
|
||||
attribute :deleted, :boolean do
|
||||
allow_nil? false
|
||||
default false
|
||||
end
|
||||
|
||||
attribute :update_forced_at, :utc_datetime do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
@@ -166,21 +206,20 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
end
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
only: [
|
||||
:id,
|
||||
:system_id,
|
||||
:eve_id,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:description,
|
||||
:type,
|
||||
:linked_system_id,
|
||||
:kind,
|
||||
:group,
|
||||
:custom_info,
|
||||
:updated,
|
||||
:inserted_at,
|
||||
:updated_at
|
||||
]
|
||||
}
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -139,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
|
||||
@@ -191,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
|
||||
|
||||
@@ -305,16 +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
|
||||
is_online: false
|
||||
})
|
||||
|
||||
:ok
|
||||
|
||||
@@ -34,6 +34,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|
||||
def start(state) do
|
||||
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
WandererApp.Cache.insert("tracked_characters", [])
|
||||
|
||||
tracked_characters
|
||||
|> Enum.each(fn character_id ->
|
||||
@@ -51,6 +52,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
tracked_characters = [character_id | characters] |> Enum.uniq()
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
is_online: false
|
||||
})
|
||||
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
|
||||
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
|
||||
|
||||
@@ -177,6 +177,10 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
characters
|
||||
|> Enum.each(fn character_id ->
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
is_online: false
|
||||
})
|
||||
end)
|
||||
|
||||
{:noreply, state}
|
||||
|
||||
@@ -102,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
|
||||
@@ -302,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(
|
||||
@@ -325,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}
|
||||
@@ -339,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] || "")
|
||||
@@ -372,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(
|
||||
@@ -380,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
|
||||
@@ -405,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(","),
|
||||
@@ -416,7 +427,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
get(
|
||||
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
|
||||
opts
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
end
|
||||
|
||||
@@ -426,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 =
|
||||
@@ -445,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,
|
||||
@@ -456,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)
|
||||
|
||||
@@ -465,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
|
||||
@@ -513,7 +527,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|> Keyword.merge(@timeout_opts)
|
||||
) do
|
||||
{:ok, %{status: 200, body: body, headers: headers}} ->
|
||||
maybe_cache_response(path, body, headers)
|
||||
maybe_cache_response(path, body, headers, opts)
|
||||
|
||||
{:ok, body}
|
||||
|
||||
@@ -530,11 +544,9 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
get_retry(path, api_opts, opts, :error_limited)
|
||||
|
||||
{:ok, %{status: status}} ->
|
||||
IO.inspect(status)
|
||||
{:error, "Unexpected status: #{status}"}
|
||||
|
||||
{:error, _reason} ->
|
||||
IO.inspect(_reason)
|
||||
{:error, "Request failed"}
|
||||
end
|
||||
rescue
|
||||
@@ -545,18 +557,20 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_cache_response(path, body, %{"expires" => [expires]})
|
||||
defp maybe_cache_response(path, body, %{"expires" => [expires]}, opts)
|
||||
when is_binary(path) and not is_nil(expires) do
|
||||
try do
|
||||
cached_ttl =
|
||||
DateTime.diff(Timex.parse!(expires, "{RFC1123}"), DateTime.utc_now(), :millisecond)
|
||||
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
|
||||
)
|
||||
Cachex.put(
|
||||
:api_cache,
|
||||
path,
|
||||
body,
|
||||
ttl: cached_ttl
|
||||
)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
@logger.error(Exception.message(e))
|
||||
@@ -565,7 +579,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_cache_response(_path, _body, _headers), do: :ok
|
||||
defp maybe_cache_response(_path, _body, _headers, _opts), do: :ok
|
||||
|
||||
defp post(url, opts) do
|
||||
try do
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,10 +187,7 @@ 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
|
||||
|
||||
@@ -1,12 +1,122 @@
|
||||
defmodule WandererApp.Map.Operations.Connections do
|
||||
@moduledoc """
|
||||
CRUD and batch upsert for map connections.
|
||||
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
|
||||
alias WandererApp.Map.Server
|
||||
require Logger
|
||||
|
||||
# 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
|
||||
@@ -38,52 +148,6 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_connection(Plug.Conn.t(), map()) :: {:ok, map()} | {: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
|
||||
|
||||
def create_connection(map_id, attrs, 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") do
|
||||
info = %{
|
||||
solar_system_source_id: source,
|
||||
solar_system_target_id: target,
|
||||
character_id: char_id,
|
||||
type: parse_type(attrs["type"])
|
||||
}
|
||||
add_result = Server.add_connection(map_id, info)
|
||||
case add_result do
|
||||
:ok -> {:ok, :created}
|
||||
{:ok, []} ->
|
||||
Logger.warning("[do_create] Server.add_connection returned :ok, [] for map_id=#{inspect(map_id)}, source=#{inspect(source)}, target=#{inspect(target)}")
|
||||
{:error, :inconsistent_state}
|
||||
{:error, %Invalid{errors: errors}} = err ->
|
||||
if Enum.any?(errors, &is_unique_constraint_error?/1), do: {:skip, :exists}, else: err
|
||||
{:error, _} = err ->
|
||||
Logger.error("[do_create] Server.add_connection error: #{inspect(err)}")
|
||||
{:error, :server_error}
|
||||
_ ->
|
||||
Logger.error("[do_create] Unexpected add_result: #{inspect(add_result)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
else
|
||||
{:ok, []} ->
|
||||
Logger.warning("[do_create] Source or target system not found: attrs=#{inspect(attrs)}")
|
||||
{:error, :inconsistent_state}
|
||||
{:error, _} = err ->
|
||||
Logger.error("[do_create] parse_int error: #{inspect(err)}, attrs=#{inspect(attrs)}")
|
||||
{:error, :parse_error}
|
||||
_ ->
|
||||
Logger.error("[do_create] Unexpected error in preconditions: attrs=#{inspect(attrs)}")
|
||||
{:error, :unexpected_precondition_error}
|
||||
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),
|
||||
@@ -185,29 +249,6 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
|
||||
# -- Helpers ---------------------------------------------------------------
|
||||
|
||||
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 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
|
||||
_ -> 0
|
||||
end
|
||||
end
|
||||
defp parse_type(_), do: 0
|
||||
|
||||
defp is_unique_constraint_error?(%{constraint: :unique}), do: true
|
||||
defp is_unique_constraint_error?(%{constraint: :unique_constraint}), do: true
|
||||
defp is_unique_constraint_error?(_), do: false
|
||||
|
||||
defp apply_connection_updates(map_id, conn, attrs, _char_id) do
|
||||
Enum.reduce_while(attrs, :ok, fn {key, val}, _acc ->
|
||||
result =
|
||||
@@ -256,4 +297,16 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
})
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -173,10 +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()
|
||||
)
|
||||
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
|
||||
@@ -351,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
|
||||
@@ -495,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,211 @@ 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,
|
||||
:update_forced_at
|
||||
])
|
||||
)
|
||||
|
||||
_ ->
|
||||
: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 |> Map.put(:update_forced_at, DateTime.utc_now())
|
||||
) do
|
||||
{:ok, _updated} ->
|
||||
: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}
|
||||
|
||||
@@ -9,6 +9,7 @@ defmodule WandererApp.Structure 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))
|
||||
@@ -107,7 +108,11 @@ defmodule WandererApp.Structure do
|
||||
updated_data = Map.delete(updated_data, :id)
|
||||
|
||||
# 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)
|
||||
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,
|
||||
@@ -124,10 +129,18 @@ defmodule WandererApp.Structure do
|
||||
:status,
|
||||
:end_time
|
||||
]
|
||||
|
||||
filtered_data = Map.take(merged_data, allowed_keys)
|
||||
Logger.info("[Structure] update_structures_in_db: calling update for id=#{existing.id} with: #{inspect(filtered_data)}")
|
||||
|
||||
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.info("[Structure] update_structures_in_db: update result for id=#{existing.id}: #{inspect(new_record)}")
|
||||
|
||||
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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -200,48 +200,28 @@ 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
|
||||
@@ -277,46 +257,30 @@ 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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -246,7 +246,6 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
in: :query,
|
||||
description: "Map slug",
|
||||
type: :string,
|
||||
example: "my-map",
|
||||
required: false
|
||||
],
|
||||
map_id: [
|
||||
@@ -319,7 +318,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
end
|
||||
|
||||
@doc """
|
||||
GET /api/map/structure_timers
|
||||
GET /api/map/structure-timers
|
||||
|
||||
Returns structure timers for visible systems on the map or for a specific system.
|
||||
"""
|
||||
@@ -327,6 +326,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
operation :show_structure_timers,
|
||||
summary: "Show Structure Timers",
|
||||
description: "Retrieves structure timers for a map.",
|
||||
deprecated: true,
|
||||
parameters: [
|
||||
map_id: [
|
||||
in: :query,
|
||||
@@ -342,7 +342,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
],
|
||||
system_id: [
|
||||
in: :query,
|
||||
description: "System ID",
|
||||
description: "Optional: System ID to filter timers for a specific system",
|
||||
type: :string,
|
||||
required: false
|
||||
]
|
||||
@@ -790,15 +790,13 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
in: :query,
|
||||
description: "Map identifier (UUID) - Either map_id or slug must be provided",
|
||||
type: :string,
|
||||
required: false,
|
||||
example: ""
|
||||
required: false
|
||||
],
|
||||
slug: [
|
||||
in: :query,
|
||||
description: "Map slug - Either map_id or slug must be provided",
|
||||
type: :string,
|
||||
required: false,
|
||||
example: "map-name"
|
||||
required: false
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
|
||||
@@ -2,15 +2,9 @@ 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.Helpers.APIUtils
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -129,23 +129,46 @@ defmodule WandererAppWeb.MapConnectionAPIController do
|
||||
|
||||
operation :index,
|
||||
summary: "List Map Connections",
|
||||
description: "Lists all connections for a map.",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
solar_system_source: [in: :query, type: :integer, required: false],
|
||||
solar_system_target: [in: :query, type: :integer, required: false]
|
||||
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 Map Connections",
|
||||
"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"),
|
||||
@@ -187,7 +210,7 @@ defmodule WandererAppWeb.MapConnectionAPIController do
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
@@ -218,12 +241,11 @@ defmodule WandererAppWeb.MapConnectionAPIController do
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
system_id: [in: :path, type: :string, required: false]
|
||||
]
|
||||
],
|
||||
request_body: {"Connection create", "application/json", @connection_request_schema},
|
||||
responses: ResponseSchemas.create_responses(@detail_response_schema)
|
||||
@@ -256,7 +278,7 @@ defmodule WandererAppWeb.MapConnectionAPIController do
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
@@ -344,7 +366,7 @@ defmodule WandererAppWeb.MapConnectionAPIController do
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
@@ -442,9 +464,49 @@ defmodule WandererAppWeb.MapConnectionAPIController do
|
||||
@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]],
|
||||
responses: ResponseSchemas.standard_responses(@list_response_schema)
|
||||
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)
|
||||
|
||||
@@ -24,15 +24,18 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
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: :number, format: :float, description: "X coordinate"},
|
||||
position_y: %Schema{type: :number, format: :float, description: "Y coordinate"},
|
||||
status: %Schema{type: :string, description: "System status"},
|
||||
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: :array, items: %Schema{type: :string}, nullable: true, description: "Labels"}
|
||||
labels: %Schema{type: :string, description: "Comma-separated list of labels"}
|
||||
},
|
||||
required: ~w(id map_id solar_system_id)a
|
||||
}
|
||||
@@ -42,23 +45,27 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
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: :number, format: :float, description: "X coordinate"},
|
||||
position_y: %Schema{type: :number, format: :float, description: "Y coordinate"},
|
||||
status: %Schema{type: :string, description: "System status"},
|
||||
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: :array, items: %Schema{type: :string}, nullable: true, description: "Labels"}
|
||||
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.5,
|
||||
position_y: 200.3,
|
||||
visible: true
|
||||
position_x: 100,
|
||||
position_y: 200,
|
||||
visible: true,
|
||||
labels: "market,hub"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,24 +73,29 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_name: %Schema{type: :string, description: "EVE solar system name", nullable: true},
|
||||
position_x: %Schema{type: :number, format: :float, description: "X coordinate", nullable: true},
|
||||
position_y: %Schema{type: :number, format: :float, description: "Y coordinate", nullable: true},
|
||||
status: %Schema{type: :string, description: "System status", 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: :array, items: %Schema{type: :string}, nullable: true, description: "Labels"}
|
||||
labels: %Schema{type: :string, description: "Comma-separated list of labels"}
|
||||
},
|
||||
example: %{
|
||||
solar_system_name: "Jita",
|
||||
position_x: 101.0,
|
||||
position_y: 202.0,
|
||||
position_x: 101,
|
||||
position_y: 202,
|
||||
visible: false,
|
||||
status: "active",
|
||||
status: 0,
|
||||
tag: "HQ",
|
||||
locked: true
|
||||
locked: true,
|
||||
labels: "market,hub"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,10 +302,10 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "my-map-slug or map UUID"
|
||||
example: "map-slug or map UUID"
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
@@ -314,12 +326,17 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "my-map-slug or map UUID"
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
id: [in: :path, type: :string, required: true]
|
||||
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
|
||||
@@ -334,10 +351,10 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "my-map-slug or map UUID"
|
||||
example: "map-slug or map UUID"
|
||||
]
|
||||
],
|
||||
request_body: {"Systems+Connections upsert", "application/json", @batch_request_schema},
|
||||
@@ -358,12 +375,17 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "my-map-slug or map UUID"
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
id: [in: :path, type: :string, required: true]
|
||||
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)
|
||||
@@ -381,10 +403,10 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "my-map-slug or map UUID"
|
||||
example: "map-slug or map UUID"
|
||||
]
|
||||
],
|
||||
request_body: {"Batch delete", "application/json", @batch_delete_schema},
|
||||
@@ -428,12 +450,17 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "my-map-slug or map UUID"
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
id: [in: :path, type: :string, required: true]
|
||||
id: [
|
||||
in: :path,
|
||||
description: "System ID",
|
||||
type: :string,
|
||||
required: true
|
||||
]
|
||||
],
|
||||
responses: ResponseSchemas.standard_responses(@delete_response_schema)
|
||||
def delete_single(conn, %{"id" => id}) do
|
||||
@@ -462,7 +489,20 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
summary: "List Map Systems (Legacy)",
|
||||
deprecated: true,
|
||||
description: "Deprecated, use GET /api/maps/:map_identifier/systems instead",
|
||||
parameters: [map_id: [in: :query]],
|
||||
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
|
||||
|
||||
@@ -470,7 +510,26 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
summary: "Show Map System (Legacy)",
|
||||
deprecated: true,
|
||||
description: "Deprecated, use GET /api/maps/:map_identifier/systems/:id instead",
|
||||
parameters: [map_id: [in: :query], id: [in: :query]],
|
||||
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
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ defmodule WandererAppWeb.MapSystemStructureAPIController do
|
||||
|
||||
@moduledoc """
|
||||
API controller for managing map system structures.
|
||||
Includes legacy structure-timers endpoint (deprecated).
|
||||
"""
|
||||
|
||||
# Inlined OpenAPI schema for a map system structure
|
||||
@@ -174,16 +173,25 @@ defmodule WandererAppWeb.MapSystemStructureAPIController do
|
||||
end
|
||||
|
||||
@doc """
|
||||
@deprecated "Use /structures instead. This endpoint will be removed in a future release."
|
||||
Legacy: Get structure timers for a map.
|
||||
Get structure timers for a map.
|
||||
"""
|
||||
operation :structure_timers,
|
||||
summary: "Get structure timers for a map (Legacy)",
|
||||
deprecated: true,
|
||||
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: :array, items: %Schema{type: :object}}}]
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -212,7 +212,7 @@ defmodule WandererAppWeb.Router do
|
||||
get "/system", MapSystemAPIController, :show_system
|
||||
get "/connections", MapConnectionAPIController, :list_all_connections
|
||||
get "/characters", MapAPIController, :list_tracked_characters
|
||||
get "/structure-timers", MapSystemStructureAPIController, :structure_timers
|
||||
get "/structure-timers", MapAPIController, :show_structure_timers
|
||||
get "/character-activity", MapAPIController, :character_activity
|
||||
get "/user_characters", MapAPIController, :user_characters
|
||||
|
||||
|
||||
@@ -38,14 +38,23 @@ defmodule WandererAppWeb.Schemas.ApiSchemas do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
eve_id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
corporation_id: %Schema{type: :string},
|
||||
corporation_ticker: %Schema{type: :string},
|
||||
alliance_id: %Schema{type: :string},
|
||||
alliance_ticker: %Schema{type: :string}
|
||||
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: ["eve_id", "name"]
|
||||
required: ~w(eve_id name)a
|
||||
}
|
||||
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.64.2"
|
||||
@version "1.65.4"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
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**
|
||||
|
||||
---
|
||||
@@ -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,21 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddSigUpdateForcedAt 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
|
||||
add :update_forced_at, :utc_datetime
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:map_system_signatures_v1) do
|
||||
remove :update_forced_at
|
||||
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"
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"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?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "update_forced_at",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"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": "915C0896211ECCB6C38871664117E7D470C794825536E7F0887DC5B92681F17B",
|
||||
"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"
|
||||
}
|
||||
Reference in New Issue
Block a user