diff --git a/assets/js/hooks/Mapper/components/map/components/SolarSystemNode/SolarSystemNodeDefault.tsx b/assets/js/hooks/Mapper/components/map/components/SolarSystemNode/SolarSystemNodeDefault.tsx index 298f178f..43a5a262 100644 --- a/assets/js/hooks/Mapper/components/map/components/SolarSystemNode/SolarSystemNodeDefault.tsx +++ b/assets/js/hooks/Mapper/components/map/components/SolarSystemNode/SolarSystemNodeDefault.tsx @@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow'; import clsx from 'clsx'; import classes from './SolarSystemNodeDefault.module.scss'; import { PrimeIcons } from 'primereact/api'; -import { useLocalCounter, useSolarSystemNode, useNodeKillsCount } from '../../hooks/useSolarSystemLogic'; +import { useLocalCounter, useSolarSystemNode, useNodeKillsCount } from '../../hooks'; import { EFFECT_BACKGROUND_STYLES, MARKER_BOOKMARK_BG_STYLES, diff --git a/assets/js/hooks/Mapper/components/map/components/SolarSystemNode/SolarSystemNodeTheme.tsx b/assets/js/hooks/Mapper/components/map/components/SolarSystemNode/SolarSystemNodeTheme.tsx index ac746766..83d33518 100644 --- a/assets/js/hooks/Mapper/components/map/components/SolarSystemNode/SolarSystemNodeTheme.tsx +++ b/assets/js/hooks/Mapper/components/map/components/SolarSystemNode/SolarSystemNodeTheme.tsx @@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow'; import clsx from 'clsx'; import classes from './SolarSystemNodeTheme.module.scss'; import { PrimeIcons } from 'primereact/api'; -import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks/useSolarSystemLogic'; +import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks'; import { EFFECT_BACKGROUND_STYLES, MARKER_BOOKMARK_BG_STYLES, diff --git a/assets/js/hooks/Mapper/components/map/hooks/index.ts b/assets/js/hooks/Mapper/components/map/hooks/index.ts index 708fcae9..03fd94c4 100644 --- a/assets/js/hooks/Mapper/components/map/hooks/index.ts +++ b/assets/js/hooks/Mapper/components/map/hooks/index.ts @@ -1,3 +1,11 @@ export * from './useMapHandlers'; export * from './useUpdateNodes'; export * from './useNodesEdgesState'; +export * from './useBackgroundVars'; +export * from './useKillsCounter'; +export * from './useSystemName'; +export * from './useNodesEdgesState'; +export * from './useSolarSystemNode'; +export * from './useUnsplashedSignatures'; +export * from './useUpdateNodes'; +export * from './useNodeKillsCount'; diff --git a/assets/js/hooks/Mapper/components/map/hooks/useLabelsInfo.ts b/assets/js/hooks/Mapper/components/map/hooks/useLabelsInfo.ts new file mode 100644 index 00000000..56eae645 --- /dev/null +++ b/assets/js/hooks/Mapper/components/map/hooks/useLabelsInfo.ts @@ -0,0 +1,31 @@ +import { useMemo } from 'react'; +import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager'; +import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants'; +interface UseLabelsInfoParams { + labels: string | null; + linkedSigPrefix: string | null; + isShowLinkedSigId: boolean; +} + +export type LabelInfo = { + id: string; + shortName: string; +}; + +function sortedLabels(labels: string[]): LabelInfo[] { + if (!labels) return []; + return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x] as LabelInfo); +} + +export function useLabelsInfo({ labels, linkedSigPrefix, isShowLinkedSigId }: UseLabelsInfoParams) { + const labelsManager = useMemo(() => new LabelsManager(labels ?? ''), [labels]); + const labelsInfo = useMemo(() => sortedLabels(labelsManager.list), [labelsManager]); + const labelCustom = useMemo(() => { + if (isShowLinkedSigId && linkedSigPrefix) { + return labelsManager.customLabel ? `${linkedSigPrefix}・${labelsManager.customLabel}` : linkedSigPrefix; + } + return labelsManager.customLabel; + }, [linkedSigPrefix, isShowLinkedSigId, labelsManager]); + + return { labelsInfo, labelCustom }; +} diff --git a/assets/js/hooks/Mapper/components/map/hooks/useNodeKillsCount.ts b/assets/js/hooks/Mapper/components/map/hooks/useNodeKillsCount.ts new file mode 100644 index 00000000..6e4c5a1d --- /dev/null +++ b/assets/js/hooks/Mapper/components/map/hooks/useNodeKillsCount.ts @@ -0,0 +1,42 @@ +import { useEffect, useState, useCallback } from 'react'; +import { useMapEventListener } from '@/hooks/Mapper/events'; +import { Commands } from '@/hooks/Mapper/types'; + +interface Kill { + solar_system_id: number | string; + kills: number; +} + +interface MapEvent { + name: Commands; + data?: any; + payload?: Kill[]; +} + +export function useNodeKillsCount( + systemId: number | string, + initialKillsCount: number | null +): number | null { + const [killsCount, setKillsCount] = useState(initialKillsCount); + + useEffect(() => { + setKillsCount(initialKillsCount); + }, [initialKillsCount]); + + const handleEvent = useCallback((event: MapEvent): boolean => { + if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) { + const killForSystem = event.payload.find( + kill => kill.solar_system_id.toString() === systemId.toString() + ); + if (killForSystem && typeof killForSystem.kills === 'number') { + setKillsCount(killForSystem.kills); + } + return true; + } + return false; + }, [systemId]); + + useMapEventListener(handleEvent); + + return killsCount; +} diff --git a/assets/js/hooks/Mapper/components/map/hooks/useSolarSystemLogic.ts b/assets/js/hooks/Mapper/components/map/hooks/useSolarSystemNode.ts similarity index 61% rename from assets/js/hooks/Mapper/components/map/hooks/useSolarSystemLogic.ts rename to assets/js/hooks/Mapper/components/map/hooks/useSolarSystemNode.ts index 6e48bf79..800579a7 100644 --- a/assets/js/hooks/Mapper/components/map/hooks/useSolarSystemLogic.ts +++ b/assets/js/hooks/Mapper/components/map/hooks/useSolarSystemNode.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { MapSolarSystemType } from '../map.types'; import { NodeProps } from 'reactflow'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; @@ -7,19 +7,12 @@ import { useMapState } from '@/hooks/Mapper/components/map/MapProvider'; import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick'; import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants'; import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace'; -import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers'; +import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers'; import { sortWHClasses } from '@/hooks/Mapper/helpers'; -import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager'; -import { CharacterTypeRaw, Commands, OutCommand, SystemSignature } from '@/hooks/Mapper/types'; -import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants'; -import { useMapEventListener } from '@/hooks/Mapper/events'; - -export type LabelInfo = { - id: string; - shortName: string; -}; - -export type UnsplashedSignatureType = SystemSignature & { sig_id: string }; +import { CharacterTypeRaw, OutCommand, SystemSignature } from '@/hooks/Mapper/types'; +import { useUnsplashedSignatures } from './useUnsplashedSignatures'; +import { useSystemName } from './useSystemName'; +import { LabelInfo, useLabelsInfo } from './useLabelsInfo'; function getActivityType(count: number): string { if (count <= 5) return 'activityNormal'; @@ -34,11 +27,6 @@ const SpaceToClass: Record = { [Spaces.Gallente]: 'Gallente', }; -function sortedLabels(labels: string[]): LabelInfo[] { - if (!labels) return []; - return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x] as LabelInfo); -} - export function useLocalCounter(nodeVars: SolarSystemNodeVars) { const localCounterCharacters = useMemo(() => { return nodeVars.charactersInSystem @@ -127,21 +115,19 @@ export function useSolarSystemNode(props: NodeProps): SolarS const linkedSigPrefix = useMemo(() => (linkedSigEveId ? linkedSigEveId.split('-')[0] : null), [linkedSigEveId]); - const labelsManager = useMemo(() => new LabelsManager(labels ?? ''), [labels]); - const labelsInfo = useMemo(() => sortedLabels(labelsManager.list), [labelsManager]); - const labelCustom = useMemo(() => { - if (isShowLinkedSigId && linkedSigPrefix) { - return labelsManager.customLabel ? `${linkedSigPrefix}・${labelsManager.customLabel}` : linkedSigPrefix; - } - return labelsManager.customLabel; - }, [linkedSigPrefix, isShowLinkedSigId, labelsManager]); + const { labelsInfo, labelCustom } = useLabelsInfo({ + labels, + linkedSigPrefix, + isShowLinkedSigId, + }); const killsCount = useMemo(() => kills[solar_system_id] ?? null, [kills, solar_system_id]); const killsActivityType = killsCount ? getActivityType(killsCount) : null; - const hasUserCharacters = useMemo(() => { - return charactersInSystem.some(x => userCharacters.includes(x.eve_id)); - }, [charactersInSystem, userCharacters]); + const hasUserCharacters = useMemo( + () => charactersInSystem.some(x => userCharacters.includes(x.eve_id)), + [charactersInSystem, userCharacters], + ); const dbClick = useDoubleClick(() => { outCommand({ @@ -153,54 +139,19 @@ export function useSolarSystemNode(props: NodeProps): SolarS const showHandlers = isConnecting || hoverNodeId === id; const space = showKSpaceBG ? REGIONS_MAP[region_id] : ''; - const regionClass = showKSpaceBG ? SpaceToClass[space] : null; + const regionClass = showKSpaceBG ? SpaceToClass[space] || null : null; - const computedTemporaryName = useMemo(() => { - if (!isTempSystemNameEnabled) { - return ''; - } - if (isShowLinkedSigIdTempName && linkedSigPrefix) { - return temporary_name ? `${linkedSigPrefix}・${temporary_name}` : `${linkedSigPrefix}・${solar_system_name}`; - } - return temporary_name; - }, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, solar_system_name, temporary_name]); + const { systemName, computedTemporaryName, customName } = useSystemName({ + isTempSystemNameEnabled, + temporary_name, + solar_system_name: solar_system_name || '', + isShowLinkedSigIdTempName, + linkedSigPrefix, + name, + }); - const systemName = useMemo(() => { - if (isTempSystemNameEnabled && computedTemporaryName) { - return computedTemporaryName; - } - return solar_system_name; - }, [isTempSystemNameEnabled, solar_system_name, computedTemporaryName]); + const { unsplashedLeft, unsplashedRight } = useUnsplashedSignatures(systemSigs, isShowUnsplashedSignatures); - const customName = useMemo(() => { - if (isTempSystemNameEnabled && computedTemporaryName && name) { - return name; - } - if (solar_system_name !== name && name) { - return name; - } - return null; - }, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]); - - const [unsplashedLeft, unsplashedRight] = useMemo(() => { - if (!isShowUnsplashedSignatures) { - return [[], []]; - } - return prepareUnsplashedChunks( - systemSigs - .filter(s => s.group === 'Wormhole' && !s.linked_system) - .map(s => ({ - eve_id: s.eve_id, - type: s.type, - custom_info: s.custom_info, - kind: s.kind, - name: s.name, - group: s.group, - })) as UnsplashedSignatureType[], - ); - }, [isShowUnsplashedSignatures, systemSigs]); - - // Ensure hubs are always strings. const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]); const nodeVars: SolarSystemNodeVars = { @@ -225,12 +176,10 @@ export function useSolarSystemNode(props: NodeProps): SolarS dbClick, sortedStatics, effectName: effect_name, - regionName: region_name, solarSystemId: solar_system_id.toString(), - solarSystemName: solar_system_name, locked, hubs: hubsAsStrings, - name: name, + name, isConnecting, hoverNodeId, charactersInSystem, @@ -239,6 +188,8 @@ export function useSolarSystemNode(props: NodeProps): SolarS isThickConnections, classTitle: class_title, temporaryName: computedTemporaryName, + regionName: region_name, + solarSystemName: solar_system_name, }; return nodeVars; @@ -281,25 +232,3 @@ export interface SolarSystemNodeVars { classTitle: string | null; temporaryName?: string | null; } - -export function useNodeKillsCount(systemId: number | string, initialKillsCount: number | null): number | null { - const [killsCount, setKillsCount] = useState(initialKillsCount); - - useEffect(() => { - setKillsCount(initialKillsCount); - }, [initialKillsCount]); - - useMapEventListener(event => { - if (event.name === Commands.killsUpdated && event.data?.toString() === systemId.toString()) { - //@ts-ignore - if (event.payload && typeof event.payload.kills === 'number') { - // @ts-ignore - setKillsCount(event.payload.kills); - } - return true; - } - return false; - }); - - return killsCount; -} diff --git a/assets/js/hooks/Mapper/components/map/hooks/useSystemName.ts b/assets/js/hooks/Mapper/components/map/hooks/useSystemName.ts new file mode 100644 index 00000000..7c219159 --- /dev/null +++ b/assets/js/hooks/Mapper/components/map/hooks/useSystemName.ts @@ -0,0 +1,49 @@ +// useSystemName.ts +import { useMemo } from 'react'; + +interface UseSystemNameParams { + isTempSystemNameEnabled: boolean; + temporary_name?: string | null; + solar_system_name: string; + isShowLinkedSigIdTempName: boolean; + linkedSigPrefix: string | null; + name?: string | null; +} + +export function useSystemName({ + isTempSystemNameEnabled, + temporary_name, + solar_system_name, + isShowLinkedSigIdTempName, + linkedSigPrefix, + name, +}: UseSystemNameParams) { + const computedTemporaryName = useMemo(() => { + if (!isTempSystemNameEnabled) { + return ''; + } + if (isShowLinkedSigIdTempName && linkedSigPrefix) { + return temporary_name ? `${linkedSigPrefix}・${temporary_name}` : `${linkedSigPrefix}・${solar_system_name}`; + } + return temporary_name ?? ''; + }, [isTempSystemNameEnabled, temporary_name, solar_system_name, isShowLinkedSigIdTempName, linkedSigPrefix]); + + const systemName = useMemo(() => { + if (isTempSystemNameEnabled && computedTemporaryName) { + return computedTemporaryName; + } + return solar_system_name; + }, [isTempSystemNameEnabled, computedTemporaryName, solar_system_name]); + + const customName = useMemo(() => { + if (isTempSystemNameEnabled && computedTemporaryName && name) { + return name; + } + if (solar_system_name !== name && name) { + return name; + } + return null; + }, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]); + + return { systemName, computedTemporaryName, customName }; +} diff --git a/assets/js/hooks/Mapper/components/map/hooks/useUnsplashedSignatures.ts b/assets/js/hooks/Mapper/components/map/hooks/useUnsplashedSignatures.ts new file mode 100644 index 00000000..2fdde948 --- /dev/null +++ b/assets/js/hooks/Mapper/components/map/hooks/useUnsplashedSignatures.ts @@ -0,0 +1,30 @@ +import { useMemo } from 'react'; +import { SystemSignature } from '@/hooks/Mapper/types'; +import { prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers'; + +export type UnsplashedSignatureType = SystemSignature & { sig_id: string }; + +export function useUnsplashedSignatures(systemSigs: SystemSignature[], isShowUnsplashedSignatures: boolean) { + return useMemo(() => { + if (!isShowUnsplashedSignatures) { + return { + unsplashedLeft: [] as SystemSignature[], + unsplashedRight: [] as SystemSignature[], + }; + } + const chunks = prepareUnsplashedChunks( + systemSigs + .filter(s => s.group === 'Wormhole' && !s.linked_system) + .map(s => ({ + eve_id: s.eve_id, + type: s.type, + custom_info: s.custom_info, + kind: s.kind, + name: s.name, + group: s.group, + })) as UnsplashedSignatureType[], + ); + const [unsplashedLeft, unsplashedRight] = chunks; + return { unsplashedLeft, unsplashedRight }; + }, [isShowUnsplashedSignatures, systemSigs]); +} diff --git a/assets/js/hooks/Mapper/components/map/hooks/useUpdateNodes.ts b/assets/js/hooks/Mapper/components/map/hooks/useUpdateNodes.ts index ab3273f5..a0e8a30b 100644 --- a/assets/js/hooks/Mapper/components/map/hooks/useUpdateNodes.ts +++ b/assets/js/hooks/Mapper/components/map/hooks/useUpdateNodes.ts @@ -6,9 +6,9 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types'; const useThrottle = () => { const throttleSeed = useRef(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const throttleFunction = useRef((func: any, delay = 200) => { if (!throttleSeed.current) { - // Call the callback immediately for the first time func(); throttleSeed.current = setTimeout(() => { throttleSeed.current = null; @@ -75,7 +75,7 @@ export const useUpdateNodes = (nodes: Node[]) => { const visibleNodes = new Set(nodes.filter(x => isNodeVisible(x, viewport)).map(x => x.id)); update({ visibleNodes }); - }, [nodes]); + }, [getViewport, nodes, update]); useOnViewportChange({ onChange: () => throttle(updateNodesVisibility.bind(this)), @@ -84,5 +84,5 @@ export const useUpdateNodes = (nodes: Node[]) => { useEffect(() => { updateNodesVisibility(); - }, [nodes]); + }, [nodes, updateNodesVisibility]); }; diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemKills/hooks/useSystemKills.ts b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemKills/hooks/useSystemKills.ts index 42c488e3..06c4c7ae 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemKills/hooks/useSystemKills.ts +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemKills/hooks/useSystemKills.ts @@ -1,9 +1,10 @@ import { useCallback, useMemo, useState, useEffect, useRef } from 'react'; import debounce from 'lodash.debounce'; -import { OutCommand } from '@/hooks/Mapper/types/mapHandlers'; +import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers'; import { DetailedKill } from '@/hooks/Mapper/types/kills'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useKillsWidgetSettings } from './useKillsWidgetSettings'; +import { useMapEventListener, MapEvent } from '@/hooks/Mapper/events'; interface UseSystemKillsProps { systemId?: string; @@ -13,16 +14,17 @@ interface UseSystemKillsProps { sinceHours?: number; } -function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceHours: number): DetailedKill[] { +function combineKills( + existing: DetailedKill[], + incoming: DetailedKill[], + sinceHours: number +): DetailedKill[] { const cutoff = Date.now() - sinceHours * 60 * 60 * 1000; const byId: Record = {}; for (const kill of [...existing, ...incoming]) { - if (!kill.kill_time) { - continue; - } + if (!kill.kill_time) continue; const killTimeMs = new Date(kill.kill_time).valueOf(); - if (killTimeMs >= cutoff) { byId[kill.killmail_id] = kill; } @@ -31,101 +33,117 @@ function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceH return Object.values(byId); } -export function useSystemKills({ systemId, outCommand, showAllVisible = false, sinceHours = 24 }: UseSystemKillsProps) { +interface DetailedKillsEvent extends MapEvent { + payload: Record; +} + +export function useSystemKills({ + systemId, + outCommand, + showAllVisible = false, + sinceHours = 24, +}: UseSystemKillsProps) { const { data, update } = useMapRootState(); const { detailedKills = {}, systems = [] } = data; - const [settings] = useKillsWidgetSettings(); const excludedSystems = settings.excludedSystems; - // When showing all visible kills, filter out excluded systems; - // when showAllVisible is false, ignore the exclusion filter. + const updateDetailedKills = useCallback((newKillsMap: Record) => { + update((prev) => { + const oldKills = prev.detailedKills ?? {}; + const updated = { ...oldKills }; + for (const [sid, killsArr] of Object.entries(newKillsMap)) { + updated[sid] = killsArr; + } + return { ...prev, detailedKills: updated }; + }, true); + }, [update]); + + useMapEventListener((event: MapEvent) => { + if (event.name === Commands.detailedKillsUpdated) { + const detailedEvent = event as DetailedKillsEvent; + if (systemId && !Object.keys(detailedEvent.payload).includes(systemId.toString())) { + return false; + } + updateDetailedKills(detailedEvent.payload); + return true; + } + return false; + }); + const effectiveSystemIds = useMemo(() => { if (showAllVisible) { - return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id))); + return systems.map((s) => s.id).filter((id) => !excludedSystems.includes(Number(id))); } - return systems.map(s => s.id); + return systems.map((s) => s.id); }, [systems, excludedSystems, showAllVisible]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const didFallbackFetch = useRef(Object.keys(detailedKills).length !== 0); - const mergeKillsIntoGlobal = useCallback( - (killsMap: Record) => { - update(prev => { - const oldMap = prev.detailedKills ?? {}; - const updated: Record = { ...oldMap }; + const mergeKillsIntoGlobal = useCallback((killsMap: Record) => { + update((prev) => { + const oldMap = prev.detailedKills ?? {}; + const updated: Record = { ...oldMap }; - for (const [sid, newKills] of Object.entries(killsMap)) { - const existing = updated[sid] ?? []; - const combined = combineKills(existing, newKills, sinceHours); - updated[sid] = combined; - } - - return { - ...prev, - detailedKills: updated, - }; - }); - }, - [update, sinceHours], - ); - - const fetchKills = useCallback( - async (forceFallback = false) => { - setIsLoading(true); - setError(null); - - try { - let eventType: OutCommand; - let requestData: Record; - - if (showAllVisible || forceFallback) { - eventType = OutCommand.getSystemsKills; - requestData = { - system_ids: effectiveSystemIds, - since_hours: sinceHours, - }; - } else if (systemId) { - eventType = OutCommand.getSystemKills; - requestData = { - system_id: systemId, - since_hours: sinceHours, - }; - } else { - // If there's no system and not showing all, do nothing - setIsLoading(false); - return; - } - - const resp = await outCommand({ - type: eventType, - data: requestData, - }); - - // Single system => `resp.kills` - if (resp?.kills) { - const arr = resp.kills as DetailedKill[]; - const sid = systemId ?? 'unknown'; - mergeKillsIntoGlobal({ [sid]: arr }); - } - // multiple systems => `resp.systems_kills` - else if (resp?.systems_kills) { - mergeKillsIntoGlobal(resp.systems_kills as Record); - } else { - console.warn('[useSystemKills] Unexpected kills response =>', resp); - } - } catch (err) { - console.error('[useSystemKills] Failed to fetch kills:', err); - setError(err instanceof Error ? err.message : 'Error fetching kills'); - } finally { - setIsLoading(false); + for (const [sid, newKills] of Object.entries(killsMap)) { + const existing = updated[sid] ?? []; + const combined = combineKills(existing, newKills, sinceHours); + updated[sid] = combined; } - }, - [showAllVisible, systemId, outCommand, effectiveSystemIds, sinceHours, mergeKillsIntoGlobal], - ); + + return { ...prev, detailedKills: updated }; + }); + }, [update, sinceHours]); + + const fetchKills = useCallback(async (forceFallback = false) => { + setIsLoading(true); + setError(null); + + try { + let eventType: OutCommand; + let requestData: Record; + + if (showAllVisible || forceFallback) { + eventType = OutCommand.getSystemsKills; + requestData = { + system_ids: effectiveSystemIds, + since_hours: sinceHours, + }; + } else if (systemId) { + eventType = OutCommand.getSystemKills; + requestData = { + system_id: systemId, + since_hours: sinceHours, + }; + } else { + setIsLoading(false); + return; + } + + const resp = await outCommand({ + type: eventType, + data: requestData, + }); + + if (resp?.kills) { + const arr = resp.kills as DetailedKill[]; + const sid = systemId ?? 'unknown'; + mergeKillsIntoGlobal({ [sid]: arr }); + } + else if (resp?.systems_kills) { + mergeKillsIntoGlobal(resp.systems_kills as Record); + } else { + console.warn('[useSystemKills] Unexpected kills response =>', resp); + } + } catch (err) { + console.error('[useSystemKills] Failed to fetch kills:', err); + setError(err instanceof Error ? err.message : 'Error fetching kills'); + } finally { + setIsLoading(false); + } + }, [showAllVisible, systemId, outCommand, effectiveSystemIds, sinceHours, mergeKillsIntoGlobal]); const debouncedFetchKills = useMemo( () => @@ -138,12 +156,11 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s const finalKills = useMemo(() => { if (showAllVisible) { - return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []); + return effectiveSystemIds.flatMap((sid) => detailedKills[sid] ?? []); } else if (systemId) { return detailedKills[systemId] ?? []; } else if (didFallbackFetch.current) { - // if we already did a fallback, we may have data for multiple systems - return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []); + return effectiveSystemIds.flatMap((sid) => detailedKills[sid] ?? []); } return []; }, [showAllVisible, systemId, effectiveSystemIds, detailedKills]); @@ -153,9 +170,8 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s useEffect(() => { if (!systemId && !showAllVisible && !didFallbackFetch.current) { didFallbackFetch.current = true; - // Cancel any queued debounced calls, then do the fallback. debouncedFetchKills.cancel(); - fetchKills(true); // forceFallback => fetch as though showAllVisible is true + fetchKills(true); } }, [systemId, showAllVisible, debouncedFetchKills, fetchKills]); @@ -164,14 +180,13 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s if (showAllVisible || systemId) { debouncedFetchKills(); - // Clean up the debounce on unmount or changes return () => debouncedFetchKills.cancel(); } }, [showAllVisible, systemId, effectiveSystemIds, debouncedFetchKills]); const refetch = useCallback(() => { debouncedFetchKills.cancel(); - fetchKills(); // immediate (non-debounced) call + fetchKills(); }, [debouncedFetchKills, fetchKills]); return {