import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { SERVER_STATS_CONFIG, type ServerResourceDataPoint } from '@tracearr/shared'; import { toast } from 'sonner'; import { api } from '@/lib/api'; import { useRef, useCallback } from 'react'; export function useServers() { return useQuery({ queryKey: ['servers', 'list'], queryFn: api.servers.list, staleTime: 1000 * 60 * 5, // 5 minutes }); } export function useCreateServer() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (data: { name: string; type: string; url: string; token: string }) => api.servers.create(data), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ['servers', 'list'] }); toast.success('Server Added', { description: 'The server has been added successfully.' }); }, onError: (error: Error) => { toast.error('Failed to Add Server', { description: error.message }); }, }); } export function useDeleteServer() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (id: string) => api.servers.delete(id), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ['servers', 'list'] }); toast.success('Server Removed', { description: 'The server has been removed successfully.' }); }, onError: (error: Error) => { toast.error('Failed to Remove Server', { description: error.message }); }, }); } export function useSyncServer() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (id: string) => api.servers.sync(id), onSuccess: (data) => { void queryClient.invalidateQueries({ queryKey: ['servers', 'list'] }); void queryClient.invalidateQueries({ queryKey: ['users', 'list'] }); // Show detailed results const parts: string[] = []; if (data.usersAdded > 0) parts.push(`${data.usersAdded} users added`); if (data.usersUpdated > 0) parts.push(`${data.usersUpdated} users updated`); if (data.librariesSynced > 0) parts.push(`${data.librariesSynced} libraries`); if (data.errors.length > 0) parts.push(`${data.errors.length} errors`); const description = parts.length > 0 ? parts.join(', ') : 'No changes detected'; if (data.errors.length > 0) { toast.warning(data.success ? 'Sync Completed with Errors' : 'Sync Completed with Errors', { description }); // Log errors to console for debugging console.error('Sync errors:', data.errors); } else { toast.success('Server Synced', { description }); } }, onError: (error: Error) => { toast.error('Sync Failed', { description: error.message }); }, }); } /** * Hook for fetching server resource statistics with fixed 2-minute window * Polls every 10 seconds, displays last 2 minutes of data (12 points) * X-axis is static (2m → NOW), data slides through as new points arrive * * @param serverId - Server ID to fetch stats for * @param enabled - Whether polling is enabled (typically tied to component mount) */ export function useServerStatistics(serverId: string | undefined, enabled: boolean = true) { // Accumulate data points across polls, keyed by timestamp for deduplication const dataMapRef = useRef>(new Map()); // Merge new data with existing, keep most recent DATA_POINTS const mergeData = useCallback((newData: ServerResourceDataPoint[]) => { const map = dataMapRef.current; // Add/update data points for (const point of newData) { map.set(point.at, point); } // Sort by timestamp descending (newest first), keep DATA_POINTS const sorted = Array.from(map.values()) .sort((a, b) => b.at - a.at) .slice(0, SERVER_STATS_CONFIG.DATA_POINTS); // Rebuild map with only kept points dataMapRef.current = new Map(sorted.map((p) => [p.at, p])); // Return in ascending order (oldest first) for chart rendering return sorted.reverse(); }, []); const query = useQuery({ queryKey: ['servers', 'statistics', serverId], queryFn: async () => { if (!serverId) throw new Error('Server ID required'); const response = await api.servers.statistics(serverId); // Merge with accumulated data const mergedData = mergeData(response.data); return { ...response, data: mergedData, }; }, enabled: enabled && !!serverId, // Poll every 10 seconds refetchInterval: SERVER_STATS_CONFIG.POLL_INTERVAL_SECONDS * 1000, // Don't poll when tab is hidden refetchIntervalInBackground: false, // Don't refetch on window focus (we have interval polling) refetchOnWindowFocus: false, // Keep previous data while fetching new placeholderData: (prev) => prev, // Data is fresh until next poll staleTime: (SERVER_STATS_CONFIG.POLL_INTERVAL_SECONDS * 1000) - 500, }); // Calculate averages from windowed data const dataPoints = query.data?.data as ServerResourceDataPoint[] | undefined; const dataLength = dataPoints?.length ?? 0; const averages = dataPoints && dataLength > 0 ? { hostCpu: Math.round( dataPoints.reduce((sum: number, p) => sum + p.hostCpuUtilization, 0) / dataLength ), processCpu: Math.round( dataPoints.reduce((sum: number, p) => sum + p.processCpuUtilization, 0) / dataLength ), hostMemory: Math.round( dataPoints.reduce((sum: number, p) => sum + p.hostMemoryUtilization, 0) / dataLength ), processMemory: Math.round( dataPoints.reduce((sum: number, p) => sum + p.processMemoryUtilization, 0) / dataLength ), } : null; return { ...query, averages, }; }