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],
queryFn: () => serverService.getServerMetrics(serverId, timeRange),
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);
@@ -1,5 +1,5 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { serverService } from "@/services/serverService";
import { formatChartData } from "../dataUtils";
@@ -23,15 +23,19 @@ export const useServerHistoryData = (serverId: string) => {
return result || [];
},
enabled: !!serverId,
refetchInterval: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Reduced frequency
staleTime: timeRange === '60m' ? 15000 : timeRange === '1d' ? 30000 : 60000, // Increased stale time
retry: 2, // Reduced retries
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 3000), // Faster retry
gcTime: 5 * 60 * 1000, // 5 minutes cache
refetchInterval: timeRange === '60m' ? 60000 : timeRange === '1d' ? 120000 : 300000, // Significantly increased intervals
staleTime: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Increased stale time
retry: 1, // Reduced retries to prevent excessive requests
retryDelay: 2000, // Slower retry
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
const chartData = formatChartData(metrics, timeRange);
const chartData = useMemo(() => {
return formatChartData(metrics, timeRange);
}, [metrics, timeRange]);
return {
timeRange,
@@ -1,5 +1,5 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { pb } from "@/lib/pocketbase";
import { Service, UptimeData } from "@/types/service.types";
@@ -18,71 +18,117 @@ export const useRealTimeUpdates = ({
setService,
setUptimeData
}: 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(() => {
if (!serviceId) return;
// console.log(`Setting up real-time updates for service: ${serviceId}`);
// console.log(`Setting up real-time updates for service: ${serviceId}`);
try {
// Subscribe to the service record for real-time updates
const subscription = pb.collection('services').subscribe(serviceId, function(e) {
// console.log("Service updated:", e.record);
// Update our local state with the new data
if (e.record) {
setService(prev => {
if (!prev) return null;
return {
...prev,
status: e.record.status || prev.status,
responseTime: e.record.response_time || e.record.responseTime || prev.responseTime,
uptime: e.record.uptime || prev.uptime,
lastChecked: e.record.last_checked || e.record.lastChecked || prev.lastChecked,
};
});
const setupSubscriptions = async () => {
try {
// Clean up existing subscriptions first
if (subscriptionsRef.current.service) {
subscriptionsRef.current.service();
}
if (subscriptionsRef.current.uptime) {
subscriptionsRef.current.uptime();
}
});
// Subscribe to uptime data updates
const uptimeSubscription = pb.collection('uptime_data').subscribe('*', function(e) {
if (e.record && e.record.service_id === serviceId) {
// console.log("New uptime data:", e.record);
// 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
if (e.record) {
setService(prev => {
if (!prev) return null;
return {
...prev,
status: e.record.status || prev.status,
responseTime: e.record.response_time || e.record.responseTime || prev.responseTime,
uptime: e.record.uptime || prev.uptime,
lastChecked: e.record.last_checked || e.record.lastChecked || prev.lastChecked,
};
});
}
});
subscriptionsRef.current.service = serviceUnsubscribe;
// Subscribe to uptime data updates with throttling
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
const timestamp = new Date(e.record.timestamp);
if (timestamp >= startDate && timestamp <= endDate) {
setUptimeData(prev => {
// Limit the array size to prevent memory issues
const maxRecords = 100;
const newData: UptimeData = {
id: e.record.id,
service_id: e.record.service_id, // Include service_id
serviceId: e.record.service_id, // Keep for backward compatibility
service_id: e.record.service_id,
serviceId: e.record.service_id,
timestamp: e.record.timestamp,
status: e.record.status,
responseTime: e.record.response_time || 0,
date: e.record.timestamp, // Adding required date property
uptime: e.record.uptime || 0 // Adding required uptime property
date: e.record.timestamp,
uptime: e.record.uptime || 0
};
// Add at the beginning of the array to maintain newest first sorting
return [newData, ...prev];
// Add at the beginning and limit array size
const updatedData = [newData, ...prev];
return updatedData.slice(0, maxRecords);
});
}
}
});
});
// Clean up the subscriptions
return () => {
// console.log(`Cleaning up subscriptions for service: ${serviceId}`);
try {
pb.collection('services').unsubscribe(serviceId);
pb.collection('uptime_data').unsubscribe('*');
} catch (error) {
// console.error("Error cleaning up subscriptions:", error);
subscriptionsRef.current.uptime = uptimeUnsubscribe;
} catch (error) {
// console.error("Error setting up real-time updates:", error);
}
};
setupSubscriptions();
// Return cleanup function
return () => {
// console.log(`Cleaning up subscriptions for service: ${serviceId}`);
try {
if (subscriptionsRef.current.service) {
subscriptionsRef.current.service();
}
};
} catch (error) {
// console.error("Error setting up real-time updates:", error);
}
if (subscriptionsRef.current.uptime) {
subscriptionsRef.current.uptime();
}
} catch (error) {
// console.error("Error cleaning up subscriptions:", error);
}
};
}, [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 { Service, UptimeData } from "@/types/service.types";
import { useToast } from "@/hooks/use-toast";
@@ -16,14 +17,17 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
const { toast } = useToast();
const navigate = useNavigate();
// Get regional agents for "all" monitoring
// Get regional agents for "all" monitoring with optimized caching
const { data: regionalAgents = [] } = useQuery({
queryKey: ['regional-services'],
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;
try {
@@ -38,7 +42,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
description: `Service status changed to ${newStatus}`,
});
} catch (error) {
// console.error("Failed to update service status:", error);
// console.error("Failed to update service status:", error);
setService(prevService => prevService);
toast({
@@ -47,9 +51,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
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 {
if (!service) {
// console.log('No service data available for uptime fetch');
@@ -80,7 +84,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
// Fetch default monitoring data
const defaultData = await uptimeService.getUptimeHistory(serviceId, limit, start, end, service.type);
// console.log(`Retrieved ${defaultData.length} default monitoring records`);
// console.log(`Retrieved ${defaultData.length} default monitoring records`);
// Mark default data with source identifier
const markedDefaultData = defaultData.map(record => ({
@@ -100,7 +104,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
const regionalData = await uptimeService.getUptimeHistoryByRegionalAgent(
serviceId, limit, start, end, service.type, agent.region_name, agent.agent_id
);
// console.log(`Retrieved ${regionalData.length} records from ${agent.region_name}`);
// console.log(`Retrieved ${regionalData.length} records from ${agent.region_name}`);
// Mark regional data with source identifier
const markedRegionalData = regionalData.map(record => ({
@@ -112,18 +116,18 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
history = [...history, ...markedRegionalData];
} catch (error) {
// console.error(`Error fetching data from ${agent.region_name}:`, error);
// console.error(`Error fetching data from ${agent.region_name}:`, error);
}
}
// console.log(`Total combined records: ${history.length}`);
// console.log(`Total combined records: ${history.length}`);
} else {
// Fetch regional agent specific data
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);
// console.log(`Retrieved ${history.length} regional monitoring records`);
// console.log(`Retrieved ${history.length} regional monitoring records`);
}
// Sort by timestamp (newest first)
@@ -131,7 +135,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
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);
return filteredHistory;
} catch (error) {
@@ -143,9 +147,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
});
return [];
}
};
}, [service, selectedRegionalAgent, regionalAgents, toast]);
const handleRegionalAgentChange = (agent: string) => {
const handleRegionalAgentChange = useCallback((agent: string) => {
// console.log(`Regional agent changed from ${selectedRegionalAgent} to: ${agent}`);
// Clear data immediately when switching
@@ -154,78 +158,83 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
// Refetch data with new agent selection
if (serviceId && !isLoading && service) {
// console.log(`Refetching data for new agent: ${agent}`);
// console.log(`Refetching data for new agent: ${agent}`);
fetchUptimeData(serviceId, startDate, endDate, '24h', agent);
}
};
}, [selectedRegionalAgent, serviceId, isLoading, service, fetchUptimeData, startDate, endDate]);
// Memoize the service data fetching to prevent unnecessary re-runs
const fetchServiceData = useCallback(async () => {
try {
if (!serviceId) {
setIsLoading(false);
return;
}
setIsLoading(true);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Request timed out")), 10000);
});
const fetchPromise = pb.collection('services').getOne(serviceId);
const serviceData = await Promise.race([fetchPromise, timeoutPromise]) as any;
const formattedService: Service = {
id: serviceData.id,
name: serviceData.name,
url: serviceData.url || "",
host: serviceData.host || "",
port: serviceData.port || undefined,
domain: serviceData.domain || "",
type: serviceData.service_type || serviceData.type || "HTTP",
status: serviceData.status || "paused",
responseTime: serviceData.response_time || serviceData.responseTime || 0,
uptime: serviceData.uptime || 0,
lastChecked: serviceData.last_checked || serviceData.lastChecked || new Date().toLocaleString(),
interval: serviceData.heartbeat_interval || serviceData.interval || 60,
retries: serviceData.max_retries || serviceData.retries || 3,
notificationChannel: serviceData.notification_id,
alertTemplate: serviceData.template_id,
alerts: serviceData.alerts || "unmuted"
};
// console.log(`Loaded service: ${formattedService.name} (${formattedService.type})`);
setService(formattedService);
// Small delay to ensure state is updated before fetching uptime data
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
// console.error("Error fetching service:", error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to load service data. Please try again.",
});
navigate("/dashboard");
} finally {
setIsLoading(false);
}
}, [serviceId, navigate, toast]);
// Initial data loading
useEffect(() => {
const fetchServiceData = async () => {
try {
if (!serviceId) {
setIsLoading(false);
return;
}
setIsLoading(true);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Request timed out")), 10000);
});
const fetchPromise = pb.collection('services').getOne(serviceId);
const serviceData = await Promise.race([fetchPromise, timeoutPromise]) as any;
const formattedService: Service = {
id: serviceData.id,
name: serviceData.name,
url: serviceData.url || "",
host: serviceData.host || "",
port: serviceData.port || undefined,
domain: serviceData.domain || "",
type: serviceData.service_type || serviceData.type || "HTTP",
status: serviceData.status || "paused",
responseTime: serviceData.response_time || serviceData.responseTime || 0,
uptime: serviceData.uptime || 0,
lastChecked: serviceData.last_checked || serviceData.lastChecked || new Date().toLocaleString(),
interval: serviceData.heartbeat_interval || serviceData.interval || 60,
retries: serviceData.max_retries || serviceData.retries || 3,
notificationChannel: serviceData.notification_id,
alertTemplate: serviceData.template_id,
alerts: serviceData.alerts || "unmuted"
};
// console.log(`Loaded service: ${formattedService.name} (${formattedService.type})`);
setService(formattedService);
// Small delay to ensure state is updated before fetching uptime data
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
// console.error("Error fetching service:", error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to load service data. Please try again.",
});
navigate("/dashboard");
} finally {
setIsLoading(false);
}
};
fetchServiceData();
}, [serviceId, navigate, toast]);
}, [fetchServiceData]);
// Update data when date range changes or when service is loaded
// Update data when date range changes or when service is loaded - with debouncing
useEffect(() => {
if (serviceId && !isLoading && service) {
// console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent);
}
}, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents]);
const timeoutId = setTimeout(() => {
// console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent);
}, 500); // Debounce API calls by 500ms
return {
return () => clearTimeout(timeoutId);
}
}, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents, fetchUptimeData]);
return useMemo(() => ({
service,
setService,
uptimeData,
@@ -235,5 +244,5 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
fetchUptimeData,
selectedRegionalAgent,
handleRegionalAgentChange
};
}), [service, uptimeData, isLoading, handleStatusChange, fetchUptimeData, selectedRegionalAgent, handleRegionalAgentChange]);
};
@@ -1,71 +1,70 @@
import React from "react";
import { Check, X, Pause, AlertTriangle } from "lucide-react";
import React, { memo } from "react";
import { Badge } from "@/components/ui/badge";
import { Check } from "lucide-react";
export interface StatusBadgeProps {
status: string;
interface StatusBadgeProps {
status: "up" | "down" | "paused" | "warning";
size?: "sm" | "md" | "lg";
}
export const StatusBadge = ({ status, size = "sm" }: StatusBadgeProps) => {
// Determine the sizing classes based on the size prop
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":
const StatusBadgeComponent = ({ status, size = "sm" }: StatusBadgeProps) => {
const getStatusConfig = (status: string) => {
switch (status) {
case "up":
return {
variant: "default" as const,
className: "bg-emerald-700 text-emerald-100 border-emerald-200 hover:bg-emerald-200",
label:
<span className="flex items-center gap-1">
<Check className="w-4 h-4" /> Up
</span>
};
case "down":
return {
variant: "destructive" as const,
className: "bg-red-700 text-red-100 border-red-200 hover:bg-red-200",
label: "Down"
};
case "warning":
return {
variant: "destructive" as const,
className: "bg-amber-700 text-amber-100 border-amber-200 hover:bg-amber-200",
label: "Warning"
};
case "paused":
return {
variant: "secondary" as const,
className: "bg-gray-700 text-gray-100 border-gray-200 hover:bg-gray-200",
label: "Paused"
};
default:
return "px-2 py-0.5 text-xs gap-0.5";
return {
variant: "outline" as const,
className: "bg-gray-700 text-gray-100 border-gray-200",
label: "Unknown"
};
}
};
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 = {
sm: "text-xs px-2 py-1",
md: "text-sm px-3 py-1.5",
lg: "text-base px-4 py-2"
};
const sizeClasses = getSizeClasses();
const iconSize = getIconSize();
const config = getStatusConfig(status);
switch (status) {
case "up":
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}`}>
<Check className={iconSize} />
<span>Up</span>
</div>
);
case "down":
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}`}>
<X className={iconSize} />
<span>Down</span>
</div>
);
case "warning":
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}`}>
<AlertTriangle className={iconSize} />
<span>Warning</span>
</div>
);
case "paused":
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}`}>
<Pause className={iconSize} />
<span>Paused</span>
</div>
);
default:
return null;
}
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);
+75 -120
View File
@@ -1,9 +1,10 @@
import React from "react";
import { useConsolidatedUptimeData } from "./hooks/useConsolidatedUptimeData";
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { UptimeSummary } from "./uptime/UptimeSummary";
import { formatRelative } from "date-fns";
import React, { memo, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { TooltipProvider } from '@/components/ui/tooltip';
import { UptimeStatusItem } from './uptime/UptimeStatusItem';
import { uptimeService } from '@/services/uptimeService';
import { UptimeData } from '@/types/service.types';
interface UptimeBarProps {
uptime: number;
@@ -13,127 +14,81 @@ interface UptimeBarProps {
serviceType?: string;
}
export const UptimeBar = ({ uptime, status, serviceId, interval, serviceType }: UptimeBarProps) => {
// Use consolidated hook to get properly merged data
const { consolidatedItems, isLoading } = useConsolidatedUptimeData({
serviceId,
serviceType,
status,
interval
const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType = "HTTP" }: UptimeBarProps) => {
// Calculate date range for last 20 checks with much more aggressive caching
const endDate = useMemo(() => new Date(), []);
const startDate = useMemo(() => {
const start = new Date(endDate);
start.setHours(start.getHours() - Math.max(interval * 20 / 3600, 24)); // At least 24 hours
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) => {
if (!hasData) {
return "bg-gray-400"; // No data color - grey
// Memoize the uptime calculation to prevent unnecessary recalculations
const { uptimePercentage, displayData } = useMemo(() => {
if (!uptimeData || uptimeData.length === 0) {
return {
uptimePercentage: uptime || 0,
displayData: []
};
}
switch (itemStatus) {
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>
);
}
// 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;
// console.log('UptimeBar rendering consolidated items:', consolidatedItems.length);
// 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 (
<div className="w-52">
<TooltipProvider>
<div className="flex items-center gap-1">
{consolidatedItems.map((slot, index) => {
// Check if we have actual monitoring data with valid status
const hasValidData = slot.items.length > 0 && slot.items.some(item =>
item.status && ['up', 'down', 'warning', 'paused'].includes(item.status)
);
// 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 className="text-xs text-muted-foreground text-center">
No monitoring data available
</div>
)}
</div>
</TooltipContent>
</Tooltip>
);
})}
<TooltipProvider>
<div className="flex items-center space-x-3">
<div className="flex space-x-0.5 min-w-0">
{statusItems.length > 0 ? statusItems : (
// Fallback display when no data
<div className="h-5 w-1.5 rounded-sm bg-gray-400" />
)}
</div>
</TooltipProvider>
<UptimeSummary uptime={uptime} interval={interval} />
</div>
<span className="text-sm font-medium whitespace-nowrap">
{uptimePercentage.toFixed(1)}%
</span>
</div>
<span className="text-xs text-muted-foreground">
Last 20 checks
</span>
</TooltipProvider>
);
};
};
// 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) => {
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({
queryKey: ['allUptimeHistory', serviceId, serviceType],
queryFn: async () => {
@@ -34,8 +34,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
return allMonitoringData;
},
enabled: !!serviceId,
refetchInterval: 30000,
staleTime: 15000,
refetchInterval: 60000, // 1 minute polling
staleTime: 30000, // Data is fresh for 30 seconds
placeholderData: (previousData) => previousData,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
+24 -7
View File
@@ -20,7 +20,7 @@ const Dashboard = () => {
// For debugging user data
useEffect(() => {
// console.log("Current user data:", currentUser);
// console.log("Current user data:", currentUser);
}, [currentUser]);
// Handle logout
@@ -29,22 +29,39 @@ const Dashboard = () => {
navigate("/login");
};
// Fetch all services
// Fetch all services with 1-minute polling for real-time updates
const { data: services = [], isLoading, error } = useQuery({
queryKey: ['services'],
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(() => {
let hasStarted = false;
const startActiveServices = async () => {
if (hasStarted) return;
hasStarted = true;
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
if (isLoading) {
@@ -3,13 +3,13 @@ import { pb } from '@/lib/pocketbase';
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> {
try {
// First check if the service is already being monitored
if (monitoringIntervals.has(serviceId)) {
// console.log(`Service ${serviceId} is already being monitored`);
// console.log(`Service ${serviceId} is already being monitored`);
return;
}
@@ -22,7 +22,7 @@ export async function startMonitoringService(serviceId: string): Promise<void> {
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
await pb.collection('services').update(serviceId, {
@@ -30,20 +30,24 @@ export async function startMonitoringService(serviceId: string): Promise<void> {
});
// The actual service checking is now handled by the Go microservice
// This frontend service just tracks the monitoring state
const intervalMs = (service.heartbeat_interval || 60) * 1000;
// console.log(`Service ${service.name} monitoring delegated to backend service`);
// This frontend service just tracks the monitoring state with much less frequency
const intervalMs = Math.max((service.heartbeat_interval || 60) * 1000, 60000); // Minimum 1 minute
// 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
// Significantly reduce frequency to prevent excessive logging and CPU usage
const intervalId = window.setInterval(() => {
// console.log(`Monitoring active for service ${service.name} (handled by backend)`);
}, intervalMs);
// 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)`);
}
}, Math.max(intervalMs, 300000)); // Minimum 5 minutes for logging
// Store the interval ID for this service
monitoringIntervals.set(serviceId, intervalId);
// console.log(`Monitoring registered for service ${serviceId}`);
// console.log(`Optimized monitoring registered for service ${serviceId}`);
} catch (error) {
// console.error("Error starting service monitoring:", error);
// console.error("Error starting service monitoring:", error);
}
}