From dc586dc15fef2d925796696baef3d9e814de7b27 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Sat, 19 Jul 2025 15:23:28 +0700 Subject: [PATCH] Fix: Server Historical Performance chart improvement Fix: Improve sidebar collapse/expand performance --- .../src/components/dashboard/Sidebar.tsx | 8 +- .../components/dashboard/sidebar/MenuItem.tsx | 16 +- .../docker/DockerContainersTable.tsx | 2 +- .../components/docker/DockerStatsCards.tsx | 55 +- .../components/servers/EditServerDialog.tsx | 680 ++++++++++++++++++ .../components/servers/OneClickInstallTab.tsx | 2 +- .../servers/ServerHistoryCharts.tsx | 34 +- .../src/components/servers/ServerTable.tsx | 349 ++++----- .../components/servers/charts/CPUChart.tsx | 93 ++- .../components/servers/charts/DiskChart.tsx | 93 ++- .../components/servers/charts/MemoryChart.tsx | 93 ++- .../servers/charts/NetworkChart.tsx | 127 ++-- .../components/servers/charts/dataUtils.ts | 113 ++- application/src/pages/InstanceMonitoring.tsx | 10 +- application/src/pages/ServerDetail.tsx | 104 ++- application/src/services/serverService.ts | 271 +++---- .../src/services/serverThresholdService.ts | 85 +++ application/src/services/templateService.ts | 41 +- application/src/types/server.types.ts | 1 + 19 files changed, 1534 insertions(+), 643 deletions(-) create mode 100644 application/src/components/servers/EditServerDialog.tsx create mode 100644 application/src/services/serverThresholdService.ts diff --git a/application/src/components/dashboard/Sidebar.tsx b/application/src/components/dashboard/Sidebar.tsx index 4d15f9d..21ba733 100644 --- a/application/src/components/dashboard/Sidebar.tsx +++ b/application/src/components/dashboard/Sidebar.tsx @@ -13,7 +13,13 @@ export const Sidebar = ({ collapsed }: SidebarProps) => { const { theme } = useTheme(); return ( -
+
diff --git a/application/src/components/dashboard/sidebar/MenuItem.tsx b/application/src/components/dashboard/sidebar/MenuItem.tsx index 019d5ad..ec9f8cd 100644 --- a/application/src/components/dashboard/sidebar/MenuItem.tsx +++ b/application/src/components/dashboard/sidebar/MenuItem.tsx @@ -34,7 +34,6 @@ export const MenuItem: React.FC = ({ e.stopPropagation(); if (hasNavigation && path) { - // Use navigate instead of window.location to prevent full page reload navigate(path, { replace: false }); } }; @@ -44,11 +43,22 @@ export const MenuItem: React.FC = ({ return (
- {!collapsed && {t(translationKey)}} + {!collapsed && ( + + {t(translationKey)} + + )}
); }; \ No newline at end of file diff --git a/application/src/components/docker/DockerContainersTable.tsx b/application/src/components/docker/DockerContainersTable.tsx index 5ec906c..9e39429 100644 --- a/application/src/components/docker/DockerContainersTable.tsx +++ b/application/src/components/docker/DockerContainersTable.tsx @@ -62,7 +62,7 @@ export const DockerContainersTable = ({ containers, isLoading, onRefresh }: Dock
-
+
diff --git a/application/src/components/docker/DockerStatsCards.tsx b/application/src/components/docker/DockerStatsCards.tsx index 6376b6d..cf43d56 100644 --- a/application/src/components/docker/DockerStatsCards.tsx +++ b/application/src/components/docker/DockerStatsCards.tsx @@ -3,44 +3,51 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Container, Play, Square, AlertTriangle } from "lucide-react"; import { DockerStats } from "@/types/docker.types"; +import { useTheme } from "@/contexts/ThemeContext"; interface DockerStatsCardsProps { stats: DockerStats; } export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => { + const { theme } = useTheme(); + const cards = [ { title: "Total Containers", value: stats.total, icon: Container, color: "text-blue-600", - bgColor: "bg-blue-50", - borderColor: "border-blue-200", + gradient: theme === 'dark' + ? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(59, 130, 246, 0.6) 100%)" + : "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #3b82f6 100%)" }, { title: "Running", value: stats.running, icon: Play, color: "text-green-600", - bgColor: "bg-green-50", - borderColor: "border-green-200", + gradient: theme === 'dark' + ? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(16, 185, 129, 0.6) 100%)" + : "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #10b981 100%)" }, { title: "Stopped", value: stats.stopped, icon: Square, color: "text-gray-600", - bgColor: "bg-gray-50", - borderColor: "border-gray-200", + gradient: theme === 'dark' + ? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(107, 114, 128, 0.6) 100%)" + : "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #6b7280 100%)" }, { title: "Warning", value: stats.warning, icon: AlertTriangle, color: "text-amber-600", - bgColor: "bg-amber-50", - borderColor: "border-amber-200", + gradient: theme === 'dark' + ? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(245, 158, 11, 0.6) 100%)" + : "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #f59e0b 100%)" }, ]; @@ -49,23 +56,39 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => { {cards.map((card) => { const IconComponent = card.icon; return ( - - - + + {/* Grid Pattern Overlay */} +
+
+
+ + + {card.title} -
- +
+
- +
-
+
{card.value}
Containers diff --git a/application/src/components/servers/EditServerDialog.tsx b/application/src/components/servers/EditServerDialog.tsx new file mode 100644 index 0000000..9c6b618 --- /dev/null +++ b/application/src/components/servers/EditServerDialog.tsx @@ -0,0 +1,680 @@ +import React, { useState, useEffect } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { useToast } from "@/hooks/use-toast"; +import { pb } from "@/lib/pocketbase"; +import { Server } from "@/types/server.types"; +import { RefreshCw, X } from "lucide-react"; +import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; +import { templateService, NotificationTemplate } from "@/services/templateService"; +import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService"; + +interface EditServerDialogProps { + server: Server | null; + open: boolean; + onOpenChange: (open: boolean) => void; + onServerUpdated: () => void; +} + +interface ServerFormData { + name: string; + check_interval: number; + retry_attempts: number; + docker_monitoring: boolean; + notification_enabled: boolean; + notification_channels: string[]; // Changed to array for multiple selections + threshold_id: string; + template_id: string; +} + +interface ThresholdFormData { + cpu_threshold: number; + ram_threshold: number; + disk_threshold: number; + network_threshold: number; +} + +export const EditServerDialog: React.FC = ({ + server, + open, + onOpenChange, + onServerUpdated, +}) => { + const [formData, setFormData] = useState({ + name: "", + check_interval: 60, + retry_attempts: 3, + docker_monitoring: false, + notification_enabled: false, + notification_channels: [], // Changed to array + threshold_id: "none", + template_id: "none", + }); + + const [thresholdFormData, setThresholdFormData] = useState({ + cpu_threshold: 80, + ram_threshold: 80, + disk_threshold: 80, + network_threshold: 80, + }); + + const [isSubmitting, setIsSubmitting] = useState(false); + const [alertConfigs, setAlertConfigs] = useState([]); + const [templates, setTemplates] = useState([]); + const [thresholds, setThresholds] = useState([]); + const [selectedTemplate, setSelectedTemplate] = useState(null); + const [selectedThreshold, setSelectedThreshold] = useState(null); + const [loadingAlertConfigs, setLoadingAlertConfigs] = useState(false); + const [loadingTemplates, setLoadingTemplates] = useState(false); + const [loadingThresholds, setLoadingThresholds] = useState(false); + const { toast } = useToast(); + + // Initialize form data when server changes + useEffect(() => { + if (server) { + console.log("Setting form data for server:", server); + // Parse comma-separated notification_id into array + const notificationChannels = server.notification_id + ? server.notification_id.split(',').map(id => id.trim()).filter(id => id) + : []; + + setFormData({ + name: server.name || "", + check_interval: server.check_interval || 60, + retry_attempts: 3, + docker_monitoring: server.docker === "true", + notification_enabled: notificationChannels.length > 0, + notification_channels: notificationChannels, + threshold_id: server.threshold_id || "none", + template_id: server.template_id || "none", + }); + } + }, [server]); + + // Load data when dialog opens + useEffect(() => { + if (open) { + loadAlertConfigurations(); + loadTemplates(); + loadThresholds(); + } + }, [open]); + + // Load existing threshold data when thresholds are loaded and we have a server with threshold_id + useEffect(() => { + if (server && server.threshold_id && thresholds.length > 0) { + console.log("Loading existing threshold data for server:", server.threshold_id); + const existingThreshold = thresholds.find(t => t.id === server.threshold_id); + if (existingThreshold) { + console.log("Found existing threshold:", existingThreshold); + setSelectedThreshold(existingThreshold); + // Handle the API response format with proper field names and type conversion + setThresholdFormData({ + cpu_threshold: parseInt(String(existingThreshold.cpu_threshold)) || 80, + ram_threshold: parseInt(String((existingThreshold as any).ram_threshold_message || existingThreshold.ram_threshold)) || 80, + disk_threshold: parseInt(String(existingThreshold.disk_threshold)) || 80, + network_threshold: parseInt(String(existingThreshold.network_threshold)) || 80, + }); + } + } + }, [server, thresholds]); + + // Update selected template when form data or templates change + useEffect(() => { + if (formData.template_id && formData.template_id !== "none" && templates.length > 0) { + const template = templates.find(t => t.id === formData.template_id); + setSelectedTemplate(template || null); + } else { + setSelectedTemplate(null); + } + }, [formData.template_id, templates]); + + // Update selected threshold when threshold_id changes in form + useEffect(() => { + if (formData.threshold_id && formData.threshold_id !== "none" && thresholds.length > 0) { + const threshold = thresholds.find(t => t.id === formData.threshold_id); + setSelectedThreshold(threshold || null); + if (threshold) { + // Handle the API response format with proper field names and type conversion + setThresholdFormData({ + cpu_threshold: parseInt(String(threshold.cpu_threshold)) || 80, + ram_threshold: parseInt(String((threshold as any).ram_threshold_message || threshold.ram_threshold)) || 80, + disk_threshold: parseInt(String(threshold.disk_threshold)) || 80, + network_threshold: parseInt(String(threshold.network_threshold)) || 80, + }); + } + } else if (formData.threshold_id === "none") { + setSelectedThreshold(null); + setThresholdFormData({ + cpu_threshold: 80, + ram_threshold: 80, + disk_threshold: 80, + network_threshold: 80, + }); + } + }, [formData.threshold_id, thresholds]); + + const loadAlertConfigurations = async () => { + try { + setLoadingAlertConfigs(true); + const configs = await alertConfigService.getAlertConfigurations(); + setAlertConfigs(configs); + } catch (error) { + console.error('Error loading alert configurations:', error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to load notification channels", + }); + } finally { + setLoadingAlertConfigs(false); + } + }; + + const loadTemplates = async () => { + try { + setLoadingTemplates(true); + const templateList = await templateService.getTemplates(); + setTemplates(templateList); + } catch (error) { + console.error('Error loading templates:', error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to load templates", + }); + } finally { + setLoadingTemplates(false); + } + }; + + const loadThresholds = async () => { + try { + setLoadingThresholds(true); + const thresholdList = await serverThresholdService.getServerThresholds(); + setThresholds(thresholdList); + } catch (error) { + console.error('Error loading server thresholds:', error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to load server thresholds", + }); + } finally { + setLoadingThresholds(false); + } + }; + + const handleThresholdUpdate = async () => { + if (!selectedThreshold) return; + + try { + // Use the correct field name for RAM threshold + const updateData = { + cpu_threshold: thresholdFormData.cpu_threshold, + ram_threshold_message: thresholdFormData.ram_threshold, // Use the correct field name + disk_threshold: thresholdFormData.disk_threshold, + network_threshold: thresholdFormData.network_threshold, + }; + + await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateData); + + // Update local state + setSelectedThreshold({ + ...selectedThreshold, + ...thresholdFormData, + }); + + // Update thresholds list + setThresholds(prev => prev.map(t => + t.id === selectedThreshold.id + ? { ...t, ...thresholdFormData } + : t + )); + + toast({ + title: "Threshold updated", + description: "Server threshold values have been updated successfully.", + }); + } catch (error) { + console.error('Error updating threshold:', error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to update threshold values.", + }); + } + }; + + const handleNotificationChannelToggle = (channelId: string, checked: boolean) => { + setFormData(prev => ({ + ...prev, + notification_channels: checked + ? [...prev.notification_channels, channelId] + : prev.notification_channels.filter(id => id !== channelId) + })); + }; + + const removeNotificationChannel = (channelId: string) => { + setFormData(prev => ({ + ...prev, + notification_channels: prev.notification_channels.filter(id => id !== channelId) + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!server || isSubmitting) return; + + try { + setIsSubmitting(true); + + // Convert notification channels array to comma-separated string + const notificationChannelsString = formData.notification_enabled + ? formData.notification_channels.join(',') + : ""; + + const updateData = { + name: formData.name, + check_interval: formData.check_interval, + docker: formData.docker_monitoring ? "true" : "false", + notification_id: notificationChannelsString, + threshold_id: formData.notification_enabled && formData.threshold_id !== "none" ? formData.threshold_id : "", + template_id: formData.notification_enabled && formData.template_id !== "none" ? formData.template_id : "", + updated: new Date().toISOString(), + }; + + await pb.collection('servers').update(server.id, updateData); + + toast({ + title: "Server updated", + description: `${formData.name} has been updated successfully.`, + }); + + onServerUpdated(); + onOpenChange(false); + + } catch (error) { + console.error('Error updating server:', error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to update server. Please try again.", + }); + } finally { + setIsSubmitting(false); + } + }; + + const handleCancel = () => { + if (server) { + const notificationChannels = server.notification_id + ? server.notification_id.split(',').map(id => id.trim()).filter(id => id) + : []; + + setFormData({ + name: server.name || "", + check_interval: server.check_interval || 60, + retry_attempts: 3, + docker_monitoring: server.docker === "true", + notification_enabled: notificationChannels.length > 0, + notification_channels: notificationChannels, + threshold_id: server.threshold_id || "none", + template_id: server.template_id || "none", + }); + } + onOpenChange(false); + }; + + return ( + + + + Edit Server Configuration + + +
+
+
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder="Enter server name" + required + /> +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ setFormData(prev => ({ + ...prev, + docker_monitoring: checked + }))} + /> + +
+
+
+ + {/* Notification Status Toggle */} +
+
+ setFormData(prev => ({ + ...prev, + notification_enabled: checked, + notification_channels: checked ? prev.notification_channels : [], + threshold_id: checked ? prev.threshold_id : "none", + template_id: checked ? prev.template_id : "none" + }))} + /> + +
+ + {/* Expanded Notification Settings */} + {formData.notification_enabled && ( + + + Notification Settings + + + {/* Multiple Notification Channels Selection */} +
+ +
+ {loadingAlertConfigs ? ( +
Loading channels...
+ ) : alertConfigs.length > 0 ? ( + alertConfigs.map((config) => ( +
+ + handleNotificationChannelToggle(config.id || "", checked as boolean) + } + /> + +
+ )) + ) : ( +
No notification channels available
+ )} +
+ + {/* Selected Channels Display */} + {formData.notification_channels.length > 0 && ( +
+ +
+ {formData.notification_channels.map((channelId) => { + const channel = alertConfigs.find(c => c.id === channelId); + return ( +
+ {channel?.notify_name || channelId} + +
+ ); + })} +
+
+ )} +
+ + {/* Server Set Threshold Selection */} +
+ + +
+ + {/* Editable Threshold Details */} + {selectedThreshold && ( + + + Threshold Details: {selectedThreshold.name} + + + +
+
+ + setThresholdFormData(prev => ({ + ...prev, + cpu_threshold: parseInt(e.target.value) || 0 + }))} + className="mt-1" + /> +
+
+ + setThresholdFormData(prev => ({ + ...prev, + ram_threshold: parseInt(e.target.value) || 0 + }))} + className="mt-1" + /> +
+
+ + setThresholdFormData(prev => ({ + ...prev, + disk_threshold: parseInt(e.target.value) || 0 + }))} + className="mt-1" + /> +
+
+ + setThresholdFormData(prev => ({ + ...prev, + network_threshold: parseInt(e.target.value) || 0 + }))} + className="mt-1" + /> +
+
+
+
+ )} + + {/* Server Template Selection */} +
+ + +
+ + {/* Template Details */} + {selectedTemplate && ( + + + Template Details: {selectedTemplate.name} + + +
+
+ +

+ {selectedTemplate.up_message || "No RAM threshold message defined"} +

+
+
+ +

+ {selectedTemplate.down_message || "No CPU threshold message defined"} +

+
+
+ +

+ {selectedTemplate.incident_message || "No disk threshold message defined"} +

+
+
+ +

+ {selectedTemplate.maintenance_message || "No network threshold message defined"} +

+
+
+
+
+ )} +
+
+ )} +
+ +
+ + +
+ +
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/servers/OneClickInstallTab.tsx b/application/src/components/servers/OneClickInstallTab.tsx index 767e73e..1067d20 100644 --- a/application/src/components/servers/OneClickInstallTab.tsx +++ b/application/src/components/servers/OneClickInstallTab.tsx @@ -27,7 +27,7 @@ export const OneClickInstallTab: React.FC = ({ onDialogClose, }) => { const getOneClickInstallCommand = () => { - const scriptUrl = "https://raw.githubusercontent.com/operacle/checke-server-agent/refs/heads/main/server-agent.sh"; + const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh"; return `curl -L -o server-agent.sh "${scriptUrl}" chmod +x server-agent.sh diff --git a/application/src/components/servers/ServerHistoryCharts.tsx b/application/src/components/servers/ServerHistoryCharts.tsx index 274f966..688c69f 100644 --- a/application/src/components/servers/ServerHistoryCharts.tsx +++ b/application/src/components/servers/ServerHistoryCharts.tsx @@ -24,7 +24,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { isFetching } = useServerHistoryData(serverId); - //console.log('ServerHistoryCharts: Rendering with serverId:', serverId, 'timeRange:', timeRange); + console.log('ServerHistoryCharts: Rendering with serverId:', serverId, 'timeRange:', timeRange); // Memoize latest data calculation to prevent unnecessary recalculations const latestData = useMemo(() => { @@ -48,10 +48,10 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
{/* Skeleton loading cards */} -
+
{[1, 2, 3, 4].map((index) => ( - +
@@ -60,7 +60,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
-
+
@@ -71,7 +71,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { } if (error) { - // console.error('ServerHistoryCharts: Error loading data:', error); + console.error('ServerHistoryCharts: Error loading data:', error); return (
@@ -122,11 +122,11 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { ); } - // console.log('ServerHistoryCharts: Rendering charts with', chartData.length, 'data points for time range:', timeRange); + console.log('ServerHistoryCharts: Rendering charts with', chartData.length, 'data points for time range:', timeRange); return (
-
+

Historical Performance

@@ -143,12 +143,20 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
- {/* Use CSS Grid for better performance than flexbox */} -
- - - - + {/* Improved responsive grid layout */} +
+
+ +
+
+ +
+
+ +
+
+ +
); diff --git a/application/src/components/servers/ServerTable.tsx b/application/src/components/servers/ServerTable.tsx index dd1c43b..02ceb43 100644 --- a/application/src/components/servers/ServerTable.tsx +++ b/application/src/components/servers/ServerTable.tsx @@ -1,4 +1,3 @@ - import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; @@ -13,9 +12,11 @@ import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Tr import { Server } from "@/types/server.types"; import { ServerStatusBadge } from "./ServerStatusBadge"; import { OSTypeIcon } from "./OSTypeIcon"; +import { EditServerDialog } from "./EditServerDialog"; import { serverService } from "@/services/serverService"; import { useToast } from "@/hooks/use-toast"; import { pb } from "@/lib/pocketbase"; +import { useTheme } from "@/contexts/ThemeContext"; interface ServerTableProps { servers: Server[]; @@ -24,8 +25,10 @@ interface ServerTableProps { } export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => { + const { theme } = useTheme(); const [searchTerm, setSearchTerm] = useState(""); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [editDialogOpen, setEditDialogOpen] = useState(false); const [selectedServer, setSelectedServer] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const [pausingServers, setPausingServers] = useState>(new Set()); @@ -104,9 +107,9 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) } }; - const handleEdit = (serverId: string) => { - // TODO: Implement edit functionality - console.log('Edit server:', serverId); + const handleEdit = (server: Server) => { + setSelectedServer(server); + setEditDialogOpen(true); }; const handleDelete = (server: Server) => { @@ -165,8 +168,8 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) return ( <> - - + +
Servers
@@ -185,185 +188,191 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
- + {filteredServers.length === 0 ? ( -
+

No servers found

) : ( -
-
-
- - - Name - Status - OS - IP Address - CPU - Memory - Disk - Uptime - Last Checked - Actions - - - - {filteredServers.map((server) => { - const cpuUsage = server.cpu_usage || 0; - const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0; - const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0; - const isPaused = server.status === "paused"; - const isProcessing = pausingServers.has(server.id); +
+
+ + + Name + Status + OS + IP Address + CPU + Memory + Disk + Uptime + Last Checked + Actions + + + + {filteredServers.map((server) => { + const cpuUsage = server.cpu_usage || 0; + const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0; + const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0; + const isPaused = server.status === "paused"; + const isProcessing = pausingServers.has(server.id); - return ( - - -
- {server.name} + return ( + + +
+ {server.name} +
+
+ + + + +
+ + + {server.os_type} + +
+
+ + + {server.ip_address} + + + +
+
+ {cpuUsage.toFixed(1)}% + {server.cpu_cores} cores
- - - - - -
- - - {server.os_type} - + 90 ? "bg-red-500" : + cpuUsage > 75 ? "bg-orange-500" : + cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500" + } + /> +
+
+ +
+
+ {memoryUsage.toFixed(1)}% + {serverService.formatBytes(server.ram_total)}
- - - - {server.ip_address} - - - -
-
- {cpuUsage.toFixed(1)}% - {server.cpu_cores} cores -
- 90 ? "bg-red-500" : - cpuUsage > 75 ? "bg-orange-500" : - cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500" - } - /> + 90 ? "bg-red-500" : + memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500" + } + /> +
+
+ +
+
+ {diskUsage.toFixed(1)}% + {serverService.formatBytes(server.disk_total)}
- - -
-
- {memoryUsage.toFixed(1)}% - {serverService.formatBytes(server.ram_total)} -
- 90 ? "bg-red-500" : - memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500" - } - /> -
-
- -
-
- {diskUsage.toFixed(1)}% - {serverService.formatBytes(server.disk_total)} -
- 95 ? "bg-red-500" : - diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500" - } - /> -
-
- -
- {server.uptime} -
-
- -
- {new Date(server.last_checked).toLocaleString()} -
-
- - - - - - - handleViewDetails(server.id)}> - - View Server Detail - - {server.docker === 'true' && ( - handleViewContainers(server.id)}> - - Container Monitoring - + 95 ? "bg-red-500" : + diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500" + } + /> +
+
+ +
+ {server.uptime} +
+
+ +
+ {new Date(server.last_checked).toLocaleString()} +
+
+ + + + + + + handleViewDetails(server.id)}> + + View Server Detail + + {server.docker === 'true' && ( + handleViewContainers(server.id)}> + + Container Monitoring - - handleEdit(server.id)}> - - Edit Server - - handleDelete(server)} - className="text-red-600 focus:text-red-600" - > - - Delete Server - - - - - - ); - })} - -
-
+ )} + + handlePauseResume(server)} + disabled={isProcessing} + > + {isPaused ? ( + <> + + Resume Monitoring + + ) : ( + <> + + Pause Monitoring + + )} + + + handleEdit(server)}> + + Edit Server + + handleDelete(server)} + className="text-red-600 focus:text-red-600" + > + + Delete Server + + + + + + ); + })} + +
)} + {/* Edit Server Dialog */} + + {/* Delete Confirmation Dialog */} diff --git a/application/src/components/servers/charts/CPUChart.tsx b/application/src/components/servers/charts/CPUChart.tsx index c5fb013..29dff23 100644 --- a/application/src/components/servers/charts/CPUChart.tsx +++ b/application/src/components/servers/charts/CPUChart.tsx @@ -1,7 +1,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ChartContainer, ChartTooltip } from "@/components/ui/chart"; -import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts"; +import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts"; import { Cpu } from "lucide-react"; import { useTheme } from "@/contexts/ThemeContext"; import { DetailedTooltipContent } from "./tooltips/DetailedTooltipContent"; @@ -18,65 +18,62 @@ export const CPUChart = ({ data, latestData }: CPUChartProps) => { const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280'; return ( - - + +
- +
- CPU Usage + CPU Usage
{latestData && ( -
+
{latestData.cpuUsage}%
{latestData.cpuCores} cores
)} - - - - - - - - - - - - - } - cursor={{ stroke: getGridColor() }} - /> - - - + +
+ + + + + + + + + + + + } + cursor={{ stroke: getGridColor() }} + /> + + + +
); diff --git a/application/src/components/servers/charts/DiskChart.tsx b/application/src/components/servers/charts/DiskChart.tsx index da82c15..d94098c 100644 --- a/application/src/components/servers/charts/DiskChart.tsx +++ b/application/src/components/servers/charts/DiskChart.tsx @@ -1,7 +1,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ChartContainer, ChartTooltip } from "@/components/ui/chart"; -import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts"; +import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts"; import { HardDrive } from "lucide-react"; import { useTheme } from "@/contexts/ThemeContext"; import { DetailedTooltipContent } from "./tooltips/DetailedTooltipContent"; @@ -18,65 +18,62 @@ export const DiskChart = ({ data, latestData }: DiskChartProps) => { const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280'; return ( - - + +
- +
- Disk Usage + Disk Usage
{latestData && ( -
+
{latestData.diskUsagePercent}%
{latestData.diskUsed} / {latestData.diskTotal}
)} - - - - - - - - - - - - - } - cursor={{ stroke: getGridColor() }} - /> - - - + +
+ + + + + + + + + + + + } + cursor={{ stroke: getGridColor() }} + /> + + + +
); diff --git a/application/src/components/servers/charts/MemoryChart.tsx b/application/src/components/servers/charts/MemoryChart.tsx index a0abd7b..be341ff 100644 --- a/application/src/components/servers/charts/MemoryChart.tsx +++ b/application/src/components/servers/charts/MemoryChart.tsx @@ -1,7 +1,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ChartContainer, ChartTooltip } from "@/components/ui/chart"; -import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts"; +import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts"; import { MemoryStick } from "lucide-react"; import { useTheme } from "@/contexts/ThemeContext"; import { DetailedTooltipContent } from "./tooltips/DetailedTooltipContent"; @@ -18,65 +18,62 @@ export const MemoryChart = ({ data, latestData }: MemoryChartProps) => { const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280'; return ( - - + +
- +
- Memory Usage + Memory Usage
{latestData && ( -
+
{latestData.ramUsagePercent}%
{latestData.ramUsed} / {latestData.ramTotal}
)} - - - - - - - - - - - - - } - cursor={{ stroke: getGridColor() }} - /> - - - + +
+ + + + + + + + + + + + } + cursor={{ stroke: getGridColor() }} + /> + + + +
); diff --git a/application/src/components/servers/charts/NetworkChart.tsx b/application/src/components/servers/charts/NetworkChart.tsx index cbb25b5..69985d6 100644 --- a/application/src/components/servers/charts/NetworkChart.tsx +++ b/application/src/components/servers/charts/NetworkChart.tsx @@ -1,7 +1,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ChartContainer, ChartTooltip } from "@/components/ui/chart"; -import { LineChart, Line, XAxis, YAxis, CartesianGrid } from "recharts"; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts"; import { Wifi } from "lucide-react"; import { useTheme } from "@/contexts/ThemeContext"; import { NetworkTooltipContent } from "./tooltips/NetworkTooltipContent"; @@ -18,84 +18,77 @@ export const NetworkChart = ({ data, latestData }: NetworkChartProps) => { const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280'; return ( - - + +
- +
- Network Traffic + Network Traffic
{latestData && ( -
+
{latestData.networkRxSpeed} KB/s ↓
{latestData.networkTxSpeed} KB/s ↑
)} - - - - - - - - - - - - - - - - - - - - } - cursor={{ stroke: getGridColor() }} - /> - - - - + +
+ + + + + + + + + + + + + + + + + + + } + cursor={{ stroke: getGridColor() }} + /> + + + + +
); diff --git a/application/src/components/servers/charts/dataUtils.ts b/application/src/components/servers/charts/dataUtils.ts index d58e2a0..20976fe 100644 --- a/application/src/components/servers/charts/dataUtils.ts +++ b/application/src/components/servers/charts/dataUtils.ts @@ -1,4 +1,3 @@ - export const formatBytes = (bytes: number, decimals = 2) => { if (bytes === 0) return '0 B'; const k = 1024; @@ -49,34 +48,82 @@ export const timeRangeOptions = [ { value: '3m' as TimeRange, label: 'Last 90 days', hours: 24 * 90 }, ]; -// Optimized time range filtering with early returns export const filterMetricsByTimeRange = (metrics: any[], timeRange: TimeRange): any[] => { - if (!metrics?.length) return []; + if (!metrics?.length) { + console.log('🔍 filterMetricsByTimeRange: No metrics provided'); + return []; + } + + console.log('🔍 filterMetricsByTimeRange: Starting with', metrics.length, 'metrics for', timeRange); const now = new Date(); + + // For 60m, let's be very specific about what we're looking for + if (timeRange === '60m') { + console.log('⏰ 60m filter: Current time:', now.toISOString()); + + // Try exact 60 minutes first + const cutoffTime60m = new Date(now.getTime() - (60 * 60 * 1000)); + console.log('⏰ 60m filter: Cutoff time:', cutoffTime60m.toISOString()); + + const filtered60m = metrics.filter(metric => { + const metricTime = new Date(metric.created || metric.timestamp); + const isWithinRange = metricTime >= cutoffTime60m && metricTime <= now; + + if (!isWithinRange) { + const ageMinutes = Math.round((now.getTime() - metricTime.getTime()) / (1000 * 60)); + console.log('⏰ Excluding record:', { + created: metric.created || metric.timestamp, + ageMinutes: ageMinutes, + reason: ageMinutes > 60 ? 'too old' : 'future date' + }); + } + + return isWithinRange; + }); + + console.log('✅ 60m strict filter result:', filtered60m.length, 'records'); + + if (filtered60m.length > 0) { + return filtered60m.sort((a, b) => + new Date(a.created || a.timestamp).getTime() - new Date(b.created || b.timestamp).getTime() + ); + } + + // If no data in exactly 60m, show what we have and return it anyway for debugging + console.log('⚠️ No data in 60m range, showing all available data ages:'); + metrics.forEach((metric, index) => { + const metricTime = new Date(metric.created || metric.timestamp); + const ageMinutes = Math.round((now.getTime() - metricTime.getTime()) / (1000 * 60)); + console.log(`Record ${index}: ${metric.created || metric.timestamp} (${ageMinutes} minutes ago)`); + }); + + // Return the most recent data regardless of age + const sorted = metrics.sort((a, b) => + new Date(b.created || b.timestamp).getTime() - new Date(a.created || a.timestamp).getTime() + ); + console.log('🔄 Returning most recent available data for 60m view'); + return sorted.slice(0, 20); // Show last 20 records + } + + // For other time ranges, use normal filtering const selectedRange = timeRangeOptions.find(opt => opt.value === timeRange); if (!selectedRange) return metrics; - // Add small buffer to avoid edge cases - const bufferMinutes = timeRange === '60m' ? 5 : timeRange === '1d' ? 30 : 60; - const cutoffTime = new Date(now.getTime() - (selectedRange.hours * 60 * 60 * 1000) - (bufferMinutes * 60 * 1000)); - -// console.log('filterMetricsByTimeRange: timeRange:', timeRange, 'cutoffTime:', cutoffTime.toISOString()); + const cutoffTime = new Date(now.getTime() - (selectedRange.hours * 60 * 60 * 1000)); const filtered = metrics.filter(metric => { const metricTime = new Date(metric.created || metric.timestamp); return metricTime >= cutoffTime && metricTime <= now; }); -// console.log('filterMetricsByTimeRange: Filtered', metrics.length, 'to', filtered.length, 'metrics'); + console.log('✅ Filtered', metrics.length, 'to', filtered.length, 'metrics for', timeRange); - // Sort by timestamp for proper chart display return filtered.sort((a, b) => new Date(a.created || a.timestamp).getTime() - new Date(b.created || b.timestamp).getTime() ); }; -// Optimized timestamp formatting with caching const timestampCache = new Map(); const formatTimestamp = (timestamp: string, timeRange: TimeRange): string => { @@ -118,7 +165,6 @@ const formatTimestamp = (timestamp: string, timeRange: TimeRange): string => { }); } - // Cache the result (limit cache size) if (timestampCache.size > 1000) { timestampCache.clear(); } @@ -127,37 +173,49 @@ const formatTimestamp = (timestamp: string, timeRange: TimeRange): string => { return formatted; }; -// Optimized chart data formatting with better performance export const formatChartData = (metrics: any[], timeRange: TimeRange) => { -// console.log('formatChartData: Input metrics count:', metrics?.length || 0, 'timeRange:', timeRange); + console.log('📊 formatChartData: Processing', { + inputCount: metrics?.length || 0, + timeRange, + sampleMetric: metrics?.[0] ? { + id: metrics[0].id, + created: metrics[0].created, + timestamp: metrics[0].timestamp, + server_id: metrics[0].server_id + } : null + }); - if (!metrics?.length) return []; + if (!metrics?.length) { + console.log('❌ formatChartData: No metrics provided'); + return []; + } const filteredMetrics = filterMetricsByTimeRange(metrics, timeRange); -// console.log('formatChartData: After time filtering:', filteredMetrics?.length || 0, 'metrics'); + console.log('📊 formatChartData: After time filtering:', filteredMetrics?.length || 0, 'metrics'); - if (!filteredMetrics.length) return []; + if (!filteredMetrics.length) { + console.log('❌ formatChartData: No metrics after time filtering'); + return []; + } // Dynamic sampling based on time range and data volume let maxDataPoints: number; switch (timeRange) { - case '60m': maxDataPoints = 60; break; // 1 point per minute max - case '1d': maxDataPoints = 144; break; // 1 point per 10 minutes max - case '7d': maxDataPoints = 168; break; // 1 point per hour max - case '1m': maxDataPoints = 120; break; // 1 point per 6 hours max - case '3m': maxDataPoints = 90; break; // 1 point per day max + case '60m': maxDataPoints = 60; break; + case '1d': maxDataPoints = 144; break; + case '7d': maxDataPoints = 168; break; + case '1m': maxDataPoints = 120; break; + case '3m': maxDataPoints = 90; break; default: maxDataPoints = 100; } - // Smart sampling - only sample if we have significantly more data const sampledMetrics = filteredMetrics.length > maxDataPoints * 1.2 ? filteredMetrics.filter((_, index) => index % Math.ceil(filteredMetrics.length / maxDataPoints) === 0) : filteredMetrics; -// console.log('formatChartData: After sampling:', sampledMetrics.length, 'metrics for display'); + console.log('📊 formatChartData: After sampling:', sampledMetrics.length, 'metrics for display'); - // Batch process the data transformation for better performance - return sampledMetrics.map((metric) => { + const formattedData = sampledMetrics.map((metric) => { const cpuUsage = typeof metric.cpu_usage === 'string' ? parseFloat(metric.cpu_usage.replace('%', '')) : parseFloat(metric.cpu_usage) || 0; @@ -210,4 +268,7 @@ export const formatChartData = (metrics: any[], timeRange: TimeRange) => { networkTxSpeed: Math.round(networkTxSpeed * 100) / 100, }; }); + + console.log('✅ formatChartData: Final formatted data count:', formattedData.length); + return formattedData; }; \ No newline at end of file diff --git a/application/src/pages/InstanceMonitoring.tsx b/application/src/pages/InstanceMonitoring.tsx index 6d9f7f0..c8dc476 100644 --- a/application/src/pages/InstanceMonitoring.tsx +++ b/application/src/pages/InstanceMonitoring.tsx @@ -98,9 +98,9 @@ const InstanceMonitoring = () => { toggleSidebar={toggleSidebar} />
-
+
{/* Header Section */} -
+

@@ -118,12 +118,12 @@ const InstanceMonitoring = () => {

{/* Stats Cards Section */} -
+
- {/* Server Table Section - This will take remaining space */} -
+ {/* Server Table Section */} +
diff --git a/application/src/pages/ServerDetail.tsx b/application/src/pages/ServerDetail.tsx index 3dee273..1a432b2 100644 --- a/application/src/pages/ServerDetail.tsx +++ b/application/src/pages/ServerDetail.tsx @@ -1,4 +1,3 @@ - import { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; @@ -10,8 +9,7 @@ import { Header } from "@/components/dashboard/Header"; import { serverService } from "@/services/serverService"; import { authService } from "@/services/authService"; import { Button } from "@/components/ui/button"; -import { ArrowLeft, Server, Database } from "lucide-react"; -import { ServerMetricsCharts } from "@/components/servers/ServerMetricsCharts"; +import { ArrowLeft, Server } from "lucide-react"; import { ServerMetricsOverview } from "@/components/servers/ServerMetricsOverview"; import { ServerHistoryCharts } from "@/components/servers/ServerHistoryCharts"; import { ServerSystemInfoCard } from "@/components/servers/ServerSystemInfoCard"; @@ -24,7 +22,7 @@ const ServerDetail = () => { const { sidebarCollapsed, toggleSidebar } = useSidebar(); const [currentUser, setCurrentUser] = useState(authService.getCurrentUser()); - //console.log('ServerDetail component loaded with serverId:', serverId); + console.log('ServerDetail component loaded with serverId:', serverId); const { data: server, @@ -36,6 +34,70 @@ const ServerDetail = () => { enabled: !!serverId }); + const getOSLogo = (server: any) => { + if (!server) return null; + + // Parse system_info if it's a string, but handle both JSON and plain text + let systemInfo: any = {}; + let systemInfoText = ''; + + if (server.system_info) { + if (typeof server.system_info === 'string') { + // Try to parse as JSON first + try { + systemInfo = JSON.parse(server.system_info); + } catch (error) { + // If JSON parsing fails, treat it as plain text + console.log('system_info is plain text:', server.system_info); + systemInfoText = server.system_info.toLowerCase(); + } + } else { + systemInfo = server.system_info; + } + } + + // Check system_info (both JSON and plain text), then fallback to os_type + const osFromJson = systemInfo.OSName || ''; + const osFromText = systemInfoText; + const osFromType = server.os_type || ''; + + // Combine all OS information for detection + const combinedOSInfo = `${osFromJson} ${osFromText} ${osFromType}`.toLowerCase(); + + console.log('OS detection info:', { osFromJson, osFromText, osFromType, combinedOSInfo }); + + // Check for specific OS distributions first (most specific to least specific) + if (combinedOSInfo.includes('ubuntu')) { + return '/upload/os/ubuntu.png'; + } else if (combinedOSInfo.includes('debian')) { + return '/upload/os/debian.png'; + } else if (combinedOSInfo.includes('centos')) { + return '/upload/os/centos.png'; + } else if (combinedOSInfo.includes('rhel') || combinedOSInfo.includes('red hat')) { + return '/upload/os/rhel.png'; + } else if (combinedOSInfo.includes('fedora')) { + return '/upload/os/fedora.png'; + } else if (combinedOSInfo.includes('suse') || combinedOSInfo.includes('opensuse')) { + return '/upload/os/suse.png'; + } else if (combinedOSInfo.includes('arch')) { + return '/upload/os/arch.png'; + } else if (combinedOSInfo.includes('alpine')) { + return '/upload/os/alpine.png'; + } else if (combinedOSInfo.includes('windows')) { + return '/upload/os/windows.png'; + } else if (combinedOSInfo.includes('macos') || combinedOSInfo.includes('darwin') || combinedOSInfo.includes('mac os')) { + return '/upload/os/macos.png'; + } else if (combinedOSInfo.includes('freebsd')) { + return '/upload/os/freebsd.png'; + } else if (combinedOSInfo.includes('linux') || combinedOSInfo.includes('gnu')) { + // Default to linux.png for any Linux-based system that doesn't match specific distributions + return '/upload/os/linux.png'; + } + + // Final fallback - if we can't determine the OS, default to linux.png + return '/upload/os/linux.png'; + }; + const handleLogout = () => { authService.logout(); navigate('/login'); @@ -46,7 +108,6 @@ const ServerDetail = () => { }; if (serverError) { - // console.error('Server detail error:', serverError); return (
@@ -101,7 +162,7 @@ const ServerDetail = () => { return (
-
+
{
-
- +
+ {server && getOSLogo(server) ? ( + OS Logo + ) : ( + + )}

{server?.name || 'Server Detail'}

-

+

Monitor server performance metrics and system health {server && ( - + {server.hostname} • {server.ip_address} • {server.os_type} - )}

- {/* System Info Card */} + + {/* System Info Card */} {server && (
@@ -159,17 +228,10 @@ const ServerDetail = () => {
)} - {/* Historical Charts Section */} - {server && ( -
- -
- )} - - {/* Metrics Charts Section */} + {/* Historical Charts Section - Single comprehensive section */} {server && (
- +
)}
diff --git a/application/src/services/serverService.ts b/application/src/services/serverService.ts index c808783..0f25142 100644 --- a/application/src/services/serverService.ts +++ b/application/src/services/serverService.ts @@ -7,7 +7,7 @@ export const serverService = { const records = await pb.collection('servers').getFullList(); return records; } catch (error) { - // console.error('Error fetching servers:', error); + console.error('Error fetching servers:', error); throw error; } }, @@ -17,190 +17,151 @@ export const serverService = { const record = await pb.collection('servers').getOne(serverId); return record; } catch (error) { - // console.error('Error fetching server:', error); + console.error('Error fetching server:', error); throw error; } }, async getServerMetrics(serverId: string, timeRange?: string): Promise { try { - // console.log('serverService.getServerMetrics: Fetching metrics for serverId:', serverId, 'timeRange:', timeRange); + console.log('🔍 serverService.getServerMetrics: Starting with serverId:', serverId, 'timeRange:', timeRange); // First, get the server to find the correct server_id for metrics let server; try { server = await this.getServer(serverId); - // console.log('serverService.getServerMetrics: Found server:', server); + console.log('✅ serverService.getServerMetrics: Found server:', { + id: server.id, + server_id: server.server_id, + name: server.name + }); } catch (error) { - // console.log('serverService.getServerMetrics: Could not fetch server details:', error); + console.log('❌ serverService.getServerMetrics: Could not fetch server details:', error); } - // Try multiple filter strategies to find data - let filter = ''; - let metricsServerId = serverId; - - // Strategy 1: Use server.server_id if available - if (server && server.server_id) { - metricsServerId = server.server_id; - filter = `server_id = "${metricsServerId}"`; - // console.log('serverService.getServerMetrics: Strategy 1 - Using server.server_id for metrics:', metricsServerId); - } else { - // Strategy 2: Use the serverId directly - filter = `server_id = "${serverId}"`; - // console.log('serverService.getServerMetrics: Strategy 2 - Using serverId directly for metrics:', serverId); - } - - // Add agent_id filter if available in server data - if (server && server.agent_id) { - filter += ` && agent_id = "${server.agent_id}"`; - // console.log('serverService.getServerMetrics: Added agent_id filter:', server.agent_id); - } - - // Add time range filter - if (timeRange) { - const now = new Date(); - let cutoffTime; - - switch (timeRange) { - case '60m': - cutoffTime = new Date(now.getTime() - (60 * 60 * 1000)); - break; - case '1d': - cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000)); - break; - case '7d': - cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000)); - break; - case '1m': - cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000)); - break; - case '3m': - cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000)); - break; - default: - cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000)); - } - - const cutoffISO = cutoffTime.toISOString(); - filter += ` && created >= "${cutoffISO}"`; - // console.log('serverService.getServerMetrics: Using time filter from:', cutoffISO, 'to now'); - } - - // console.log('serverService.getServerMetrics: Final filter:', filter); - - // Fetch filtered records with proper sorting - let records = await pb.collection('server_metrics').getFullList({ - filter: filter, + // Let's first check what data exists in the database for this server + console.log('🔍 Checking all records for this server...'); + const allServerRecords = await pb.collection('server_metrics').getFullList({ + filter: `server_id = "${serverId}" || server_id = "${server?.server_id}" || server_id = "${server?.id}"`, sort: '-created', requestKey: null }); - - // console.log('serverService.getServerMetrics: Found', records.length, 'records with primary filter'); - // If no records found with primary strategy, try fallback strategies - if (records.length === 0) { - // console.log('serverService.getServerMetrics: No records found, trying fallback strategies...'); - - // Fallback 1: Try without agent_id filter - let fallbackFilter = `server_id = "${metricsServerId}"`; - if (timeRange) { - const now = new Date(); - let cutoffTime; - - switch (timeRange) { - case '60m': - cutoffTime = new Date(now.getTime() - (60 * 60 * 1000)); - break; - case '1d': - cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000)); - break; - case '7d': - cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000)); - break; - case '1m': - cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000)); - break; - case '3m': - cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000)); - break; - default: - cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000)); - } - - const cutoffISO = cutoffTime.toISOString(); - fallbackFilter += ` && created >= "${cutoffISO}"`; - } - - // console.log('serverService.getServerMetrics: Trying fallback filter without agent_id:', fallbackFilter); - records = await pb.collection('server_metrics').getFullList({ - filter: fallbackFilter, - sort: '-created', - requestKey: null + console.log('📊 Found total records for server:', allServerRecords.length); + if (allServerRecords.length > 0) { + console.log('📅 Date range of all records:', { + newest: allServerRecords[0]?.created, + oldest: allServerRecords[allServerRecords.length - 1]?.created }); - // console.log('serverService.getServerMetrics: Fallback found', records.length, 'records'); - - // Fallback 2: Try with different server_id strategies - if (records.length === 0) { - const alternativeIds = [serverId, server?.server_id, server?.id].filter(Boolean); - // console.log('serverService.getServerMetrics: Trying alternative server IDs:', alternativeIds); + // Check last 5 records + console.log('🔄 Last 5 records timestamps:', allServerRecords.slice(0, 5).map(r => ({ + created: r.created, + age_minutes: Math.round((new Date().getTime() - new Date(r.created).getTime()) / (1000 * 60)) + }))); + } + + // Calculate time range for filtering + const now = new Date(); + let cutoffTime; + + if (timeRange === '60m') { + cutoffTime = new Date(now.getTime() - (60 * 60 * 1000)); // Exactly 60 minutes + console.log('⏰ 60m filter: Looking for records newer than:', cutoffTime.toISOString()); + console.log('⏰ Current time:', now.toISOString()); + console.log('⏰ Time difference in minutes:', Math.round((now.getTime() - cutoffTime.getTime()) / (1000 * 60))); + } else if (timeRange === '1d') { + cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000)); + } else if (timeRange === '7d') { + cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000)); + } else if (timeRange === '1m') { + cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000)); + } else if (timeRange === '3m') { + cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000)); + } else { + cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000)); + } + + // Try to get filtered records + const searchStrategies = [ + server?.server_id, + serverId, + server?.id + ].filter(Boolean); + + let filteredRecords: any[] = []; + + for (const strategy of searchStrategies) { + try { + const cutoffISO = cutoffTime.toISOString(); + const filter = `server_id = "${strategy}" && created >= "${cutoffISO}"`; - for (const altId of alternativeIds) { - if (altId && altId !== metricsServerId) { - let altFilter = `server_id = "${altId}"`; - if (timeRange) { - const now = new Date(); - let cutoffTime; - - switch (timeRange) { - case '60m': - cutoffTime = new Date(now.getTime() - (60 * 60 * 1000)); - break; - case '1d': - cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000)); - break; - case '7d': - cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000)); - break; - case '1m': - cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000)); - break; - case '3m': - cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000)); - break; - default: - cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000)); - } - - const cutoffISO = cutoffTime.toISOString(); - altFilter += ` && created >= "${cutoffISO}"`; - } + console.log(`🔍 Trying filter: ${filter}`); + + const records = await pb.collection('server_metrics').getFullList({ + filter: filter, + sort: '-created', + requestKey: null + }); + + console.log(`📊 Strategy "${strategy}" found ${records.length} records within time range`); + + if (records.length > 0) { + filteredRecords = records; + console.log('✅ Using records from strategy:', strategy); + break; + } + } catch (error) { + console.error(`❌ Error with strategy ${strategy}:`, error); + continue; + } + } + + // If no filtered records found and it's 60m, let's see what we have in a larger window + if (filteredRecords.length === 0 && timeRange === '60m') { + console.log('⚠️ No records found in 60m window, checking last 24 hours...'); + + const last24h = new Date(now.getTime() - (24 * 60 * 60 * 1000)); + + for (const strategy of searchStrategies) { + try { + const filter = `server_id = "${strategy}" && created >= "${last24h.toISOString()}"`; + + const records = await pb.collection('server_metrics').getFullList({ + filter: filter, + sort: '-created', + requestKey: null + }); + + console.log(`📊 Last 24h check for "${strategy}": ${records.length} records`); + + if (records.length > 0) { + console.log('📅 Sample record ages (minutes ago):', records.slice(0, 3).map(r => + Math.round((now.getTime() - new Date(r.created).getTime()) / (1000 * 60)) + )); - // console.log('serverService.getServerMetrics: Trying alternative ID filter:', altFilter); - const altRecords = await pb.collection('server_metrics').getFullList({ - filter: altFilter, - sort: '-created', - requestKey: null - }); - - if (altRecords.length > 0) { - // console.log('serverService.getServerMetrics: Alternative ID found', altRecords.length, 'records'); - records = altRecords; - break; - } + // Return all recent records for 60m if we have any + filteredRecords = records; + break; } + } catch (error) { + console.error(`❌ Error with 24h fallback for ${strategy}:`, error); + continue; } } } - // console.log('serverService.getServerMetrics: Final result:', records.length, 'records found'); - if (records.length > 0) { - // console.log('serverService.getServerMetrics: Sample record:', records[0]); + console.log('🎯 Final result:', filteredRecords.length, 'records found for', timeRange); + if (filteredRecords.length > 0) { + console.log('📅 Returned records age range (minutes ago):', { + newest: Math.round((now.getTime() - new Date(filteredRecords[0].created).getTime()) / (1000 * 60)), + oldest: Math.round((now.getTime() - new Date(filteredRecords[filteredRecords.length - 1].created).getTime()) / (1000 * 60)) + }); } - return records; + return filteredRecords; } catch (error) { - // console.error('Error fetching server metrics:', error); + console.error('❌ Error fetching server metrics:', error); throw error; } }, diff --git a/application/src/services/serverThresholdService.ts b/application/src/services/serverThresholdService.ts new file mode 100644 index 0000000..18a8162 --- /dev/null +++ b/application/src/services/serverThresholdService.ts @@ -0,0 +1,85 @@ + +import { pb } from "@/lib/pocketbase"; + +export interface ServerThreshold { + id: string; + name: string; + cpu_threshold: number; + ram_threshold: number; + disk_threshold: number; + network_threshold: number; + created: string; + updated: string; +} + +export interface CreateUpdateServerThresholdData { + name: string; + cpu_threshold: number; + ram_threshold: number; + disk_threshold: number; + network_threshold: number; +} + +export const serverThresholdService = { + async getServerThresholds(): Promise { + try { + console.log("Fetching server threshold templates"); + const response = await pb.collection('server_threshold_templates').getList(1, 50, { + sort: '-created', + }); + console.log("Server threshold templates response:", response); + return response.items as unknown as ServerThreshold[]; + } catch (error) { + console.error("Error fetching server threshold templates:", error); + throw error; + } + }, + + async getServerThreshold(id: string): Promise { + try { + console.log(`Fetching server threshold template with id: ${id}`); + const response = await pb.collection('server_threshold_templates').getOne(id); + console.log("Server threshold template response:", response); + return response as unknown as ServerThreshold; + } catch (error) { + console.error(`Error fetching server threshold template with id ${id}:`, error); + throw error; + } + }, + + async createServerThreshold(data: CreateUpdateServerThresholdData): Promise { + try { + console.log("Creating new server threshold template with data:", data); + const response = await pb.collection('server_threshold_templates').create(data); + console.log("Create server threshold template response:", response); + return response as unknown as ServerThreshold; + } catch (error) { + console.error("Error creating server threshold template:", error); + throw error; + } + }, + + async updateServerThreshold(id: string, data: Partial): Promise { + try { + console.log(`Updating server threshold template with id: ${id}`, data); + const response = await pb.collection('server_threshold_templates').update(id, data); + console.log("Update server threshold template response:", response); + return response as unknown as ServerThreshold; + } catch (error) { + console.error(`Error updating server threshold template with id ${id}:`, error); + throw error; + } + }, + + async deleteServerThreshold(id: string): Promise { + try { + console.log(`Deleting server threshold template with id: ${id}`); + await pb.collection('server_threshold_templates').delete(id); + console.log("Server threshold template deleted successfully"); + return true; + } catch (error) { + console.error(`Error deleting server threshold template with id ${id}:`, error); + throw error; + } + } +}; \ No newline at end of file diff --git a/application/src/services/templateService.ts b/application/src/services/templateService.ts index 85d5536..fc68e6c 100644 --- a/application/src/services/templateService.ts +++ b/application/src/services/templateService.ts @@ -1,4 +1,5 @@ + import { pb } from "@/lib/pocketbase"; export interface NotificationTemplate { @@ -37,62 +38,62 @@ export interface CreateUpdateTemplateData { export const templateService = { async getTemplates(): Promise { try { - console.log("Fetching notification templates"); - const response = await pb.collection('notification_templates').getList(1, 50, { + console.log("Fetching server notification templates"); + const response = await pb.collection('server_notification_templates').getList(1, 50, { sort: '-created', }); - console.log("Templates response:", response); + console.log("Server templates response:", response); return response.items as unknown as NotificationTemplate[]; } catch (error) { - console.error("Error fetching templates:", error); + console.error("Error fetching server templates:", error); throw error; } }, async getTemplate(id: string): Promise { try { - console.log(`Fetching template with id: ${id}`); - const response = await pb.collection('notification_templates').getOne(id); - console.log("Template response:", response); + console.log(`Fetching server template with id: ${id}`); + const response = await pb.collection('server_notification_templates').getOne(id); + console.log("Server template response:", response); return response as unknown as NotificationTemplate; } catch (error) { - console.error(`Error fetching template with id ${id}:`, error); + console.error(`Error fetching server template with id ${id}:`, error); throw error; } }, async createTemplate(data: CreateUpdateTemplateData): Promise { try { - console.log("Creating new template with data:", data); - const response = await pb.collection('notification_templates').create(data); - console.log("Create template response:", response); + console.log("Creating new server template with data:", data); + const response = await pb.collection('server_notification_templates').create(data); + console.log("Create server template response:", response); return response as unknown as NotificationTemplate; } catch (error) { - console.error("Error creating template:", error); + console.error("Error creating server template:", error); throw error; } }, async updateTemplate(id: string, data: Partial): Promise { try { - console.log(`Updating template with id: ${id}`, data); - const response = await pb.collection('notification_templates').update(id, data); - console.log("Update template response:", response); + console.log(`Updating server template with id: ${id}`, data); + const response = await pb.collection('server_notification_templates').update(id, data); + console.log("Update server template response:", response); return response as unknown as NotificationTemplate; } catch (error) { - console.error(`Error updating template with id ${id}:`, error); + console.error(`Error updating server template with id ${id}:`, error); throw error; } }, async deleteTemplate(id: string): Promise { try { - console.log(`Deleting template with id: ${id}`); - await pb.collection('notification_templates').delete(id); - console.log("Template deleted successfully"); + console.log(`Deleting server template with id: ${id}`); + await pb.collection('server_notification_templates').delete(id); + console.log("Server template deleted successfully"); return true; } catch (error) { - console.error(`Error deleting template with id ${id}:`, error); + console.error(`Error deleting server template with id ${id}:`, error); throw error; } } diff --git a/application/src/types/server.types.ts b/application/src/types/server.types.ts index c346d01..90e0f2b 100644 --- a/application/src/types/server.types.ts +++ b/application/src/types/server.types.ts @@ -19,6 +19,7 @@ export interface Server { last_checked: string; server_token: string; template_id: string; + threshold_id: string; notification_id: string; timestamp: string; connection: string;