From da2605ee03056092cf3ee01aa74dde09b8016e35 Mon Sep 17 00:00:00 2001 From: Guarzo Date: Wed, 7 May 2025 13:55:19 -0400 Subject: [PATCH 1/3] feat: improve signature undo process --- .../SignatureView/SignatureView.tsx | 2 +- .../SystemSignatureHeader.tsx | 15 +- .../SystemSignatures/SystemSignatures.tsx | 228 +++++++------ .../SystemSignaturesContent.tsx | 18 +- .../hooks/usePendingDeletions.ts | 69 +--- .../hooks/useSystemSignaturesData.ts | 16 +- assets/js/hooks/Mapper/types/mapHandlers.ts | 7 +- assets/js/hooks/Mapper/types/signatures.ts | 3 + assets/static/images/news/05-08-undo/undo.png | Bin 0 -> 6350 bytes lib/wanderer_app/api/map_system_signature.ex | 34 +- lib/wanderer_app/map/map_manager.ex | 39 ++- .../map/server/map_server_impl.ex | 6 + .../map/server/map_server_signatures_impl.ex | 300 ++++++++++-------- .../map_signatures_event_handler.ex | 33 ++ .../live/map/map_event_handler.ex | 3 +- .../2025/05-08-signature-deletion-flow.md | 70 ++++ .../20250507020200_add_deleted_signature.exs | 23 ++ .../20250507020200.json | 197 ++++++++++++ 18 files changed, 741 insertions(+), 322 deletions(-) create mode 100755 assets/static/images/news/05-08-undo/undo.png create mode 100644 priv/posts/2025/05-08-signature-deletion-flow.md create mode 100644 priv/repo/migrations/20250507020200_add_deleted_signature.exs create mode 100644 priv/resource_snapshots/repo/map_system_signatures_v1/20250507020200.json diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView/SignatureView.tsx b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView/SignatureView.tsx index 030f4029..9a53de48 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView/SignatureView.tsx +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView/SignatureView.tsx @@ -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 ( diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureHeader/SystemSignatureHeader.tsx b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureHeader/SystemSignatureHeader.tsx index 12f0c602..4a0e657b 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureHeader/SystemSignatureHeader.tsx +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureHeader/SystemSignatureHeader.tsx @@ -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(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 (
@@ -78,7 +71,9 @@ export const SystemSignaturesHeader = ({ 0 ? ` — ${undoCountdown}s left` : ''}`, + }} onClick={onUndoClick} /> )} diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx index 83fd8f93..5518f9a3 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx @@ -1,99 +1,152 @@ -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(0); - const [pendingSigs, setPendingSigs] = useState([]); - const [pendingTimeRemaining, setPendingTimeRemaining] = useState(); - const undoPendingFnRef = useRef<() => void>(() => {}); +/** + * Custom hook for managing pending signature deletions and undo countdown. + */ +function useSignatureUndo( + systemId: string | undefined, + settings: SignatureSettingsType, + outCommand: OutCommandHandler, +) { + const [pendingIds, setPendingIds] = useState([]); + const [countdown, setCountdown] = useState(0); + const intervalRef = useRef(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 => [...prev, ...ids]); }, []); - 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>, 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.length === 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([]); + return 0; + } + return prev - 1; + }); + }, 1000); + + return () => { + if (intervalRef.current != null) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [pendingIds, settings]); + + // undo handler + const handleUndo = useCallback(async () => { + if (!systemId || pendingIds.length === 0) return; + await outCommand({ + type: OutCommand.undoDeleteSignatures, + data: { system_id: systemId, eve_ids: pendingIds }, + }); + setPendingIds([]); + 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( + 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.length > 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 ( { } windowId={SIGNATURE_WINDOW_ID} > - {isNotSelectedSystem ? ( + {!isSystemSelected ? (
System is not selected
@@ -118,22 +171,17 @@ export const SystemSignatures = () => { )} + {visible && ( setVisible(false)} - onSave={handleSettingsChange} + onSave={handleSettingsSave} /> )}
diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent/SystemSignaturesContent.tsx b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent/SystemSignaturesContent.tsx index 9197c015..d0e18f25 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent/SystemSignaturesContent.tsx +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent/SystemSignaturesContent.tsx @@ -57,12 +57,8 @@ interface SystemSignaturesContentProps { onSelect?: (signature: SystemSignature) => void; onLazyDeleteChange?: (value: boolean) => void; onCountChange?: (count: number) => void; - onPendingChange?: ( - pending: React.MutableRefObject>, - 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(null); const [showSignatureSettings, setShowSignatureSettings] = useState(false); @@ -100,9 +95,8 @@ export const SystemSignaturesContent = ({ systemId, settings, onCountChange, - onPendingChange, onLazyDeleteChange, - deletionTiming, + onSignatureDeleted, }); useEffect(() => { @@ -125,6 +119,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 +153,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( diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/usePendingDeletions.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/usePendingDeletions.ts index 89f9a51b..b3d770c9 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/usePendingDeletions.ts +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/usePendingDeletions.ts @@ -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) { const { outCommand } = useMapRootState(); const pendingDeletionMapRef = useRef>({}); - // 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); diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSystemSignaturesData.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSystemSignaturesData.ts index 0e2b3295..277a8c82 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSystemSignaturesData.ts +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSystemSignaturesData.ts @@ -18,8 +18,8 @@ export const useSystemSignaturesData = ({ onCountChange, onPendingChange, onLazyDeleteChange, - deletionTiming, -}: UseSystemSignaturesDataProps) => { + onSignatureDeleted, +}: Omit & { onSignatureDeleted?: (deletedIds: string[]) => void }) => { const { outCommand } = useMapRootState(); const [signatures, setSignatures, signaturesRef] = useRefState([]); const [selectedSignatures, setSelectedSignatures] = useState([]); @@ -27,7 +27,6 @@ export const useSystemSignaturesData = ({ const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({ systemId, setSignatures, - deletionTiming, onPendingChange, }); @@ -59,6 +58,10 @@ export const useSystemSignaturesData = ({ if (removed.length > 0) { await processRemovedSignatures(removed, added, updated); + if (onSignatureDeleted) { + const deletedIds = removed.map(sig => sig.eve_id); + onSignatureDeleted(deletedIds); + } } if (updated.length !== 0 || added.length !== 0) { @@ -78,17 +81,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,7 +121,7 @@ export const useSystemSignaturesData = ({ }, [signatures]); return { - signatures, + signatures: signatures.filter(sig => !sig.deleted), selectedSignatures, setSelectedSignatures, handleDeleteSelected, diff --git a/assets/js/hooks/Mapper/types/mapHandlers.ts b/assets/js/hooks/Mapper/types/mapHandlers.ts index 1e58ddaa..d4b741f8 100644 --- a/assets/js/hooks/Mapper/types/mapHandlers.ts +++ b/assets/js/hooks/Mapper/types/mapHandlers.ts @@ -234,15 +234,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', @@ -250,7 +247,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 = (event: { type: OutCommand; data: any }) => Promise; +export type OutCommandHandler = (event: { type: OutCommand; data: unknown }) => Promise; diff --git a/assets/js/hooks/Mapper/types/signatures.ts b/assets/js/hooks/Mapper/types/signatures.ts index 0b8fe38c..ca7cdfc9 100644 --- a/assets/js/hooks/Mapper/types/signatures.ts +++ b/assets/js/hooks/Mapper/types/signatures.ts @@ -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 { diff --git a/assets/static/images/news/05-08-undo/undo.png b/assets/static/images/news/05-08-undo/undo.png new file mode 100755 index 0000000000000000000000000000000000000000..5b5a3cf1cb308c6ebbcc1c4cc35683ba58748c90 GIT binary patch literal 6350 zcmchccRZW>+sD&ION=hkQXHzJ#3nT&s%UMsdhA)8gV@AgHA+!c)TWB6+9O6#RePkQ zc1TfCJN9TKPdLx{y?*C;p4aR5-}A?veD9I_zOK)GeXs9zz4Kg0>mdW(O*#Moz@Yj_ zNf!V(6HXl$T%@6Xwh$8Is9$G1bRQ}L$_Ck&s5|HF?`z%%04fvcNmduA`Hz zEW9V%8CBieL;wI}qpEaYA8Ece9jveK6GYiud`bhF1AYdaIY*}y{eDQ{1w3r(&zi=v z=@D2_?X=%2LJyPe$$+oc^yr7$XMB0%G8nWkh(E4tD%h(jcB|ffnN8Tq?5vTBb9_4; zXC$feT&4SMEdQOmZXl5Iv%?{{9gUq(`vqGK1@iD(1{~(mT z)A{$4_-+h_DUI!6AJx|d_t|Z)AuQHrokM&CRd_V2k56AV$q?L4x7Qz zN+t`W3f*N^dZiKd+*a}8dD0HB#P>Sx}RME z(ue@tae~}7?@lEAOVoAtJg$V>hm93zbQ4%Wpy=brOzLQ08f80K_cVhJi1zIoi`2uF>G4m3GyTOPQ4Y^~Y*dU|wDxP4?S%|<4(NpJZ-+x{Gol&ZqBmAf)YVdKD3iPWh3^yNk-U#5UXe?$0BJhllx1DE4wvMR64Q>8Bq|zH}Dl zd`}n?v^QwT8Z4e5W`wglU+5j%TD5TcQF3xqWzjYio5LA?-w*?AlQg-{PR6MtV7>=J z8d}ENjl?YASnqWYjdDxLA4loGljeW_@%F}ND=k0kPAok058aS7i&9Z2@6P>cn{n~X zdOG_vu6uCus9g6&5b1NWra4i(h2XRGq*#jV)oZ5jV>Y(}PD5wPgeje+i;dwI5LgUY zk)E8KXw@1*pl~a0Jm;hs4d(%-_Tp@ zDDScV=C9OjzRbr9RLtXQvkXz-QdrOIc<}5#a>01KKFNM%#u6U^7Cc;2ixFZcWLdD= z;iqVM$`R`(iCuh@+5{Lh_Ha3XbuoBT^hwx-j|bCqGBXohs@kSjhVpyoEqd?u_6d`C z%4Ik$+xwklf<0d5m(u%^gi}0h@ZK%0D_m_`<=YDediBIUJNej4;l6mX&G ziyhrXO&f!rxy*TU+1!Cukf zO*U;kQ`7#;29L^#)stwIw_w#P)UZ0*;Vv_63G1>+_f1+ED^JQ^|9-T)f^43tf8w+6 zm8r$YDwJ7-T`6dvYD?}*J=`sa-vPZC?6b^g2%niTN1HQcWto%6#7mpDx+klHGy0o$ zuhm-}{%80NzH3!7$6r;Bnd-KK%^m4moe}oLKX3b?;qEWUmOQ0{rt6m?h^d25?+gTp z{1oxHd^qv-$o$7Rr41$R1{tn(lIp1WdQIA=g0hx>klAppGvFqRuIMe}492J;%J-?v z`V{3onA^DyppOK}yL*-fM|{R~CXJUaY-n>ETara8?`~4+qbT~xu=@G^K8Y7Y>vVI$ zV34xXnjQ0D>@Ctt4UnjPIf|BYIovVLW>s`qfXvogG|m^D!L^2K#$`JW)Um^kTfqWviVP zrEKm9j$aY9p2s@6L04Z$Z~DJ3q%Y7H43Ft`zxT^3EU6jW?VUbloxHX1^ImH$GwZ~U z)QqUmIfJ34NsWT5I3LzD+R(>9+OKYez?&a`4_R$k($8Kl zVLtQcx9-?65mF-Qo9u^VhNmwahrZ0@0z8`6SK(ZMo+ZQw1taq_`kTa%9XQcvgIdCC zYpH3XHZ<5G3?Mf*H>5*7r2^S=g^rF6@J2q6M%=ESD!N!!Ry*ehu%Mu|-Gqjkni@YC zJiAV65(U07)KO4fYCc{Y8tbo0yT-zJtAg9zKOo+Ux!n4J7bUx`w@XTIq7u3pvr7#_ zEURuBk@>O$8)xROE{SM-7m!k#E7n-#T9IhDsAwoe-&#st^MVfMphh>gmxwbDS&{`< zBJnBZ3lprUP2>;pd`_X6W=9U5X|qpcxW&tNTo?VN#AA2j1<;ewgReMTi&pm1MK^~( zY0gb;xBsfh3(bJKR5@&3Db#F=Qco2c8b_vgc4AE%J)i%9UAG#V)D;gT|cXXPKu~4;*~?N zmiLh^P*>!lrK@&tF7!( zqu6jCWL;xk7I>!uv!dd413^1h?wOvk;XrpzwGQl~U9dg;JA#yL{IONguQ^T?jIlTvN1xcH|ao+0-S z$eQ>8dTt*<2j93-_|3!Z!z&|ft7oKwf&~oH6#Br%>ajEg*~qdxXL;ao3oDUOO~{+{ z@595p-@ZAAz%t(ZJLcBT4arC~?Z++CL-k9AUpWtKpR=-D*C?F2PD5y!a#%=eN(&U* zb73X>v&=6b`_(F`*ONn1KQ3AU4doj$Bn!UFvQ%fzrMMsq*5pODDE2(Y>uy#KbZsEI za}jONeEO^^{Mc&y@{-+S#6#{7>W({E-EVaI| za53j>Sb3YwwwmY=hmx2QS=cNv4m!b%+Tvi>Kc^xVoNux|M)Z_*!*_hAB+#AvDjlvW zRL#!lWbLR4>1wKaf7I&-p!%PDm$9gT1hjI(_o2|k8b$)rYiuq(Cn8(9PmjclCi7|8{nEwjG0pq|8Z@PHb%H{%Wvh&r)?pP`M_TT`=Bk= zG6>&ifhp{U`yH9-5&`hK%ohyLwsEj!v@FM+^wvjXu2_b|%^hLv&#fMDm=We3KU1pj zq?!ONqB`3lXih?h()>jLrRc+w9cvh+GiSDgbYqpbM6|?Jy}z6AEO@}NCIyjervqYu z1s@}xHuO=w>iJlb6-$s(bG1*O;*QzkZkoMZBzyx2YShtLd&6q!M#=gzcI%e< zOHa%Ro5n=n!L*F`{QcrkUMU<>u+U#Y+VO3>Q*oy<$Kj=RF{iIJwY6Or%-$Z(maeTj zWn=jc>=X=6CbqMf}+D#7Pdqin@%)Kzc@iSz(@5z93K2#(-vH7Dmp_5J+la zPi+FGycVgLTC=sqMNA{0KD-gjCgQ++%5@g<+v~y!(%Eo(dUf@@-FJuMrA7xYA5-j~ z1j7QjCqzVU3eV+b79>JylP!wlr9G`vlqcqhu9Z>0r!(esjs**G1zf4v`(^URc{ewm z`Z^08Ezts_a&m94+v)dW*zV;R4QvER^9Cq-95OEgHRg>DlYd_7$t!B&Mmv0}NZQ(_ zG5&?oQ$l?}rDcD#A0FlKK=6n;4?TEr84L!$E1dk56Zd%2Vfxdi7XXdCf}0I|7Zvz< zR}b8KW7?`9F=iA>dA^_H)M?OCLowCep)5XH6uwK|ko~+MPuI}kt8W0g4fT2E|0ZAJ zw4ngM-He8(G9zFBiJP635_aGOt?w|Jk7Kn)Qiz_VU}wTWD@j(q&Qyxw z+Xe?{qdj}yIy(m@*NzMi9`{d~D?ffQ+350M`!5-mr6okNm9V$c_`)Cke$z#VI>Fby zX04=zL*`|<{h5l|?>Ba$U&%Cwv+hagp%Ktpuf(hSg-NB6Rxe*!@4)$r{}@we$4oFw zOH?OU5mK)pE4UY4ag{en)mTGO{ zK9C`%dhqn4p{LTiDz@d|Yjl_CB0i@=ULCl1m{^tNIN21tcpcikwgUB&lUir@4 z{{$)uzK!rX&dz45L0MFqCdPLli*sJX&K=I(CJ#+yIoJ5paJN@ZioX$jRb@{_hF3HG z0T%y4a&Hb4+nBwhb`w;UY#qSG-ir6l^o!Ni`RZ$4NU#ghs`TdKeP$|kb)C+KH`eIC z;<*MKnqSG=S1E6B)O}0^SmLD>&!|<>5uPmtRmGf``1J%6{wJjNPi_dvz%B*M;O-fF zH)ZX3(lQ5$cvTg*)b<$SlhY!~`=%&C_5N2E2n12bQBx9GzOjjl!fZ~N-sE2nw?Aq_ zpqPFYpbbuWhBW()`f2HRg)`YEfhDI5lsYe-Vz{o+UAcJv;&}wdIl=%xXPeK}%RXXjOr5PPGqK`Ki z+M`#XeFFnFqaPa%jsrb?d@`E4G*0+#?dB;GjZ6%iMsV(_`<9&A?{QDewaMd=rl$C5 z3ZxR$zvGT8RDntwEZ5cOs9<fsju6WPWUO9%7m3pF!bL5^?!ir|Ac%0836x6+yE!7qHSM~d54KL zZ)!)Z5N#>qaB39Q*e0=RqL=S4JX_@DF_)B#jINJH9-F^@{ajy{Iu-0+Zl2~p`PI}d z!&AyaaJBvJz4$Uc-R{(FYTR_HAHQ3@@l26gr2|40KAjSA=c*CTwe0&YF2d&JQPh4f zs1^S=2&dvPm7t)YZXCZ@wx)pKY*JkcdB2_NAy60`by8|u>;11%JA`s(VB>B?2`apf R`X2;9Rar}^OwlszzW^0;dZ+*Z literal 0 HcmV?d00001 diff --git a/lib/wanderer_app/api/map_system_signature.ex b/lib/wanderer_app/api/map_system_signature.ex index 6093822e..fdb9956a 100644 --- a/lib/wanderer_app/api/map_system_signature.ex +++ b/lib/wanderer_app/api/map_system_signature.ex @@ -24,7 +24,10 @@ defmodule WandererApp.Api.MapSystemSignature do ) define(:by_system_id, action: :by_system_id, args: [:system_id]) + define(:by_system_id_all, action: :by_system_id_all, args: [:system_id]) + define(:by_system_id_and_eve_ids, action: :by_system_id_and_eve_ids, args: [:system_id, :eve_ids]) define(:by_linked_system_id, action: :by_linked_system_id, args: [:linked_system_id]) + define(:by_deleted_and_updated_before!, action: :by_deleted_and_updated_before, args: [:deleted, :updated_before]) end actions do @@ -36,7 +39,8 @@ defmodule WandererApp.Api.MapSystemSignature do :description, :kind, :group, - :type + :type, + :deleted ] defaults [:read, :destroy] @@ -64,7 +68,8 @@ defmodule WandererApp.Api.MapSystemSignature do :kind, :group, :type, - :custom_info + :custom_info, + :deleted ] argument :system_id, :uuid, allow_nil?: false @@ -83,7 +88,7 @@ defmodule WandererApp.Api.MapSystemSignature do :group, :type, :custom_info, - :updated + :deleted ] primary? true @@ -105,14 +110,32 @@ defmodule WandererApp.Api.MapSystemSignature do read :by_system_id do argument(:system_id, :string, allow_nil?: false) + filter(expr(system_id == ^arg(:system_id) and deleted == false)) + end + + read :by_system_id_all do + argument(:system_id, :string, allow_nil?: false) filter(expr(system_id == ^arg(:system_id))) end + read :by_system_id_and_eve_ids do + argument(:system_id, :string, allow_nil?: false) + argument(:eve_ids, {:array, :string}, allow_nil?: false) + filter(expr(system_id == ^arg(:system_id) and eve_id in ^arg(:eve_ids))) + end + read :by_linked_system_id do argument(:linked_system_id, :integer, allow_nil?: false) filter(expr(linked_system_id == ^arg(:linked_system_id))) end + + read :by_deleted_and_updated_before do + argument(:deleted, :boolean, allow_nil?: false) + argument(:updated_before, :utc_datetime, allow_nil?: false) + + filter(expr(deleted == ^arg(:deleted) and updated_at < ^arg(:updated_before))) + end end attributes do @@ -149,7 +172,10 @@ defmodule WandererApp.Api.MapSystemSignature do allow_nil? true end - attribute :updated, :integer + attribute :deleted, :boolean do + allow_nil? false + default false + end create_timestamp(:inserted_at) update_timestamp(:updated_at) diff --git a/lib/wanderer_app/map/map_manager.ex b/lib/wanderer_app/map/map_manager.ex index 8a4dea6a..39f66019 100644 --- a/lib/wanderer_app/map/map_manager.ex +++ b/lib/wanderer_app/map/map_manager.ex @@ -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( diff --git a/lib/wanderer_app/map/server/map_server_impl.ex b/lib/wanderer_app/map/server/map_server_impl.ex index d4714a3b..54c9669e 100644 --- a/lib/wanderer_app/map/server/map_server_impl.ex +++ b/lib/wanderer_app/map/server/map_server_impl.ex @@ -93,6 +93,7 @@ defmodule WandererApp.Map.Server.Impl do Process.send_after(self(), :cleanup_connections, 5_000) Process.send_after(self(), :cleanup_systems, 10_000) Process.send_after(self(), :cleanup_characters, :timer.minutes(5)) + Process.send_after(self(), :cleanup_signatures, :timer.minutes(30)) Process.send_after(self(), :backup_state, @backup_state_timeout) WandererApp.Cache.insert("map_#{map_id}:started", true) @@ -311,6 +312,11 @@ defmodule WandererApp.Map.Server.Impl do state end + def handle_event(:cleanup_signatures, state) do + Process.send_after(self(), :cleanup_signatures, :timer.minutes(30)) + state |> SignaturesImpl.cleanup_signatures() + end + def handle_event(msg, state) do Logger.warning("Unhandled event: #{inspect(msg)}") diff --git a/lib/wanderer_app/map/server/map_server_signatures_impl.ex b/lib/wanderer_app/map/server/map_server_signatures_impl.ex index bb755886..7ab3c665 100644 --- a/lib/wanderer_app/map/server/map_server_signatures_impl.ex +++ b/lib/wanderer_app/map/server/map_server_signatures_impl.ex @@ -3,147 +3,183 @@ 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, + state = %{map_id: map_id}, %{ - 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, + %{sig | deleted: false} + ) + + _ -> + :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 + MapSystemSignature.update(existing, update_params) + 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 diff --git a/lib/wanderer_app_web/live/map/event_handlers/map_signatures_event_handler.ex b/lib/wanderer_app_web/live/map/event_handlers/map_signatures_event_handler.ex index 9f55d9b2..6279812b 100644 --- a/lib/wanderer_app_web/live/map/event_handlers/map_signatures_event_handler.ex +++ b/lib/wanderer_app_web/live/map/event_handlers/map_signatures_event_handler.ex @@ -269,6 +269,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) diff --git a/lib/wanderer_app_web/live/map/map_event_handler.ex b/lib/wanderer_app_web/live/map/map_event_handler.ex index 5d32dea3..fecbdcc2 100644 --- a/lib/wanderer_app_web/live/map/map_event_handler.ex +++ b/lib/wanderer_app_web/live/map/map_event_handler.ex @@ -116,7 +116,8 @@ defmodule WandererAppWeb.MapEventHandler do "update_signatures", "get_signatures", "link_signature_to_system", - "unlink_signature" + "unlink_signature", + "undo_delete_signatures" ] @map_structures_events [ diff --git a/priv/posts/2025/05-08-signature-deletion-flow.md b/priv/posts/2025/05-08-signature-deletion-flow.md new file mode 100644 index 00000000..7e7e9569 --- /dev/null +++ b/priv/posts/2025/05-08-signature-deletion-flow.md @@ -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** + +--- \ No newline at end of file diff --git a/priv/repo/migrations/20250507020200_add_deleted_signature.exs b/priv/repo/migrations/20250507020200_add_deleted_signature.exs new file mode 100644 index 00000000..70d5724d --- /dev/null +++ b/priv/repo/migrations/20250507020200_add_deleted_signature.exs @@ -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 diff --git a/priv/resource_snapshots/repo/map_system_signatures_v1/20250507020200.json b/priv/resource_snapshots/repo/map_system_signatures_v1/20250507020200.json new file mode 100644 index 00000000..d733fc42 --- /dev/null +++ b/priv/resource_snapshots/repo/map_system_signatures_v1/20250507020200.json @@ -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" +} \ No newline at end of file From 1364779f810e711977ea6d5f333feb6cb48799e6 Mon Sep 17 00:00:00 2001 From: Guarzo Date: Wed, 14 May 2025 09:44:38 -0400 Subject: [PATCH 2/3] feat: support german and french signatures --- .../SystemSignatures/SystemSignatures.tsx | 26 +- .../SystemSignaturesContent.tsx | 241 ++++++++++-------- .../widgets/SystemSignatures/constants.ts | 137 +++++++--- .../hooks/useSystemSignaturesData.ts | 19 +- .../hooks/Mapper/helpers/parseSignatures.ts | 39 ++- assets/js/hooks/Mapper/types/signatures.ts | 40 +++ lib/wanderer_app/api/map_system_signature.ex | 2 +- .../map/server/map_server_signatures_impl.ex | 8 +- 8 files changed, 344 insertions(+), 168 deletions(-) diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx index 5518f9a3..3800b871 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx @@ -25,12 +25,16 @@ function useSignatureUndo( settings: SignatureSettingsType, outCommand: OutCommandHandler, ) { - const [pendingIds, setPendingIds] = useState([]); - const [countdown, setCountdown] = useState(0); + const [countdown, setCountdown] = useState(0); + const [pendingIds, setPendingIds] = useState>(new Set()); const intervalRef = useRef(null); const addDeleted = useCallback((ids: string[]) => { - setPendingIds(prev => [...prev, ...ids]); + setPendingIds(prev => { + const next = new Set(prev); + ids.forEach(id => next.add(id)); + return next; + }); }, []); // kick off or clear countdown whenever pendingIds changes @@ -41,7 +45,7 @@ function useSignatureUndo( intervalRef.current = null; } - if (pendingIds.length === 0) { + if (pendingIds.size === 0) { setCountdown(0); return; } @@ -58,7 +62,7 @@ function useSignatureUndo( if (prev <= 1) { clearInterval(intervalRef.current!); intervalRef.current = null; - setPendingIds([]); + setPendingIds(new Set()); return 0; } return prev - 1; @@ -71,16 +75,16 @@ function useSignatureUndo( intervalRef.current = null; } }; - }, [pendingIds, settings]); + }, [pendingIds, settings[SETTINGS_KEYS.DELETION_TIMING]]); // undo handler const handleUndo = useCallback(async () => { - if (!systemId || pendingIds.length === 0) return; + if (!systemId || pendingIds.size === 0) return; await outCommand({ type: OutCommand.undoDeleteSignatures, - data: { system_id: systemId, eve_ids: pendingIds }, + data: { system_id: systemId, eve_ids: Array.from(pendingIds) }, }); - setPendingIds([]); + setPendingIds(new Set()); setCountdown(0); if (intervalRef.current != null) { clearInterval(intervalRef.current); @@ -117,7 +121,7 @@ export const SystemSignatures = () => { const { pendingIds, countdown, addDeleted, handleUndo } = useSignatureUndo(systemId, currentSettings, outCommand); useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => { - if (pendingIds.length > 0 && countdown > 0) { + if (pendingIds.size > 0 && countdown > 0) { event.preventDefault(); event.stopPropagation(); handleUndo(); @@ -154,7 +158,7 @@ export const SystemSignatures = () => { { if (selectable) return; @@ -186,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; @@ -234,113 +245,121 @@ export const SystemSignaturesContent = ({ No signatures
) : ( - - - - sig.group ?? ''} - hidden={isCompact} - sortable - /> - + )} = { [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 = (() => { + const flattened: Record = {}; + 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 = (() => { + const flattened: Record = {}; + for (const [, mappings] of Object.entries(LANGUAGE_TYPE_MAPPINGS)) { + Object.assign(flattened, mappings); + } + return flattened; +})(); diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSystemSignaturesData.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSystemSignaturesData.ts index 277a8c82..5f05a3fc 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSystemSignaturesData.ts +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/hooks/useSystemSignaturesData.ts @@ -19,10 +19,13 @@ export const useSystemSignaturesData = ({ onPendingChange, onLazyDeleteChange, onSignatureDeleted, -}: Omit & { onSignatureDeleted?: (deletedIds: string[]) => void }) => { +}: Omit & { + onSignatureDeleted?: (deletedIds: string[]) => void; +}) => { const { outCommand } = useMapRootState(); const [signatures, setSignatures, signaturesRef] = useRefState([]); const [selectedSignatures, setSelectedSignatures] = useState([]); + const [hasUnsupportedLanguage, setHasUnsupportedLanguage] = useState(false); const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({ systemId, @@ -41,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), @@ -50,6 +54,18 @@ export const useSystemSignaturesData = ({ return; } + // Check if any signatures might be using unsupported languages + // This is a basic heuristic: if we have signatures where the original group wasn't mapped + const clipboardRows = clipboardString.split('\n').filter(row => row.trim() !== ''); + const detectedSignatureCount = clipboardRows.filter(row => row.match(/^[A-Z]{3}-\d{3}/)).length; + + // If we detected valid IDs but got fewer parsed signatures, we might have language issues + if (detectedSignatureCount > 0 && incomingSignatures.length < detectedSignatureCount) { + setHasUnsupportedLanguage(true); + } else { + setHasUnsupportedLanguage(false); + } + const currentNonPending = lazyDeleteValue ? signaturesRef.current.filter(sig => !sig.pendingDeletion) : signaturesRef.current.filter(sig => !sig.pendingDeletion || !sig.pendingAddition); @@ -127,5 +143,6 @@ export const useSystemSignaturesData = ({ handleDeleteSelected, handleSelectAll, handlePaste, + hasUnsupportedLanguage, }; }; diff --git a/assets/js/hooks/Mapper/helpers/parseSignatures.ts b/assets/js/hooks/Mapper/helpers/parseSignatures.ts index 628267ce..68c3dcdc 100644 --- a/assets/js/hooks/Mapper/helpers/parseSignatures.ts +++ b/assets/js/hooks/Mapper/helpers/parseSignatures.ts @@ -1,5 +1,8 @@ import { SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types'; -import { MAPPING_TYPE_TO_ENG } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts'; +import { + MAPPING_GROUP_TO_ENG, + MAPPING_TYPE_TO_ENG, +} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts'; export const parseSignatures = (value: string, availableKeys: string[]): SystemSignature[] => { const outArr: SystemSignature[] = []; @@ -14,13 +17,37 @@ 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', type: '', }; diff --git a/assets/js/hooks/Mapper/types/signatures.ts b/assets/js/hooks/Mapper/types/signatures.ts index ca7cdfc9..11269019 100644 --- a/assets/js/hooks/Mapper/types/signatures.ts +++ b/assets/js/hooks/Mapper/types/signatures.ts @@ -78,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', @@ -97,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', +} diff --git a/lib/wanderer_app/api/map_system_signature.ex b/lib/wanderer_app/api/map_system_signature.ex index 6a7d8d1e..1925307a 100644 --- a/lib/wanderer_app/api/map_system_signature.ex +++ b/lib/wanderer_app/api/map_system_signature.ex @@ -204,7 +204,7 @@ defmodule WandererApp.Api.MapSystemSignature do :kind, :group, :custom_info, - :updated, + :deleted, :inserted_at, :updated_at ] diff --git a/lib/wanderer_app/map/server/map_server_signatures_impl.ex b/lib/wanderer_app/map/server/map_server_signatures_impl.ex index 7ab3c665..d9cea2c2 100644 --- a/lib/wanderer_app/map/server/map_server_signatures_impl.ex +++ b/lib/wanderer_app/map/server/map_server_signatures_impl.ex @@ -97,7 +97,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do %MapSystemSignature{deleted: true} = deleted_sig -> MapSystemSignature.update!( deleted_sig, - %{sig | deleted: false} + Map.take(sig, [:name, :description, :kind, :group, :type, :custom_info, :deleted]) ) _ -> @@ -152,7 +152,11 @@ defmodule WandererApp.Map.Server.SignaturesImpl do defp apply_update_signature(%MapSystemSignature{} = existing, update_params) when not is_nil(update_params) do - MapSystemSignature.update(existing, update_params) + case MapSystemSignature.update(existing, update_params) do + {:ok, _} -> :ok + {:error, reason} -> + Logger.error("Failed to update signature #{existing.id}: #{inspect(reason)}") + end end defp track_activity(event, map_id, solar_system_id, user_id, character_id, signatures) do From 331db10029ee7514e5f0b669f8057f91f6a7b88c Mon Sep 17 00:00:00 2001 From: Guarzo Date: Mon, 19 May 2025 10:28:46 -0400 Subject: [PATCH 3/3] fix: update openapi spec for other apis --- .../map/server/map_server_signatures_impl.ex | 2 +- .../controllers/access_list_api_controller.ex | 72 ++++----------- .../controllers/map_api_controller.ex | 12 ++- .../map_connection_api_controller.ex | 88 ++++++++++++++++--- .../controllers/map_system_api_controller.ex | 81 +++++++++++++---- .../map_system_structure_api_controller.ex | 20 +++-- lib/wanderer_app_web/router.ex | 2 +- 7 files changed, 178 insertions(+), 99 deletions(-) diff --git a/lib/wanderer_app/map/server/map_server_signatures_impl.ex b/lib/wanderer_app/map/server/map_server_signatures_impl.ex index d9cea2c2..7792e5e1 100644 --- a/lib/wanderer_app/map/server/map_server_signatures_impl.ex +++ b/lib/wanderer_app/map/server/map_server_signatures_impl.ex @@ -12,7 +12,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do Public entrypoint for updating signatures on a map system. """ def update_signatures( - state = %{map_id: map_id}, + %{map_id: map_id} = state, %{ solar_system_id: system_solar_id, character_id: char_id, diff --git a/lib/wanderer_app_web/controllers/access_list_api_controller.ex b/lib/wanderer_app_web/controllers/access_list_api_controller.ex index c1104230..df86737b 100644 --- a/lib/wanderer_app_web/controllers/access_list_api_controller.ex +++ b/lib/wanderer_app_web/controllers/access_list_api_controller.ex @@ -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 diff --git a/lib/wanderer_app_web/controllers/map_api_controller.ex b/lib/wanderer_app_web/controllers/map_api_controller.ex index be2d110e..ba8b1f30 100644 --- a/lib/wanderer_app_web/controllers/map_api_controller.ex +++ b/lib/wanderer_app_web/controllers/map_api_controller.ex @@ -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: [ diff --git a/lib/wanderer_app_web/controllers/map_connection_api_controller.ex b/lib/wanderer_app_web/controllers/map_connection_api_controller.ex index 2c9c162a..c9f73f78 100644 --- a/lib/wanderer_app_web/controllers/map_connection_api_controller.ex +++ b/lib/wanderer_app_web/controllers/map_connection_api_controller.ex @@ -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) diff --git a/lib/wanderer_app_web/controllers/map_system_api_controller.ex b/lib/wanderer_app_web/controllers/map_system_api_controller.ex index 6cc91911..29f45da9 100644 --- a/lib/wanderer_app_web/controllers/map_system_api_controller.ex +++ b/lib/wanderer_app_web/controllers/map_system_api_controller.ex @@ -290,10 +290,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 +314,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 +339,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 +363,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 +391,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 +438,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 +477,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 +498,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 diff --git a/lib/wanderer_app_web/controllers/map_system_structure_api_controller.ex b/lib/wanderer_app_web/controllers/map_system_structure_api_controller.ex index 857b8434..c45d8681 100644 --- a/lib/wanderer_app_web/controllers/map_system_structure_api_controller.ex +++ b/lib/wanderer_app_web/controllers/map_system_structure_api_controller.ex @@ -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) diff --git a/lib/wanderer_app_web/router.ex b/lib/wanderer_app_web/router.ex index 276dfbca..a3338df8 100644 --- a/lib/wanderer_app_web/router.ex +++ b/lib/wanderer_app_web/router.ex @@ -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