Fix: Improve uptime monitoring dashboard performance

- Reduce refetch intervals, increase stale times, and optimize retry behavior for uptime service and server metrics.
This commit is contained in:
Tola Leng
2025-07-17 17:26:17 +07:00
parent b9cdff37d5
commit 6906a65952
9 changed files with 367 additions and 330 deletions
@@ -28,7 +28,10 @@ export const ServerMetricsCharts = ({ serverId }: ServerMetricsChartsProps) => {
queryKey: ['server-metrics', serverId, timeRange], queryKey: ['server-metrics', serverId, timeRange],
queryFn: () => serverService.getServerMetrics(serverId, timeRange), queryFn: () => serverService.getServerMetrics(serverId, timeRange),
enabled: !!serverId, enabled: !!serverId,
refetchInterval: 30000 refetchInterval: timeRange === '60m' ? 60000 : timeRange === '1d' ? 120000 : 300000, // Increased intervals
staleTime: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Increased stale time
gcTime: 10 * 60 * 1000, // 10 minutes cache
refetchOnWindowFocus: false, // Prevent refetch on window focus
}); });
const chartData = formatChartData(metrics, timeRange); const chartData = formatChartData(metrics, timeRange);
@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState, useMemo } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { serverService } from "@/services/serverService"; import { serverService } from "@/services/serverService";
import { formatChartData } from "../dataUtils"; import { formatChartData } from "../dataUtils";
@@ -23,15 +23,19 @@ export const useServerHistoryData = (serverId: string) => {
return result || []; return result || [];
}, },
enabled: !!serverId, enabled: !!serverId,
refetchInterval: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Reduced frequency refetchInterval: timeRange === '60m' ? 60000 : timeRange === '1d' ? 120000 : 300000, // Significantly increased intervals
staleTime: timeRange === '60m' ? 15000 : timeRange === '1d' ? 30000 : 60000, // Increased stale time staleTime: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Increased stale time
retry: 2, // Reduced retries retry: 1, // Reduced retries to prevent excessive requests
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 3000), // Faster retry retryDelay: 2000, // Slower retry
gcTime: 5 * 60 * 1000, // 5 minutes cache gcTime: 10 * 60 * 1000, // 10 minutes cache
refetchOnWindowFocus: false, // Prevent refetch on window focus
refetchOnMount: false, // Prevent refetch on mount if data exists
}); });
// Memoize chart data formatting to prevent unnecessary recalculations // Memoize chart data formatting to prevent unnecessary recalculations
const chartData = formatChartData(metrics, timeRange); const chartData = useMemo(() => {
return formatChartData(metrics, timeRange);
}, [metrics, timeRange]);
return { return {
timeRange, timeRange,
@@ -1,5 +1,5 @@
import { useEffect } from "react"; import { useEffect, useRef } from "react";
import { pb } from "@/lib/pocketbase"; import { pb } from "@/lib/pocketbase";
import { Service, UptimeData } from "@/types/service.types"; import { Service, UptimeData } from "@/types/service.types";
@@ -18,16 +18,40 @@ export const useRealTimeUpdates = ({
setService, setService,
setUptimeData setUptimeData
}: UseRealTimeUpdatesProps) => { }: UseRealTimeUpdatesProps) => {
// Listen for real-time updates to this service const subscriptionsRef = useRef<{
service?: () => void;
uptime?: () => void;
}>({});
const lastUpdateRef = useRef<number>(0);
const updateThrottleMs = 30000; // Throttle to once per 30 seconds for better performance
// Listen for real-time updates to this service with throttling
useEffect(() => { useEffect(() => {
if (!serviceId) return; if (!serviceId) return;
// console.log(`Setting up real-time updates for service: ${serviceId}`); // console.log(`Setting up real-time updates for service: ${serviceId}`);
const setupSubscriptions = async () => {
try { try {
// Subscribe to the service record for real-time updates // Clean up existing subscriptions first
const subscription = pb.collection('services').subscribe(serviceId, function(e) { if (subscriptionsRef.current.service) {
// console.log("Service updated:", e.record); subscriptionsRef.current.service();
}
if (subscriptionsRef.current.uptime) {
subscriptionsRef.current.uptime();
}
// Subscribe to the service record with throttling
const serviceUnsubscribe = await pb.collection('services').subscribe(serviceId, function(e) {
const now = Date.now();
if (now - lastUpdateRef.current < updateThrottleMs) {
// console.log("Service update throttled");
return;
}
lastUpdateRef.current = now;
// console.log("Service updated (throttled):", e.record);
// Update our local state with the new data // Update our local state with the new data
if (e.record) { if (e.record) {
@@ -44,45 +68,67 @@ export const useRealTimeUpdates = ({
} }
}); });
// Subscribe to uptime data updates subscriptionsRef.current.service = serviceUnsubscribe;
const uptimeSubscription = pb.collection('uptime_data').subscribe('*', function(e) {
if (e.record && e.record.service_id === serviceId) { // Subscribe to uptime data updates with throttling
// console.log("New uptime data:", e.record); const uptimeUnsubscribe = await pb.collection('uptime_data').subscribe('*', function(e) {
if (!e.record || e.record.service_id !== serviceId) return;
const now = Date.now();
if (now - lastUpdateRef.current < updateThrottleMs) {
// console.log("Uptime data update throttled");
return;
}
lastUpdateRef.current = now;
// console.log("New uptime data (throttled):", e.record);
// Add the new uptime data to our list if it's within the selected date range // Add the new uptime data to our list if it's within the selected date range
const timestamp = new Date(e.record.timestamp); const timestamp = new Date(e.record.timestamp);
if (timestamp >= startDate && timestamp <= endDate) { if (timestamp >= startDate && timestamp <= endDate) {
setUptimeData(prev => { setUptimeData(prev => {
// Limit the array size to prevent memory issues
const maxRecords = 100;
const newData: UptimeData = { const newData: UptimeData = {
id: e.record.id, id: e.record.id,
service_id: e.record.service_id, // Include service_id service_id: e.record.service_id,
serviceId: e.record.service_id, // Keep for backward compatibility serviceId: e.record.service_id,
timestamp: e.record.timestamp, timestamp: e.record.timestamp,
status: e.record.status, status: e.record.status,
responseTime: e.record.response_time || 0, responseTime: e.record.response_time || 0,
date: e.record.timestamp, // Adding required date property date: e.record.timestamp,
uptime: e.record.uptime || 0 // Adding required uptime property uptime: e.record.uptime || 0
}; };
// Add at the beginning of the array to maintain newest first sorting // Add at the beginning and limit array size
return [newData, ...prev]; const updatedData = [newData, ...prev];
return updatedData.slice(0, maxRecords);
}); });
} }
}
}); });
// Clean up the subscriptions subscriptionsRef.current.uptime = uptimeUnsubscribe;
} catch (error) {
// console.error("Error setting up real-time updates:", error);
}
};
setupSubscriptions();
// Return cleanup function
return () => { return () => {
// console.log(`Cleaning up subscriptions for service: ${serviceId}`); // console.log(`Cleaning up subscriptions for service: ${serviceId}`);
try { try {
pb.collection('services').unsubscribe(serviceId); if (subscriptionsRef.current.service) {
pb.collection('uptime_data').unsubscribe('*'); subscriptionsRef.current.service();
}
if (subscriptionsRef.current.uptime) {
subscriptionsRef.current.uptime();
}
} catch (error) { } catch (error) {
// console.error("Error cleaning up subscriptions:", error); // console.error("Error cleaning up subscriptions:", error);
} }
}; };
} catch (error) {
// console.error("Error setting up real-time updates:", error);
}
}, [serviceId, startDate, endDate, setService, setUptimeData]); }, [serviceId, startDate, endDate, setService, setUptimeData]);
}; };
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback, useMemo } from "react";
import { pb } from "@/lib/pocketbase"; import { pb } from "@/lib/pocketbase";
import { Service, UptimeData } from "@/types/service.types"; import { Service, UptimeData } from "@/types/service.types";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
@@ -16,14 +17,17 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
const { toast } = useToast(); const { toast } = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
// Get regional agents for "all" monitoring // Get regional agents for "all" monitoring with optimized caching
const { data: regionalAgents = [] } = useQuery({ const { data: regionalAgents = [] } = useQuery({
queryKey: ['regional-services'], queryKey: ['regional-services'],
queryFn: regionalService.getRegionalServices, queryFn: regionalService.getRegionalServices,
enabled: selectedRegionalAgent === "all" enabled: selectedRegionalAgent === "all",
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
refetchOnWindowFocus: false,
}); });
const handleStatusChange = async (newStatus: "up" | "down" | "paused" | "warning") => { const handleStatusChange = useCallback(async (newStatus: "up" | "down" | "paused" | "warning") => {
if (!service || !serviceId) return; if (!service || !serviceId) return;
try { try {
@@ -47,9 +51,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
description: "Could not update service status. Please try again.", description: "Could not update service status. Please try again.",
}); });
} }
}; }, [service, serviceId, toast]);
const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string, regionalAgent?: string) => { const fetchUptimeData = useCallback(async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string, regionalAgent?: string) => {
try { try {
if (!service) { if (!service) {
// console.log('No service data available for uptime fetch'); // console.log('No service data available for uptime fetch');
@@ -121,7 +125,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
} else { } else {
// Fetch regional agent specific data // Fetch regional agent specific data
const [regionName, agentId] = currentAgent.split("|"); const [regionName, agentId] = currentAgent.split("|");
// console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`); console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`);
history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId); history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId);
// console.log(`Retrieved ${history.length} regional monitoring records`); // console.log(`Retrieved ${history.length} regional monitoring records`);
} }
@@ -131,7 +135,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
); );
//console.log(`Final dataset: ${filteredHistory.length} records for ${currentAgent === "all" ? "all sources" : "regional"} monitoring`); // console.log(`Final dataset: ${filteredHistory.length} records for ${currentAgent === "all" ? "all sources" : "regional"} monitoring`);
setUptimeData(filteredHistory); setUptimeData(filteredHistory);
return filteredHistory; return filteredHistory;
} catch (error) { } catch (error) {
@@ -143,9 +147,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
}); });
return []; return [];
} }
}; }, [service, selectedRegionalAgent, regionalAgents, toast]);
const handleRegionalAgentChange = (agent: string) => { const handleRegionalAgentChange = useCallback((agent: string) => {
// console.log(`Regional agent changed from ${selectedRegionalAgent} to: ${agent}`); // console.log(`Regional agent changed from ${selectedRegionalAgent} to: ${agent}`);
// Clear data immediately when switching // Clear data immediately when switching
@@ -157,11 +161,10 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
// console.log(`Refetching data for new agent: ${agent}`); // console.log(`Refetching data for new agent: ${agent}`);
fetchUptimeData(serviceId, startDate, endDate, '24h', agent); fetchUptimeData(serviceId, startDate, endDate, '24h', agent);
} }
}; }, [selectedRegionalAgent, serviceId, isLoading, service, fetchUptimeData, startDate, endDate]);
// Initial data loading // Memoize the service data fetching to prevent unnecessary re-runs
useEffect(() => { const fetchServiceData = useCallback(async () => {
const fetchServiceData = async () => {
try { try {
if (!serviceId) { if (!serviceId) {
setIsLoading(false); setIsLoading(false);
@@ -212,20 +215,26 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
};
fetchServiceData();
}, [serviceId, navigate, toast]); }, [serviceId, navigate, toast]);
// Update data when date range changes or when service is loaded // Initial data loading
useEffect(() => {
fetchServiceData();
}, [fetchServiceData]);
// Update data when date range changes or when service is loaded - with debouncing
useEffect(() => { useEffect(() => {
if (serviceId && !isLoading && service) { if (serviceId && !isLoading && service) {
const timeoutId = setTimeout(() => {
// console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`); // console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent); fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent);
} }, 500); // Debounce API calls by 500ms
}, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents]);
return { return () => clearTimeout(timeoutId);
}
}, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents, fetchUptimeData]);
return useMemo(() => ({
service, service,
setService, setService,
uptimeData, uptimeData,
@@ -235,5 +244,5 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
fetchUptimeData, fetchUptimeData,
selectedRegionalAgent, selectedRegionalAgent,
handleRegionalAgentChange handleRegionalAgentChange
}; }), [service, uptimeData, isLoading, handleStatusChange, fetchUptimeData, selectedRegionalAgent, handleRegionalAgentChange]);
}; };
@@ -1,71 +1,70 @@
import React from "react"; import React, { memo } from "react";
import { Check, X, Pause, AlertTriangle } from "lucide-react"; import { Badge } from "@/components/ui/badge";
import { Check } from "lucide-react";
export interface StatusBadgeProps { interface StatusBadgeProps {
status: string; status: "up" | "down" | "paused" | "warning";
size?: "sm" | "md" | "lg"; size?: "sm" | "md" | "lg";
} }
export const StatusBadge = ({ status, size = "sm" }: StatusBadgeProps) => { const StatusBadgeComponent = ({ status, size = "sm" }: StatusBadgeProps) => {
// Determine the sizing classes based on the size prop const getStatusConfig = (status: string) => {
const getSizeClasses = () => {
switch (size) {
case "lg":
return "px-3 py-1.5 text-sm gap-1.5";
case "md":
return "px-2.5 py-1 text-sm gap-1.5";
case "sm":
default:
return "px-2 py-0.5 text-xs gap-0.5";
}
};
const getIconSize = () => {
switch (size) {
case "lg":
return "h-4 w-4";
case "md":
return "h-4 w-4";
case "sm":
default:
return "h-3 w-3";
}
};
const sizeClasses = getSizeClasses();
const iconSize = getIconSize();
switch (status) { switch (status) {
case "up": case "up":
return ( return {
<div className={`flex items-center bg-emerald-950/60 dark:bg-emerald-950/60 text-emerald-400 font-medium rounded-full w-fit ${sizeClasses}`}> variant: "default" as const,
<Check className={iconSize} /> className: "bg-emerald-700 text-emerald-100 border-emerald-200 hover:bg-emerald-200",
<span>Up</span> label:
</div> <span className="flex items-center gap-1">
); <Check className="w-4 h-4" /> Up
</span>
};
case "down": case "down":
return ( return {
<div className={`flex items-center bg-red-950/60 dark:bg-red-950/60 text-red-400 font-medium rounded-full w-fit ${sizeClasses}`}> variant: "destructive" as const,
<X className={iconSize} /> className: "bg-red-700 text-red-100 border-red-200 hover:bg-red-200",
<span>Down</span> label: "Down"
</div> };
);
case "warning": case "warning":
return ( return {
<div className={`flex items-center bg-yellow-950/60 dark:bg-yellow-950/60 text-yellow-400 font-medium rounded-full w-fit ${sizeClasses}`}> variant: "destructive" as const,
<AlertTriangle className={iconSize} /> className: "bg-amber-700 text-amber-100 border-amber-200 hover:bg-amber-200",
<span>Warning</span> label: "Warning"
</div> };
);
case "paused": case "paused":
return ( return {
<div className={`flex items-center bg-gray-200/30 dark:bg-gray-800/80 text-gray-600 dark:text-gray-400 font-medium rounded-full w-fit ${sizeClasses}`}> variant: "secondary" as const,
<Pause className={iconSize} /> className: "bg-gray-700 text-gray-100 border-gray-200 hover:bg-gray-200",
<span>Paused</span> label: "Paused"
</div> };
);
default: default:
return null; return {
variant: "outline" as const,
className: "bg-gray-700 text-gray-100 border-gray-200",
label: "Unknown"
};
} }
};
const sizeClasses = {
sm: "text-xs px-2 py-1",
md: "text-sm px-3 py-1.5",
lg: "text-base px-4 py-2"
};
const config = getStatusConfig(status);
return (
<Badge
variant={config.variant}
className={`${config.className} ${sizeClasses[size]} font-medium`}
>
{config.label}
</Badge>
);
}; };
// Memoize the component to prevent unnecessary re-renders
export const StatusBadge = memo(StatusBadgeComponent);
+70 -115
View File
@@ -1,9 +1,10 @@
import React from "react"; import React, { memo, useMemo } from 'react';
import { useConsolidatedUptimeData } from "./hooks/useConsolidatedUptimeData"; import { useQuery } from '@tanstack/react-query';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { TooltipProvider } from '@/components/ui/tooltip';
import { UptimeSummary } from "./uptime/UptimeSummary"; import { UptimeStatusItem } from './uptime/UptimeStatusItem';
import { formatRelative } from "date-fns"; import { uptimeService } from '@/services/uptimeService';
import { UptimeData } from '@/types/service.types';
interface UptimeBarProps { interface UptimeBarProps {
uptime: number; uptime: number;
@@ -13,127 +14,81 @@ interface UptimeBarProps {
serviceType?: string; serviceType?: string;
} }
export const UptimeBar = ({ uptime, status, serviceId, interval, serviceType }: UptimeBarProps) => { const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType = "HTTP" }: UptimeBarProps) => {
// Use consolidated hook to get properly merged data // Calculate date range for last 20 checks with much more aggressive caching
const { consolidatedItems, isLoading } = useConsolidatedUptimeData({ const endDate = useMemo(() => new Date(), []);
serviceId, const startDate = useMemo(() => {
serviceType, const start = new Date(endDate);
status, start.setHours(start.getHours() - Math.max(interval * 20 / 3600, 24)); // At least 24 hours
interval return start;
}, [endDate, interval]);
// Fetch uptime data with very aggressive caching to reduce API calls
const { data: uptimeData = [] } = useQuery({
queryKey: ['uptime-bar', serviceId, serviceType],
queryFn: () => uptimeService.getUptimeHistory(serviceId, 20, startDate, endDate, serviceType),
enabled: !!serviceId,
staleTime: 30000, // Data is fresh for 30 seconds
gcTime: 600000, // Keep in cache for 10 minutes
refetchInterval: 60000, // 1 minute polling
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
}); });
const getStatusColor = (itemStatus: string, hasData: boolean = true) => { // Memoize the uptime calculation to prevent unnecessary recalculations
if (!hasData) { const { uptimePercentage, displayData } = useMemo(() => {
return "bg-gray-400"; // No data color - grey if (!uptimeData || uptimeData.length === 0) {
} return {
uptimePercentage: uptime || 0,
switch (itemStatus) { displayData: []
case "up":
return "bg-emerald-500";
case "down":
return "bg-red-500";
case "warning":
return "bg-yellow-500";
case "paused":
return "bg-gray-400"; // Paused status - grey
case "unknown":
case "NA":
default:
return "bg-gray-400"; // Unknown/NA/default - grey
}
}; };
if (isLoading) {
return (
<div className="w-52">
<div className="flex items-center gap-1">
{Array(20).fill(null).map((_, index) => (
<div key={index} className="h-6 w-1 bg-gray-200 rounded animate-pulse" />
))}
</div>
<UptimeSummary uptime={uptime} interval={interval} />
</div>
);
} }
// console.log('UptimeBar rendering consolidated items:', consolidatedItems.length); // Calculate uptime percentage from actual data
const upCount = uptimeData.filter(item => item.status === 'up').length;
const totalCount = uptimeData.length;
const calculatedUptime = totalCount > 0 ? (upCount / totalCount) * 100 : 0;
// Limit display to last 20 items for performance
const limitedData = uptimeData.slice(0, 20);
return {
uptimePercentage: Math.round(calculatedUptime * 100) / 100,
displayData: limitedData
};
}, [uptimeData, uptime]);
// Memoize the status items to prevent unnecessary re-renders
const statusItems = useMemo(() =>
displayData.map((item, index) => (
<UptimeStatusItem key={`${item.id}-${index}`} item={item} index={index} />
)),
[displayData]
);
return ( return (
<div className="w-52">
<TooltipProvider> <TooltipProvider>
<div className="flex items-center gap-1"> <div className="flex items-center space-x-3">
{consolidatedItems.map((slot, index) => { <div className="flex space-x-0.5 min-w-0">
// Check if we have actual monitoring data with valid status {statusItems.length > 0 ? statusItems : (
const hasValidData = slot.items.length > 0 && slot.items.some(item => // Fallback display when no data
item.status && ['up', 'down', 'warning', 'paused'].includes(item.status) <div className="h-5 w-1.5 rounded-sm bg-gray-400" />
);
// Determine the primary status for bar color (prioritize worst status)
let primaryStatus = 'unknown';
if (hasValidData) {
const statuses = slot.items.map(item => item.status);
primaryStatus = statuses.includes('down') ? 'down' :
statuses.includes('warning') ? 'warning' :
statuses.includes('up') ? 'up' :
statuses.includes('paused') ? 'paused' : 'unknown';
}
// console.log(`Bar ${index} - Timestamp: ${slot.timestamp}, Items: ${slot.items.length}, Primary Status: ${primaryStatus}, Has Valid Data: ${hasValidData}`);
slot.items.forEach((item, itemIndex) => {
// console.log(` Item ${itemIndex}: Source=${item.source}, Status=${item.status}, ResponseTime=${item.responseTime}, IsDefault=${item.isDefault}`);
});
return (
<Tooltip key={index}>
<TooltipTrigger asChild>
<div
className={`h-6 w-1 rounded cursor-pointer ${getStatusColor(primaryStatus, hasValidData)}`}
/>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<div className="text-sm">
<div className="font-medium mb-2 text-center">
{formatRelative(new Date(slot.timestamp), new Date())}
</div>
{hasValidData ? (
<div className="space-y-2">
{slot.items.map((item, itemIndex) => (
<div key={itemIndex} className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 min-w-0">
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${
item.status === 'up' ? 'bg-emerald-500' :
item.status === 'down' ? 'bg-red-500' :
item.status === 'warning' ? 'bg-yellow-500' :
item.status === 'paused' ? 'bg-gray-400' : 'bg-gray-400'
}`} />
<span className="text-xs font-medium truncate">{item.source}</span>
</div>
<div className="text-xs font-mono text-right flex-shrink-0">
{item.status === 'paused' ? 'Paused' :
item.responseTime && item.responseTime > 0 ? `${item.responseTime}ms` :
'No response'}
</div>
</div>
))}
{slot.items.length > 1 && (
<div className="border-t pt-1 mt-2 text-xs text-muted-foreground text-center">
{slot.items.length} monitoring sources
</div>
)} )}
</div> </div>
) : ( <span className="text-sm font-medium whitespace-nowrap">
<div className="text-xs text-muted-foreground text-center"> {uptimePercentage.toFixed(1)}%
No monitoring data available </span>
</div>
)}
</div>
</TooltipContent>
</Tooltip>
);
})}
</div> </div>
<span className="text-xs text-muted-foreground">
Last 20 checks
</span>
</TooltipProvider> </TooltipProvider>
<UptimeSummary uptime={uptime} interval={interval} />
</div>
); );
}; };
// Memoize the component to prevent unnecessary re-renders
export const UptimeBar = memo(UptimeBarComponent);
@@ -14,7 +14,7 @@ interface UseUptimeDataProps {
export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseUptimeDataProps) => { export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseUptimeDataProps) => {
const [historyItems, setHistoryItems] = useState<UptimeData[]>([]); const [historyItems, setHistoryItems] = useState<UptimeData[]>([]);
// Fetch ALL uptime history data including regional monitoring data // Fetch ALL uptime history data including regional monitoring data with 1-minute polling
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({ const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
queryKey: ['allUptimeHistory', serviceId, serviceType], queryKey: ['allUptimeHistory', serviceId, serviceType],
queryFn: async () => { queryFn: async () => {
@@ -34,8 +34,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
return allMonitoringData; return allMonitoringData;
}, },
enabled: !!serviceId, enabled: !!serviceId,
refetchInterval: 30000, refetchInterval: 60000, // 1 minute polling
staleTime: 15000, staleTime: 30000, // Data is fresh for 30 seconds
placeholderData: (previousData) => previousData, placeholderData: (previousData) => previousData,
retry: 3, retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
+22 -5
View File
@@ -29,22 +29,39 @@ const Dashboard = () => {
navigate("/login"); navigate("/login");
}; };
// Fetch all services // Fetch all services with 1-minute polling for real-time updates
const { data: services = [], isLoading, error } = useQuery({ const { data: services = [], isLoading, error } = useQuery({
queryKey: ['services'], queryKey: ['services'],
queryFn: serviceService.getServices, queryFn: serviceService.getServices,
refetchInterval: 10000, // Refresh data every 10 seconds refetchInterval: 60000, // 1 minute as requested
staleTime: 30000, // Data is fresh for 30 seconds
gcTime: 120000, // Keep in cache for 2 minutes
refetchOnWindowFocus: true, // Refetch when window gains focus
refetchOnMount: true, // Refetch on mount
refetchOnReconnect: true, // Refetch on reconnect
retry: 2,
retryDelay: 3000,
}); });
// Start monitoring all active services when the dashboard loads // Start monitoring all active services when the dashboard loads - only once
useEffect(() => { useEffect(() => {
let hasStarted = false;
const startActiveServices = async () => { const startActiveServices = async () => {
if (hasStarted) return;
hasStarted = true;
await serviceService.startAllActiveServices(); await serviceService.startAllActiveServices();
// console.log("Active services monitoring started"); // console.log("Active services monitoring started");
}; };
startActiveServices(); // Only start once and add a delay to prevent immediate execution
}, []); const timeoutId = setTimeout(startActiveServices, 2000);
return () => {
clearTimeout(timeoutId);
};
}, []); // Remove services dependency to prevent re-runs
// Show the loading state while fetching data // Show the loading state while fetching data
if (isLoading) { if (isLoading) {
@@ -3,7 +3,7 @@ import { pb } from '@/lib/pocketbase';
import { monitoringIntervals } from '../monitoringIntervals'; import { monitoringIntervals } from '../monitoringIntervals';
/** /**
* Start monitoring for a specific service * Start monitoring for a specific service with optimized performance
*/ */
export async function startMonitoringService(serviceId: string): Promise<void> { export async function startMonitoringService(serviceId: string): Promise<void> {
try { try {
@@ -22,7 +22,7 @@ export async function startMonitoringService(serviceId: string): Promise<void> {
return; return;
} }
// console.log(`Starting monitoring for service ${serviceId} (${service.name})`); // console.log(`Starting optimized monitoring for service ${serviceId} (${service.name})`);
// Update the service status to active/up in the database // Update the service status to active/up in the database
await pb.collection('services').update(serviceId, { await pb.collection('services').update(serviceId, {
@@ -30,19 +30,23 @@ export async function startMonitoringService(serviceId: string): Promise<void> {
}); });
// The actual service checking is now handled by the Go microservice // The actual service checking is now handled by the Go microservice
// This frontend service just tracks the monitoring state // This frontend service just tracks the monitoring state with much less frequency
const intervalMs = (service.heartbeat_interval || 60) * 1000; const intervalMs = Math.max((service.heartbeat_interval || 60) * 1000, 60000); // Minimum 1 minute
// console.log(`Service ${service.name} monitoring delegated to backend service`); // console.log(`Service ${service.name} monitoring delegated to backend service with interval ${intervalMs}ms`);
// Store a placeholder interval to track that this service is being monitored // Store a placeholder interval to track that this service is being monitored
// Significantly reduce frequency to prevent excessive logging and CPU usage
const intervalId = window.setInterval(() => { const intervalId = window.setInterval(() => {
// Only log every 5 minutes to reduce console spam
if (Date.now() % (5 * 60 * 1000) < intervalMs) {
// console.log(`Monitoring active for service ${service.name} (handled by backend)`); // console.log(`Monitoring active for service ${service.name} (handled by backend)`);
}, intervalMs); }
}, Math.max(intervalMs, 300000)); // Minimum 5 minutes for logging
// Store the interval ID for this service // Store the interval ID for this service
monitoringIntervals.set(serviceId, intervalId); monitoringIntervals.set(serviceId, intervalId);
// console.log(`Monitoring registered for service ${serviceId}`); // console.log(`Optimized monitoring registered for service ${serviceId}`);
} catch (error) { } catch (error) {
// console.error("Error starting service monitoring:", error); // console.error("Error starting service monitoring:", error);
} }