diff --git a/.github/ISSUE_TEMPLATE/ask_for_help.yml b/.github/ISSUE_TEMPLATE/ask_for_help.yml index 0b058a2..54e72f7 100644 --- a/.github/ISSUE_TEMPLATE/ask_for_help.yml +++ b/.github/ISSUE_TEMPLATE/ask_for_help.yml @@ -1,4 +1,4 @@ -name: Ask for Help +name: ❓ Ask for Help description: Ask a question or request guidance about this project. title: "[Question]: " labels: [question] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ae7046d..bb41a83 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,4 +1,4 @@ -name: Bug Report +name: 🐛 Bug Report description: Report a reproducible bug to help us improve. title: "[Bug]: " labels: [bug] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ec4bb38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml index e4c64a7..735acc2 100644 --- a/.github/ISSUE_TEMPLATE/documentation.yml +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -1,4 +1,4 @@ -name: Documentation Improvement +name: 📝 Documentation Improvement description: Suggest improvements or report issues in documentation. title: "[Docs]: " labels: [documentation] diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 87cd7ca..90423a9 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,4 +1,4 @@ -name: Feature Request +name: 🚀 Feature Request description: Suggest an idea to improve this project. title: "[Feature]: " labels: [enhancement] diff --git a/.github/ISSUE_TEMPLATE/security_issue.yml b/.github/ISSUE_TEMPLATE/security_issue.yml index ee44028..31e140c 100644 --- a/.github/ISSUE_TEMPLATE/security_issue.yml +++ b/.github/ISSUE_TEMPLATE/security_issue.yml @@ -1,4 +1,4 @@ -name: Security Issue +name: 🛡️ Security Issue description: Report a potential security vulnerability. title: "[Security]: " labels: [security] diff --git a/.github/ISSUE_TEMPLATE/translation_request.yml b/.github/ISSUE_TEMPLATE/translation_request.yml index 5e2b226..a156fca 100644 --- a/.github/ISSUE_TEMPLATE/translation_request.yml +++ b/.github/ISSUE_TEMPLATE/translation_request.yml @@ -1,4 +1,4 @@ -name: Translation Request +name: 🌎 Translation Request description: Request a translation or report translation issues. title: "[Translation]: " labels: [translation] diff --git a/application/public/favicon.ico b/application/public/favicon.ico index 7ae5c72..d5dfc62 100644 Binary files a/application/public/favicon.ico and b/application/public/favicon.ico differ diff --git a/application/public/favicon_sidebar.ico b/application/public/favicon_sidebar.ico new file mode 100644 index 0000000..074b8f9 Binary files /dev/null and b/application/public/favicon_sidebar.ico differ diff --git a/application/public/upload/os/centos.png b/application/public/upload/os/centos.png new file mode 100644 index 0000000..a946ebc Binary files /dev/null and b/application/public/upload/os/centos.png differ diff --git a/application/public/upload/os/debian.png b/application/public/upload/os/debian.png new file mode 100644 index 0000000..9b025dc Binary files /dev/null and b/application/public/upload/os/debian.png differ diff --git a/application/public/upload/os/linux.png b/application/public/upload/os/linux.png new file mode 100644 index 0000000..8a69099 Binary files /dev/null and b/application/public/upload/os/linux.png differ diff --git a/application/public/upload/os/rhel.png b/application/public/upload/os/rhel.png new file mode 100644 index 0000000..5fd5e8b Binary files /dev/null and b/application/public/upload/os/rhel.png differ diff --git a/application/public/upload/os/ubuntu.png b/application/public/upload/os/ubuntu.png new file mode 100644 index 0000000..fe086e2 Binary files /dev/null and b/application/public/upload/os/ubuntu.png differ diff --git a/application/public/upload/os/windows.png b/application/public/upload/os/windows.png new file mode 100644 index 0000000..cefd4ae Binary files /dev/null and b/application/public/upload/os/windows.png differ diff --git a/application/src/api/realtime/index.ts b/application/src/api/realtime/index.ts index 3a53ee0..e114a68 100644 --- a/application/src/api/realtime/index.ts +++ b/application/src/api/realtime/index.ts @@ -2,7 +2,7 @@ // This file handles realtime notifications in a client-side environment // In a production app, this would be a server-side endpoint -console.log("API Realtime endpoint loaded"); +//console.log("API Realtime endpoint loaded"); // Simple implementation that simulates sending notifications export default async function handler(req) { 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/dashboard/sidebar/SidebarHeader.tsx b/application/src/components/dashboard/sidebar/SidebarHeader.tsx index 4ba177c..0ebbe2b 100644 --- a/application/src/components/dashboard/sidebar/SidebarHeader.tsx +++ b/application/src/components/dashboard/sidebar/SidebarHeader.tsx @@ -13,7 +13,7 @@ export const SidebarHeader: React.FC = ({ collapsed }) => {
CheckCle 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/schedule-incident/common/OverviewCard.tsx b/application/src/components/schedule-incident/common/OverviewCard.tsx index 6c96010..ccbeb45 100644 --- a/application/src/components/schedule-incident/common/OverviewCard.tsx +++ b/application/src/components/schedule-incident/common/OverviewCard.tsx @@ -18,6 +18,7 @@ interface OverviewCardProps { valueClassName?: string; isLoading?: boolean; color?: string; + gradient?: string; } export const OverviewCard = ({ @@ -30,11 +31,15 @@ export const OverviewCard = ({ valueClassName, isLoading = false, color = "blue", + gradient, }: OverviewCardProps) => { const { theme } = useTheme(); // Map color prop to gradient colors const getGradientBackground = () => { + if (gradient) { + return gradient; + } const colors = { blue: theme === 'dark' ? "linear-gradient(135deg, rgba(25, 118, 210, 0.8) 0%, rgba(66, 165, 245, 0.6) 100%)" diff --git a/application/src/components/schedule-incident/incident-management/OverviewCards.tsx b/application/src/components/schedule-incident/incident-management/OverviewCards.tsx index 454c21d..73566b9 100644 --- a/application/src/components/schedule-incident/incident-management/OverviewCards.tsx +++ b/application/src/components/schedule-incident/incident-management/OverviewCards.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { AlertCircle, CheckCircle, Clock, AlertTriangle, Flag } from 'lucide-react'; import { useLanguage } from '@/contexts/LanguageContext'; import { OverviewCard } from '../common/OverviewCard'; +import { useTheme } from '@/contexts/ThemeContext'; interface OverviewStatsProps { unresolved: number; @@ -24,6 +25,7 @@ export const OverviewCards: React.FC = ({ initialized }) => { const { t } = useLanguage(); + const { theme } = useTheme(); return (
@@ -32,35 +34,55 @@ export const OverviewCards: React.FC = ({ value={overviewStats.unresolved.toString()} icon={} isLoading={loading && initialized} - color="red" + gradient={ + theme === "dark" + ? "linear-gradient(135deg, #4b3b37 0%, rgba(239, 83, 80, 0.6) 100%)" + : "linear-gradient(135deg, #4b3b37 0%, rgba(239, 83, 80, 0.6) 100%)" + } /> } isLoading={loading && initialized} - color="amber" + gradient={ + theme === "dark" + ? "linear-gradient(135deg, #4b3b37 0%, rgba(255, 183, 77, 0.6) 100%)" + : "linear-gradient(135deg, #4b3b37 0%, rgba(255, 183, 77, 0.6) 100%)" + } /> } isLoading={loading && initialized} - color="orange" + gradient={ + theme === "dark" + ? "linear-gradient(135deg, #4b3b37 0%, rgba(255, 109, 0, 0.6) 100%)" + : "linear-gradient(135deg, #4b3b37 0%, rgba(255, 109, 0, 0.6) 100%)" + } /> } isLoading={loading && initialized} - color="green" + gradient={ + theme === "dark" + ? "linear-gradient(135deg, #4b3b37 0%, rgba(102, 187, 106, 0.6) 100%)" + : "linear-gradient(135deg, #4b3b37 0%, rgba(102, 187, 106, 0.6) 100%)" + } /> } isLoading={loading && initialized} - color="blue" + gradient={ + theme === "dark" + ? "linear-gradient(135deg, #4b3b37 0%, rgba(66, 165, 245, 0.6) 100%)" + : "linear-gradient(135deg, #4b3b37 0%, rgba(66, 165, 245, 0.6) 100%)" + } />
); diff --git a/application/src/components/servers/AddServerAgentDialog.tsx b/application/src/components/servers/AddServerAgentDialog.tsx new file mode 100644 index 0000000..755bb3d --- /dev/null +++ b/application/src/components/servers/AddServerAgentDialog.tsx @@ -0,0 +1,167 @@ + +import React, { useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useToast } from "@/hooks/use-toast"; +import { Server } from "lucide-react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { getCurrentEndpoint } from "@/lib/pocketbase"; +import { ServerAgentConfigForm } from "./ServerAgentConfigForm"; +import { OneClickInstallTab } from "./OneClickInstallTab"; +import { ManualInstallTab } from "./ManualInstallTab"; + +interface AddServerAgentDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onAgentAdded: () => void; +} + +export const AddServerAgentDialog: React.FC = ({ + open, + onOpenChange, + onAgentAdded, +}) => { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [activeTab, setActiveTab] = useState("configure"); + + // Get current PocketBase URL + const currentPocketBaseUrl = getCurrentEndpoint(); + + // Form state + const [formData, setFormData] = useState({ + serverName: "", + description: "", + osType: "", + checkInterval: "60", + retryAttempt: "3", + dockerEnabled: false, + notificationEnabled: true, + }); + + // Generated server token and agent ID + const [serverToken] = useState(() => + `srv_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}` + ); + + const [serverId] = useState(() => + `agent_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}` + ); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (isSubmitting) return; + + if (!formData.serverName || !formData.osType) { + toast({ + title: "Validation Error", + description: "Please fill in all required fields.", + variant: "destructive", + }); + return; + } + + setIsSubmitting(true); + + try { + // Here you would typically create the server monitoring configuration + // For now, we'll simulate the process + await new Promise(resolve => setTimeout(resolve, 1500)); + + toast({ + title: "Server Agent Created", + description: `${formData.serverName} monitoring agent has been configured successfully.`, + }); + + // Switch to one-click install tab after successful creation + setActiveTab("one-click"); + onAgentAdded(); + } catch (error) { + toast({ + title: "Error", + description: "Failed to create server monitoring agent.", + variant: "destructive", + }); + } finally { + setIsSubmitting(false); + } + }; + + const handleDialogClose = () => { + // Reset form and tab when dialog closes + setActiveTab("configure"); + setFormData({ + serverName: "", + description: "", + osType: "", + checkInterval: "60", + retryAttempt: "3", + dockerEnabled: false, + notificationEnabled: true, + }); + onOpenChange(false); + }; + + return ( + + + + + + Add Server Monitoring Agent + + + Configure a new server monitoring agent to track system metrics and performance. + + + + + + Configure Agent + One-Click Install + Manual Installation + + + + + + + + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/application/src/components/servers/EditServerDialog.tsx b/application/src/components/servers/EditServerDialog.tsx new file mode 100644 index 0000000..cce5755 --- /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/ManualInstallTab.tsx b/application/src/components/servers/ManualInstallTab.tsx new file mode 100644 index 0000000..9d289a8 --- /dev/null +++ b/application/src/components/servers/ManualInstallTab.tsx @@ -0,0 +1,125 @@ + +import React from "react"; +import { Button } from "@/components/ui/button"; +import { Copy, Terminal } from "lucide-react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { copyToClipboard } from "@/utils/copyUtils"; + +interface ManualInstallTabProps { + serverToken: string; + currentPocketBaseUrl: string; + formData: { + serverName: string; + osType: string; + checkInterval: string; + }; + serverId: string; + onDialogClose: () => void; +} + +export const ManualInstallTab: React.FC = ({ + serverToken, + currentPocketBaseUrl, + formData, + serverId, + onDialogClose, +}) => { + const getManualInstallSteps = () => { + const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh"; + + return [ + { + title: "Download the installation script", + command: `curl -L -o server-agent.sh "${scriptUrl}"` + }, + { + title: "Make the script executable", + command: `chmod +x server-agent.sh` + }, + { + title: "Run the installation with your configuration", + command: `SERVER_TOKEN="${serverToken}" POCKETBASE_URL="${currentPocketBaseUrl}" SERVER_NAME="${formData.serverName}" AGENT_ID="${serverId}" sudo bash server-agent.sh` + } + ]; + }; + + return ( + + + + + Manual Installation Steps + + + Step-by-step installation process + + + +
+
+
+ Server Name: {formData.serverName} +
+
+ Agent ID: {serverId} +
+
+ OS Type: {formData.osType} +
+
+ Check Interval: {formData.checkInterval}s +
+
+
+ +
+ {getManualInstallSteps().map((step, index) => ( +
+
+ + {index + 1} + + {step.title} +
+
+
+                  {step.command}
+                
+ +
+
+ ))} +
+ +
+

Prerequisites:

+
    +
  • Ensure you have root/sudo access on the target server
  • +
  • Make sure curl is installed for downloading files
  • +
  • Internet connection required for downloading script
  • +
+ +

After Installation:

+

+ The agent will start automatically and appear in your dashboard within a few minutes. +

+
+ +
+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/servers/OSSelector.tsx b/application/src/components/servers/OSSelector.tsx new file mode 100644 index 0000000..6147997 --- /dev/null +++ b/application/src/components/servers/OSSelector.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { Check } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface OSOption { + value: string; + name: string; + logo: string; // now it's a path to image, e.g., /logos/ubuntu.svg +} + +const osOptions: OSOption[] = [ + { value: "ubuntu", name: "Ubuntu", logo: "/upload/os/ubuntu.png" }, + { value: "debian", name: "Debian", logo: "/upload/os/debian.png" }, + { value: "centos", name: "CentOS", logo: "/upload/os/centos.png" }, + { value: "rhel", name: "Red Hat Enterprise Linux", logo: "/upload/os/rhel.png" }, + { value: "linux", name: "Linux (Generic)", logo: "/upload/os/linux.png" }, + { value: "windows", name: "Windows Server", logo: "/upload/os/windows.png" }, +]; + +interface OSSelectorProps { + value: string; + onValueChange: (value: string) => void; +} + +export const OSSelector: React.FC = ({ value, onValueChange }) => { + return ( +
+ {osOptions.map((os) => ( + + ))} +
+ ); +}; \ No newline at end of file diff --git a/application/src/components/servers/OneClickInstallTab.tsx b/application/src/components/servers/OneClickInstallTab.tsx new file mode 100644 index 0000000..1067d20 --- /dev/null +++ b/application/src/components/servers/OneClickInstallTab.tsx @@ -0,0 +1,101 @@ + +import React from "react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Copy, Download } from "lucide-react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { copyToClipboard } from "@/utils/copyUtils"; + +interface OneClickInstallTabProps { + serverToken: string; + currentPocketBaseUrl: string; + formData: { + serverName: string; + osType: string; + checkInterval: string; + retryAttempt: string; + }; + serverId: string; + onDialogClose: () => void; +} + +export const OneClickInstallTab: React.FC = ({ + serverToken, + currentPocketBaseUrl, + formData, + serverId, + onDialogClose, +}) => { + const getOneClickInstallCommand = () => { + 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 +SERVER_TOKEN="${serverToken}" \\ +POCKETBASE_URL="${currentPocketBaseUrl}" \\ +SERVER_NAME="${formData.serverName}" \\ +AGENT_ID="${serverId}" \\ +OS_TYPE="${formData.osType}" \\ +CHECK_INTERVAL="${formData.checkInterval}" \\ +RETRY_ATTEMPTS="${formData.retryAttempt}" \\ +sudo -E bash ./server-agent.sh`; + }; + + const handleCopyCommand = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + console.log('Copy button clicked'); // Debug log + const command = getOneClickInstallCommand(); + console.log('Copying command:', command); // Debug log + await copyToClipboard(command); + }; + + return ( + + + + + One-Click Install + + + Copy and paste this single command to install the monitoring agent instantly + + + +
+ +
+
+              {getOneClickInstallCommand()}
+            
+ +
+
+ +
+

Simply run this command on your server:

+
    +
  1. SSH into your target server
  2. +
  3. Paste and run the command above
  4. +
  5. The agent will be installed and started automatically
  6. +
+
+ +
+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/servers/ServerAgentConfigForm.tsx b/application/src/components/servers/ServerAgentConfigForm.tsx new file mode 100644 index 0000000..da2e858 --- /dev/null +++ b/application/src/components/servers/ServerAgentConfigForm.tsx @@ -0,0 +1,172 @@ + +import React from "react"; +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 { Copy } from "lucide-react"; +import { copyToClipboard } from "@/utils/copyUtils"; +import { OSSelector } from "./OSSelector"; + +interface ServerAgentConfigFormProps { + formData: { + serverName: string; + description: string; + osType: string; + checkInterval: string; + retryAttempt: string; + dockerEnabled: boolean; + notificationEnabled: boolean; + }; + setFormData: React.Dispatch>; + serverId: string; + serverToken: string; + currentPocketBaseUrl: string; + isSubmitting: boolean; + onSubmit: (e: React.FormEvent) => void; +} + +export const ServerAgentConfigForm: React.FC = ({ + formData, + setFormData, + serverId, + serverToken, + currentPocketBaseUrl, + isSubmitting, + onSubmit, +}) => { + return ( +
+
+
+ + setFormData(prev => ({ ...prev, serverName: e.target.value }))} + required + /> +

What is the name or label used as the identifier

+
+ +
+ +
+ + +
+

Auto-generated unique identifier

+
+
+ +
+
+ + setFormData(prev => ({ ...prev, osType: value }))} + /> +
+ +
+
+ + +

How often to check the server and metric status

+
+ +
+ + +

Number of retry attempts before marking as down

+
+ +
+ +
+ + +
+

Auto-generated authentication token

+
+ +
+ +
+ + +
+

Current system API URL

+
+
+
+ +
+ +
+ + ); +}; \ No newline at end of file diff --git a/application/src/components/servers/ServerHistoryCharts.tsx b/application/src/components/servers/ServerHistoryCharts.tsx index 274f966..63e313d 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) => {
-
+
@@ -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/ServerMetricsCharts.tsx b/application/src/components/servers/ServerMetricsCharts.tsx index a3fa10c..1a60497 100644 --- a/application/src/components/servers/ServerMetricsCharts.tsx +++ b/application/src/components/servers/ServerMetricsCharts.tsx @@ -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); diff --git a/application/src/components/servers/ServerTable.tsx b/application/src/components/servers/ServerTable.tsx index c73172f..89c948f 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"; @@ -6,13 +5,17 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; -import { Progress } from "@/components/ui/progress"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Trash2 } from "lucide-react"; 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[]; @@ -21,9 +24,15 @@ interface ServerTableProps { } export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => { + const { theme } = useTheme(); const [searchTerm, setSearchTerm] = useState(""); - const [pausedServers, setPausedServers] = useState>(new Set()); + 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()); const navigate = useNavigate(); + const { toast } = useToast(); const filteredServers = servers.filter(server => server.name.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -39,38 +48,165 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) navigate(`/container-monitoring/${serverId}`); }; - const handlePauseResume = (serverId: string) => { - const isPaused = pausedServers.has(serverId); - if (isPaused) { - setPausedServers(prev => { + const handlePauseResume = async (server: Server) => { + const serverId = server.id; + const isPaused = server.status === "paused"; + + if (pausingServers.has(serverId)) { + return; // Already processing this server + } + + try { + setPausingServers(prev => new Set(prev).add(serverId)); + + // Only update the status field, preserving all other server configuration + const updateData = { + status: isPaused ? "up" : "paused", + last_checked: new Date().toISOString() + }; + + await pb.collection('servers').update(serverId, updateData); + + toast({ + title: isPaused ? "Server resumed" : "Server paused", + description: `Monitoring ${isPaused ? 'resumed' : 'paused'} for ${server.name}`, + }); + + // console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`); + + // Refresh the server list to show updated status + onRefresh(); + + } catch (error) { + // console.error('Error updating server status:', error); + toast({ + variant: "destructive", + title: "Error", + description: `Failed to ${isPaused ? 'resume' : 'pause'} server monitoring. Please try again.`, + }); + } finally { + setPausingServers(prev => { const newSet = new Set(prev); newSet.delete(serverId); return newSet; }); - // console.log('Resume server monitoring:', serverId); - } else { - setPausedServers(prev => new Set(prev).add(serverId)); - // console.log('Pause server monitoring:', serverId); } }; - const handleEdit = (serverId: string) => { - // TODO: Implement edit functionality - // console.log('Edit server:', serverId); + const handleEdit = (server: Server) => { + setSelectedServer(server); + setEditDialogOpen(true); }; - const handleDelete = (serverId: string) => { - // TODO: Implement delete functionality - // console.log('Delete server:', serverId); + const handleDelete = (server: Server) => { + setSelectedServer(server); + setDeleteDialogOpen(true); + }; + + const confirmDelete = async () => { + if (!selectedServer || isDeleting) return; + + try { + setIsDeleting(true); + + // Delete the server from the database + await pb.collection('servers').delete(selectedServer.id); + + toast({ + title: "Server deleted", + description: `${selectedServer.name} has been deleted successfully.`, + }); + + // Refresh the server list + onRefresh(); + + // Close the dialog + setDeleteDialogOpen(false); + setSelectedServer(null); + + } catch (error) { + // console.error('Error deleting server:', error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to delete server. Please try again.", + }); + } finally { + setIsDeleting(false); + } + }; + + const CustomProgressBar = ({ + value, + label, + subtitle, + type + }: { + value: number; + label: string; + subtitle: string; + type: 'cpu' | 'memory' | 'disk' + }) => { + const getGradientColors = (type: string, value: number) => { + if (type === 'cpu') { + if (value > 90) return 'from-red-500 to-red-600'; + if (value > 75) return 'from-orange-500 to-orange-600'; + if (value > 60) return 'from-yellow-500 to-yellow-600'; + return 'from-green-500 to-green-600'; + } + if (type === 'memory') { + if (value > 90) return 'from-red-500 to-red-600'; + if (value > 75) return 'from-yellow-500 to-yellow-600'; + return 'from-blue-500 to-blue-600'; + } + if (type === 'disk') { + if (value > 95) return 'from-red-500 to-red-600'; + if (value > 85) return 'from-yellow-500 to-yellow-600'; + return 'from-orange-500 to-orange-600'; + } + return 'from-gray-500 to-gray-600'; + }; + + const getTextColor = (value: number) => { + if (value > 90) return 'text-red-600 dark:text-red-400'; + if (value > 75) return 'text-orange-600 dark:text-orange-400'; + if (value > 60) return 'text-yellow-600 dark:text-yellow-400'; + return 'text-green-600 dark:text-green-400'; + }; + + return ( +
+
+ + {label} + + + {subtitle} + +
+
+
+
+
+
+
+
+ +
+
+ ); }; if (isLoading) { return ( - - + + Servers - +
Loading servers... @@ -81,189 +217,214 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) } return ( - - -
- Servers -
-
- - setSearchTerm(e.target.value)} - className="pl-8" - /> + <> + + +
+ Servers +
+
+ + setSearchTerm(e.target.value)} + className="pl-8" + /> +
+
- -
-
- - - {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 = pausedServers.has(server.id); + + + {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); - return ( - - -
-
{server.name}
- -
-
- - - - -
- - {server.os_type} -
-
- - {server.ip_address} - - -
-
- {cpuUsage.toFixed(1)}% - {server.cpu_cores} cores + return ( + + +
+ {server.name}
- 90 ? "bg-red-00" : - cpuUsage > 75 ? "bg-orange-500" : - cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500" - } - /> -
- - -
-
- {memoryUsage.toFixed(1)}% - {serverService.formatBytes(server.ram_total)} + + + + + +
+ + + {server.os_type} +
- 90 ? "bg-red-500" : - memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500" - } +
+ + + {server.ip_address} + + + + -
- - -
-
- {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 + + +
+ {server.uptime} +
+
+ +
+ {new Date(server.last_checked).toLocaleString()} +
+
+ + + + + + + handleViewDetails(server.id)}> + + View Server Detail - )} - - handlePauseResume(server.id)}> - {isPaused ? ( - <> - - Resume Monitoring - - ) : ( - <> - - Pause Monitoring - + {server.docker === 'true' && ( + handleViewContainers(server.id)}> + + Container Monitoring + )} - - - handleEdit(server.id)}> - - Edit Server - - handleDelete(server.id)} - 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 */} + + + + Are you sure you want to delete this server? + + This action cannot be undone. This will permanently delete{' '} + + {selectedServer?.name} + {' '} + and all of its monitoring data. + + + + + Cancel + + + {isDeleting ? "Deleting..." : "Delete"} + + + + + ); }; \ No newline at end of file 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..4f3355e 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/components/servers/charts/hooks/useServerHistoryData.ts b/application/src/components/servers/charts/hooks/useServerHistoryData.ts index d08a437..04e0365 100644 --- a/application/src/components/servers/charts/hooks/useServerHistoryData.ts +++ b/application/src/components/servers/charts/hooks/useServerHistoryData.ts @@ -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"; @@ -7,7 +7,7 @@ import { formatChartData } from "../dataUtils"; type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m'; export const useServerHistoryData = (serverId: string) => { - const [timeRange, setTimeRange] = useState("1d"); + const [timeRange, setTimeRange] = useState("60m"); const { data: metrics = [], @@ -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, diff --git a/application/src/components/services/HeatmapChart.tsx b/application/src/components/services/HeatmapChart.tsx new file mode 100644 index 0000000..f0b5539 --- /dev/null +++ b/application/src/components/services/HeatmapChart.tsx @@ -0,0 +1,174 @@ + +import { UptimeData } from "@/types/service.types"; +import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay } from "date-fns"; + +interface HeatmapChartProps { + uptimeData: UptimeData[]; + selectedMonth: Date; +} + +export const HeatmapChart = ({ uptimeData, selectedMonth }: HeatmapChartProps) => { + const monthStart = startOfMonth(selectedMonth); + const monthEnd = endOfMonth(selectedMonth); + const daysInMonth = eachDayOfInterval({ start: monthStart, end: monthEnd }); + + const getStatusForDay = (day: Date) => { + const dayData = uptimeData.filter(data => + isSameDay(new Date(data.timestamp), day) + ); + + if (dayData.length === 0) return 'no-data'; + + // Calculate the predominant status for the day + const statusCounts = dayData.reduce((acc, data) => { + acc[data.status] = (acc[data.status] || 0) + 1; + return acc; + }, {} as Record); + + const predominantStatus = Object.entries(statusCounts) + .sort(([,a], [,b]) => b - a)[0][0]; + + return predominantStatus; + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'up': + return 'bg-emerald-500'; + case 'down': + return 'bg-red-500'; + case 'warning': + return 'bg-amber-500'; + case 'paused': + return 'bg-slate-500'; + default: + return 'bg-slate-700'; + } + }; + + const getStatusLabel = (status: string) => { + switch (status) { + case 'up': + return 'Up'; + case 'down': + return 'Down'; + case 'warning': + return 'Warning'; + case 'paused': + return 'Paused'; + default: + return 'No Data'; + } + }; + + // Group days by weeks + const weeks: Date[][] = []; + let currentWeek: Date[] = []; + + daysInMonth.forEach((day, index) => { + if (index === 0) { + // Fill empty days at the start of the month + const dayOfWeek = day.getDay(); + for (let i = 0; i < dayOfWeek; i++) { + currentWeek.push(new Date(0)); // Placeholder for empty cells + } + } + + currentWeek.push(day); + + if (currentWeek.length === 7) { + weeks.push([...currentWeek]); + currentWeek = []; + } + }); + + // Add remaining days to last week + if (currentWeek.length > 0) { + while (currentWeek.length < 7) { + currentWeek.push(new Date(0)); // Placeholder for empty cells + } + weeks.push(currentWeek); + } + + return ( +
+ {/* Header */} +
+

+ Service Health - {format(selectedMonth, 'MMMM yyyy')} +

+

+ Daily status overview for the current month +

+
+ + {/* Calendar Grid */} +
+ {/* Day labels */} +
+
Sun
+
Mon
+
Tue
+
Wed
+
Thu
+
Fri
+
Sat
+
+ + {/* Calendar days */} +
+ {weeks.map((week, weekIndex) => ( +
+ {week.map((day, dayIndex) => { + const isPlaceholder = day.getTime() === 0; + const status = isPlaceholder ? 'no-data' : getStatusForDay(day); + const dayData = isPlaceholder ? [] : uptimeData.filter(data => + isSameDay(new Date(data.timestamp), day) + ); + + return ( +
0 ? ` (${dayData.length} checks)` : ''}`} + > + {isPlaceholder ? '' : format(day, 'd')} +
+ ); + })} +
+ ))} +
+
+ + {/* Status Legend */} +
+ Status: +
+
+
+ Up +
+
+
+ Warning +
+
+
+ Down +
+
+
+ Paused +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/services/HeatmapDialog.tsx b/application/src/components/services/HeatmapDialog.tsx new file mode 100644 index 0000000..ef15c8b --- /dev/null +++ b/application/src/components/services/HeatmapDialog.tsx @@ -0,0 +1,97 @@ + +import { useState } from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { HeatmapChart } from "./HeatmapChart"; +import { UptimeData } from "@/types/service.types"; +import { addMonths, subMonths, format } from "date-fns"; + +interface HeatmapDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + serviceName: string; + uptimeData: UptimeData[]; +} + +export const HeatmapDialog = ({ + open, + onOpenChange, + serviceName, + uptimeData +}: HeatmapDialogProps) => { + const [selectedMonth, setSelectedMonth] = useState(new Date()); + + const handlePreviousMonth = () => { + setSelectedMonth(prev => subMonths(prev, 1)); + }; + + const handleNextMonth = () => { + setSelectedMonth(prev => addMonths(prev, 1)); + }; + + const handleCurrentMonth = () => { + setSelectedMonth(new Date()); + }; + + return ( + + + + + Health Heatmap - {serviceName} + + + Monthly overview of service health status with daily breakdown + + + +
+ {/* Month Navigation */} +
+ + + + + +
+ + {/* Heatmap Chart */} + +
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/services/ResponseTimeChart.tsx b/application/src/components/services/ResponseTimeChart.tsx index 51d04e2..3d70d59 100644 --- a/application/src/components/services/ResponseTimeChart.tsx +++ b/application/src/components/services/ResponseTimeChart.tsx @@ -1,27 +1,34 @@ -import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, LineChart, Line } from "recharts"; +import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, LineChart, Line, BarChart, Bar } from "recharts"; import { UptimeData } from "@/types/service.types"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { useTheme } from "@/contexts/ThemeContext"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { format } from "date-fns"; +import { Button } from "@/components/ui/button"; +import { AreaChart as AreaChartIcon, BarChart3, TrendingUp } from "lucide-react"; interface ResponseTimeChartProps { uptimeData: UptimeData[]; } +type ChartType = 'area' | 'line' | 'bar'; + export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { const { theme } = useTheme(); + const [chartType, setChartType] = useState('area'); - // Modern color palette for different chart lines with solid colors at 90-100% opacity - const modernColors = [ - { stroke: '#f59e0b', fill: 'rgba(111, 86, 63, 0.95)' }, // Yellow (changed from blue) - { stroke: '#10b981', fill: 'rgba(0, 84, 56, 0.95)' }, // Emerald - { stroke: '#3b82f6', fill: 'rgba(59, 130, 246, 0.95)' }, // Blue (moved to second position) - { stroke: '#ef4444', fill: 'rgba(239, 68, 68, 0.95)' }, // Red - { stroke: '#8b5cf6', fill: 'rgba(139, 92, 246, 0.95)' }, // Violet - { stroke: '#06b6d4', fill: 'rgba(6, 182, 212, 0.95)' }, // Cyan - { stroke: '#f97316', fill: 'rgba(231, 148, 89, 0.95)' }, // Orange - { stroke: '#84cc16', fill: 'rgba(132, 204, 22, 0.95)' }, // Lime + // Fixed color palette - consistent colors that don't change + const fixedColors = [ + { stroke: '#3b82f6', fill: 'rgba(59, 130, 246, 0.2)', name: 'Ocean Blue' }, + { stroke: '#10b981', fill: 'rgba(16, 185, 129, 0.2)', name: 'Emerald Green' }, + { stroke: '#f59e0b', fill: 'rgba(245, 158, 11, 0.2)', name: 'Golden Amber' }, + { stroke: '#ef4444', fill: 'rgba(239, 68, 68, 0.2)', name: 'Ruby Red' }, + { stroke: '#8b5cf6', fill: 'rgba(139, 92, 246, 0.2)', name: 'Royal Purple' }, + { stroke: '#06b6d4', fill: 'rgba(6, 182, 212, 0.2)', name: 'Sky Cyan' }, + { stroke: '#f97316', fill: 'rgba(249, 115, 22, 0.2)', name: 'Sunset Orange' }, + { stroke: '#84cc16', fill: 'rgba(132, 204, 22, 0.2)', name: 'Fresh Lime' }, + { stroke: '#ec4899', fill: 'rgba(236, 72, 153, 0.2)', name: 'Vibrant Pink' }, + { stroke: '#14b8a6', fill: 'rgba(20, 184, 166, 0.2)', name: 'Ocean Teal' }, ]; // Check if we have data from multiple sources @@ -160,7 +167,7 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { return [Math.max(0, minValue - padding), maxValue + padding]; }, [chartData, hasMultipleSources]); - // Get unique sources for legend with proper labeling and colors + // Get unique sources for legend with consistent fixed colors const sources = useMemo(() => { if (!hasMultipleSources) return []; @@ -175,25 +182,29 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { } }); - const regionalSources = Array.from(sourceSet).map((source, index) => { + // Sort sources to ensure consistent ordering + const sortedSources = Array.from(sourceSet).sort(); + + const regionalSources = sortedSources.map((source, index) => { const data = sourceInfo.get(source); const [regionName, agentId] = source.split('|'); // Create proper label with region and agent info let label = regionName; if (data?.agent_id && data.agent_id !== '1') { - label = `${regionName} (${data.agent_id})`; + label = `${regionName} (Agent ${data.agent_id})`; } else if (regionName === 'Default' && data?.agent_id === '1') { - label = `Default System Check (Agent 1)`; + label = `Default System Check`; } - const colorIndex = index % modernColors.length; + const colorIndex = index % fixedColors.length; return { key: `regional_${source.replace('|', '_')}`, label: label, - stroke: modernColors[colorIndex].stroke, - fill: modernColors[colorIndex].fill + stroke: fixedColors[colorIndex].stroke, + fill: fixedColors[colorIndex].fill, + colorName: fixedColors[colorIndex].name }; }); @@ -205,9 +216,10 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { const defaultSources = (hasPureDefault && !hasRegionalDefault) ? [{ key: 'default', - label: 'Default', - stroke: modernColors[regionalSources.length % modernColors.length].stroke, - fill: modernColors[regionalSources.length % modernColors.length].fill + label: 'Default System', + stroke: fixedColors[regionalSources.length % fixedColors.length].stroke, + fill: fixedColors[regionalSources.length % fixedColors.length].fill, + colorName: fixedColors[regionalSources.length % fixedColors.length].name }] : []; return [...defaultSources, ...regionalSources]; @@ -219,13 +231,13 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { const data = payload[0].payload; return ( -
-

{String(label)}

-

{String(data.date)}

+
+

{String(label)}

+

{String(data.date)}

{hasMultipleSources ? ( // Multi-source tooltip -
+
{sources.map(source => { const valueKey = source.key === 'default' ? 'defaultValue' : `${source.key}_value`; const statusKey = source.key === 'default' ? 'defaultStatus' : `${source.key}_status`; @@ -234,35 +246,45 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { if (value === undefined && status === undefined) return null; - let statusColor = "bg-gray-800"; + let statusBadgeClass = "px-2 py-1 rounded-full text-xs font-medium"; let statusText = "No Data"; if (status === "up") { - statusColor = "bg-emerald-800"; + statusBadgeClass += " bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"; statusText = "Up"; } else if (status === "down") { - statusColor = "bg-red-800"; + statusBadgeClass += " bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"; statusText = "Down"; } else if (status === "warning") { - statusColor = "bg-yellow-800"; + statusBadgeClass += " bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"; statusText = "Warning"; } else if (status === "paused") { - statusColor = "bg-gray-800"; + statusBadgeClass += " bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"; statusText = "Paused"; + } else { + statusBadgeClass += " bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"; } return ( -
-
+
+
- {String(source.label)} +
+ {String(source.label)} + {source.colorName} +
-
- {status === "paused" ? "Paused" : - value !== null && value !== undefined ? `${value} ms` : "No data"} +
+ + {statusText} + +
+ {status === "paused" ? "Paused" : + value !== null && value !== undefined ? `${value} ms` : "No data"} +
); @@ -271,19 +293,19 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { ) : ( // Single source tooltip - showing status with color <> -
-
+
- { + { data.status === "up" ? "Up" : data.status === "down" ? "Down" : data.status === "warning" ? "Warning" : "Paused" }
-

+

{data.status === "paused" ? "Monitoring paused" : data.value !== null ? `${data.value} ms` : "No data"}

@@ -295,6 +317,182 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { return null; }; + // Render different chart types + const renderChart = () => { + const commonProps = { + data: chartData, + margin: { top: 10, right: 30, left: 0, bottom: 30 } + }; + + const commonAxisProps = { + xAxis: { + dataKey: "time", + stroke: theme === 'dark' ? '#666' : '#9ca3af', + angle: -45, + textAnchor: "end" as const, + tick: { fontSize: 10 }, + height: 60, + interval: "preserveStartEnd" as const, + minTickGap: 5 + }, + yAxis: { + stroke: theme === 'dark' ? '#666' : '#9ca3af', + allowDecimals: false, + domain: yAxisDomain + } + }; + + if (hasMultipleSources) { + switch (chartType) { + case 'line': + return ( + + + + + } /> + + {sources.find(s => s.key === 'default') && ( + s.key === 'default')?.stroke} + strokeWidth={3} + dot={{ r: 4, strokeWidth: 2 }} + connectNulls={false} + /> + )} + + {sources.filter(s => s.key !== 'default').map((source) => ( + + ))} + + ); + + case 'bar': + return ( + + + + + } /> + + {sources.find(s => s.key === 'default') && ( + s.key === 'default')?.stroke} + opacity={0.8} + /> + )} + + {sources.filter(s => s.key !== 'default').map((source) => ( + + ))} + + ); + + default: // area + return ( + + + + + } /> + + {sources.find(s => s.key === 'default') && ( + s.key === 'default')?.stroke} + fill={sources.find(s => s.key === 'default')?.fill} + strokeWidth={3} + dot={false} + connectNulls={false} + /> + )} + + {sources.filter(s => s.key !== 'default').map((source) => ( + + ))} + + ); + } + } else { + // Single source charts remain the same + return ( + + + + + } /> + + + + + + + + {chartData.map((entry, index) => + entry.status === 'paused' ? ( + + ) : null + )} + + ); + } + }; + // Check if we have any data to display const hasData = uptimeData.length > 0; @@ -319,21 +517,55 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { Response Time History - {hasData && ( - - {dateRangeDisplay} - - )} +
+ {hasMultipleSources && ( +
+ + + +
+ )} + {hasData && ( + + {dateRangeDisplay} + + )} +
{hasMultipleSources && ( -
+
+
+ Monitoring Sources: +
{sources.map(source => ( -
+
- {String(source.label)} + {String(source.label)} + ({source.colorName})
))}
@@ -347,118 +579,7 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { ) : (
- {hasMultipleSources ? ( - - - - - } /> - - {/* Default monitoring area with modern solid background */} - {sources.find(s => s.key === 'default') && ( - s.key === 'default')?.stroke} - fill={sources.find(s => s.key === 'default')?.fill} - strokeWidth={2.5} - dot={false} - connectNulls={false} - /> - )} - - {/* Regional monitoring areas with modern solid backgrounds */} - {sources.filter(s => s.key !== 'default').map((source) => ( - - ))} - - ) : ( - // For single regional agent or default monitoring only - use solid background colors - - - - - } /> - - {/* Modern solid background areas for different statuses */} - - - - - - - {/* Reference lines for paused status */} - {chartData.map((entry, index) => - entry.status === 'paused' ? ( - - ) : null - )} - - )} + {renderChart()}
)} diff --git a/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx b/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx index 0bc9cc5..b4e6d82 100644 --- a/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx +++ b/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx @@ -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(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(); + } + if (subscriptionsRef.current.uptime) { + subscriptionsRef.current.uptime(); } - }; - } catch (error) { - // console.error("Error setting up real-time updates:", error); - } + } catch (error) { + // console.error("Error cleaning up subscriptions:", error); + } + }; }, [serviceId, startDate, endDate, setService, setUptimeData]); }; \ No newline at end of file diff --git a/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx b/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx index ccd6259..fa42951 100644 --- a/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx +++ b/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx @@ -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]); - // 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 { + // Memoize the service data fetching to prevent unnecessary re-runs + const fetchServiceData = useCallback(async () => { + try { + if (!serviceId) { setIsLoading(false); + return; } - }; - - fetchServiceData(); + + 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]); - // Update data when date range changes or when service is loaded + // Initial data loading + useEffect(() => { + fetchServiceData(); + }, [fetchServiceData]); + + // Update data when date range changes or when service is loaded - with debouncing useEffect(() => { 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); + 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 () => clearTimeout(timeoutId); } - }, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents]); + }, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents, fetchUptimeData]); - return { + 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]); }; \ No newline at end of file diff --git a/application/src/components/services/ServiceDetailContent.tsx b/application/src/components/services/ServiceDetailContent.tsx index d925312..8c26143 100644 --- a/application/src/components/services/ServiceDetailContent.tsx +++ b/application/src/components/services/ServiceDetailContent.tsx @@ -1,4 +1,3 @@ - import { Service, UptimeData } from "@/types/service.types"; import { ServiceHeader } from "@/components/services/ServiceHeader"; import { ServiceStatsCards } from "@/components/services/ServiceStatsCards"; @@ -37,6 +36,7 @@ export const ServiceDetailContent = ({ onStatusChange={onStatusChange} selectedRegionalAgent={selectedRegionalAgent} onRegionalAgentChange={onRegionalAgentChange} + uptimeData={uptimeData} /> diff --git a/application/src/components/services/ServiceHeader.tsx b/application/src/components/services/ServiceHeader.tsx index 5016297..9bf2b15 100644 --- a/application/src/components/services/ServiceHeader.tsx +++ b/application/src/components/services/ServiceHeader.tsx @@ -1,93 +1,116 @@ -import { ArrowLeft, Globe } from "lucide-react"; +import { ArrowLeft, Globe, BarChart3 } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { StatusBadge } from "@/components/services/StatusBadge"; import { ServiceMonitoringButton } from "@/components/services/ServiceMonitoringButton"; import { RegionalAgentFilter } from "@/components/services/RegionalAgentFilter"; -import { Service } from "@/types/service.types"; +import { HeatmapDialog } from "./HeatmapDialog"; +import { Service, UptimeData } from "@/types/service.types"; import { useLanguage } from "@/contexts/LanguageContext"; import { cn } from "@/lib/utils"; +import { useState } from "react"; interface ServiceHeaderProps { service: Service; onStatusChange?: (newStatus: "up" | "down" | "paused" | "warning") => void; selectedRegionalAgent?: string; onRegionalAgentChange?: (agent: string) => void; + uptimeData?: UptimeData[]; } export function ServiceHeader({ service, onStatusChange, selectedRegionalAgent, - onRegionalAgentChange + onRegionalAgentChange, + uptimeData = [] }: ServiceHeaderProps) { const navigate = useNavigate(); const { t } = useLanguage(); + const [showHeatmap, setShowHeatmap] = useState(false); return ( -
- - -
-
-

{service.name}

- - {/* Pulsating Circle Animation */} -
- - + <> +
+ + +
+
+

{service.name}

+ + {/* Pulsating Circle Animation */} +
+ + +
+ + {service.url && ( + + + {service.url} + + )}
- {service.url && ( - + + +
- -
- - - {selectedRegionalAgent !== undefined && onRegionalAgentChange && ( - - )} + + Heatmap + + {selectedRegionalAgent !== undefined && onRegionalAgentChange && ( + + )} +
-
+ + + ); } \ No newline at end of file diff --git a/application/src/components/services/StatusBadge.tsx b/application/src/components/services/StatusBadge.tsx index 2621b0a..df8b8de 100644 --- a/application/src/components/services/StatusBadge.tsx +++ b/application/src/components/services/StatusBadge.tsx @@ -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: + + Up + + + }; + 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 ( -
- - Up -
- ); - case "down": - return ( -
- - Down -
- ); - case "warning": - return ( -
- - Warning -
- ); - case "paused": - return ( -
- - Paused -
- ); - default: - return null; - } + return ( + + {config.label} + + ); }; + +// Memoize the component to prevent unnecessary re-renders +export const StatusBadge = memo(StatusBadgeComponent); \ No newline at end of file diff --git a/application/src/components/services/UptimeBar.tsx b/application/src/components/services/UptimeBar.tsx index 1a827fd..642e35e 100644 --- a/application/src/components/services/UptimeBar.tsx +++ b/application/src/components/services/UptimeBar.tsx @@ -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 - } - - 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 + // Memoize the uptime calculation to prevent unnecessary recalculations + const { uptimePercentage, displayData } = useMemo(() => { + if (!uptimeData || uptimeData.length === 0) { + return { + uptimePercentage: uptime || 0, + displayData: [] + }; } - }; - - if (isLoading) { - return ( -
-
- {Array(20).fill(null).map((_, index) => ( -
- ))} -
- -
- ); - } - // console.log('UptimeBar rendering consolidated items:', consolidatedItems.length); + // Calculate uptime percentage from actual data + const upCount = uptimeData.filter(item => item.status === 'up').length; + const totalCount = uptimeData.length; + const calculatedUptime = totalCount > 0 ? (upCount / totalCount) * 100 : 0; - return ( -
- -
- {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) - ); + // Limit display to last 20 items for performance + const limitedData = uptimeData.slice(0, 20); - // 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'; - } + return { + uptimePercentage: Math.round(calculatedUptime * 100) / 100, + displayData: limitedData + }; + }, [uptimeData, uptime]); - // 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}`); - }); + // Memoize the status items to prevent unnecessary re-renders + const statusItems = useMemo(() => + displayData.map((item, index) => ( + + )), + [displayData] + ); - return ( - - -
- - -
-
- {formatRelative(new Date(slot.timestamp), new Date())} -
- {hasValidData ? ( -
- {slot.items.map((item, itemIndex) => ( -
-
-
- {item.source} -
-
- {item.status === 'paused' ? 'Paused' : - item.responseTime && item.responseTime > 0 ? `${item.responseTime}ms` : - 'No response'} -
-
- ))} - {slot.items.length > 1 && ( -
- {slot.items.length} monitoring sources -
- )} -
- ) : ( -
- No monitoring data available -
- )} -
- - - ); - })} + return ( + +
+
+ {statusItems.length > 0 ? statusItems : ( + // Fallback display when no data +
+ )}
- - -
+ + {uptimePercentage.toFixed(1)}% + +
+ + + Last 20 checks + +
); -}; \ No newline at end of file +}; + +// Memoize the component to prevent unnecessary re-renders +export const UptimeBar = memo(UptimeBarComponent); \ No newline at end of file diff --git a/application/src/components/services/hooks/useUptimeData.ts b/application/src/components/services/hooks/useUptimeData.ts index 0bfe235..96a20d1 100644 --- a/application/src/components/services/hooks/useUptimeData.ts +++ b/application/src/components/services/hooks/useUptimeData.ts @@ -14,7 +14,7 @@ interface UseUptimeDataProps { export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseUptimeDataProps) => { const [historyItems, setHistoryItems] = useState([]); - // 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), diff --git a/application/src/components/ssl-domain/SSLCertificatesTable.tsx b/application/src/components/ssl-domain/SSLCertificatesTable.tsx index ce6fb15..75df57a 100644 --- a/application/src/components/ssl-domain/SSLCertificatesTable.tsx +++ b/application/src/components/ssl-domain/SSLCertificatesTable.tsx @@ -1,8 +1,8 @@ + import React, { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Plus } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Table, TableBody, @@ -37,10 +37,12 @@ import { fetchSSLCertificates, addSSLCertificate, deleteSSLCertificate } from "@ import { pb } from "@/lib/pocketbase"; import { SSLCertificate } from "@/types/ssl.types"; import { useLanguage } from "@/contexts/LanguageContext"; +import { useTheme } from "@/contexts/ThemeContext"; import { toast } from "sonner"; export const SSLCertificatesTable = () => { const { t } = useLanguage(); + const { theme } = useTheme(); const queryClient = useQueryClient(); const [showAddDialog, setShowAddDialog] = useState(false); const [showEditDialog, setShowEditDialog] = useState(false); @@ -65,7 +67,7 @@ export const SSLCertificatesTable = () => { setShowAddDialog(false); toast.success(t('certificateAdded')); } catch (error) { - console.error("Error adding certificate:", error); + // console.error("Error adding certificate:", error); toast.error(t('failedToAddCertificate')); } finally { setIsSubmitting(false); @@ -87,7 +89,7 @@ export const SSLCertificatesTable = () => { setSelectedCertificate(null); toast.success(t('certificateUpdated')); } catch (error) { - console.error("Error updating certificate:", error); + // console.error("Error updating certificate:", error); toast.error(t('failedToUpdateCertificate')); } finally { setIsSubmitting(false); @@ -105,7 +107,7 @@ export const SSLCertificatesTable = () => { setSelectedCertificate(null); toast.success(t('certificateDeleted')); } catch (error) { - console.error("Error deleting certificate:", error); + // console.error("Error deleting certificate:", error); toast.error(t('failedToDeleteCertificate')); } finally { setIsSubmitting(false); @@ -128,24 +130,28 @@ export const SSLCertificatesTable = () => { }; return ( - <> - - +
+ {certificates.length === 0 ? ( +
+ {t('noCertificatesFound')} +
+ ) : ( +
- - - {t('domain')} - {t('status')} - {t('issuer')} - {t('validUntil')} - {t('daysLeft')} - Check Interval - + + + {t('domain')} + {t('status')} + {t('issuer')} + {t('validUntil')} + {t('daysLeft')} + Check Interval + Actions {certificates.map((certificate) => ( - + {certificate.domain} @@ -164,7 +170,7 @@ export const SSLCertificatesTable = () => { {certificate.check_interval || 1} {t('days')} - + { ))}
- - {certificates.length === 0 && ( -
- {t('noCertificatesFound')} -
- )} - - +
+ )} {/* View Certificate Dialog */} { - +
); }; \ No newline at end of file diff --git a/application/src/components/ssl-domain/SSLDomainContent.tsx b/application/src/components/ssl-domain/SSLDomainContent.tsx index 6769462..bb38980 100644 --- a/application/src/components/ssl-domain/SSLDomainContent.tsx +++ b/application/src/components/ssl-domain/SSLDomainContent.tsx @@ -30,12 +30,12 @@ export const SSLDomainContent = () => { queryKey: ['ssl-certificates'], queryFn: async () => { try { - // console.log("Fetching SSL certificates from SSLDomainContent..."); + console.log("Fetching SSL certificates from SSLDomainContent..."); const result = await fetchSSLCertificates(); - // console.log("Received SSL certificates:", result); + console.log("Received SSL certificates:", result); return result; } catch (error) { - // console.error("Error fetching certificates:", error); + console.error("Error fetching certificates:", error); toast.error(t('failedToLoadCertificates')); throw error; } @@ -53,7 +53,7 @@ export const SSLDomainContent = () => { toast.success(t('sslCertificateAdded')); }, onError: (error) => { - // console.error("Error adding SSL certificate:", error); + console.error("Error adding SSL certificate:", error); toast.error(error instanceof Error ? error.message : t('failedToAddCertificate')); } }); @@ -61,7 +61,7 @@ export const SSLDomainContent = () => { // Edit certificate mutation - Updated to ensure thresholds are properly updated const editMutation = useMutation({ mutationFn: async (certificate: SSLCertificate) => { - // console.log("Updating certificate with data:", certificate); + console.log("Updating certificate with data:", certificate); // Create the update data object const updateData = { @@ -70,12 +70,12 @@ export const SSLDomainContent = () => { notification_channel: certificate.notification_channel, }; - // console.log("Update data to be sent:", updateData); + console.log("Update data to be sent:", updateData); // Update certificate in the database using PocketBase directly const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData); - // console.log("PocketBase update response:", updated); + console.log("PocketBase update response:", updated); // After updating the settings, refresh the certificate to ensure it's up to date // This will also check if notification needs to be sent based on updated thresholds @@ -90,7 +90,7 @@ export const SSLDomainContent = () => { toast.success(t('sslCertificateUpdated')); }, onError: (error) => { - // console.error("Error updating SSL certificate:", error); + console.error("Error updating SSL certificate:", error); toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate')); } }); @@ -103,33 +103,26 @@ export const SSLDomainContent = () => { toast.success(t('sslCertificateDeleted')); }, onError: (error) => { - // console.error("Error deleting SSL certificate:", error); + console.error("Error deleting SSL certificate:", error); toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate')); } }); - // Refresh certificate mutation - Updated to ensure notifications are sent + // Refresh certificate mutation - Updated to remove individual toast notifications const refreshMutation = useMutation({ mutationFn: checkAndUpdateCertificate, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] }); setRefreshingId(null); - toast.success(t('sslCertificateRefreshed').replace('{domain}', data.domain)); + // Removed individual success toast notification }, onError: (error) => { - // console.error("Error refreshing SSL certificate:", error); - - let errorMessage = t('failedToCheckCertificate'); - - if (error instanceof Error) { - errorMessage = error.message; - } - - toast.error(errorMessage); + console.error("Error refreshing SSL certificate:", error); setRefreshingId(null); // Still refresh the data to show any partial information that was saved queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] }); + // Removed individual error toast notification } }); @@ -149,7 +142,7 @@ export const SSLDomainContent = () => { } }, onError: (error) => { - // console.error("Error refreshing all certificates:", error); + console.error("Error refreshing all certificates:", error); toast.error(t('failedToCheckCertificate')); setIsRefreshingAll(false); @@ -174,7 +167,7 @@ export const SSLDomainContent = () => { }; const handleUpdateCertificate = (certificate: SSLCertificate) => { - // console.log("Handling certificate update with data:", certificate); + console.log("Handling certificate update with data:", certificate); editMutation.mutate(certificate); }; diff --git a/application/src/pages/ContainerMonitoring.tsx b/application/src/pages/ContainerMonitoring.tsx index c302806..b5090d4 100644 --- a/application/src/pages/ContainerMonitoring.tsx +++ b/application/src/pages/ContainerMonitoring.tsx @@ -29,7 +29,7 @@ const ContainerMonitoring = () => { }); const [currentUser, setCurrentUser] = useState(authService.getCurrentUser()); - console.log('ContainerMonitoring component loaded with serverId:', serverId); + // console.log('ContainerMonitoring component loaded with serverId:', serverId); const { data: containers = [], @@ -39,26 +39,26 @@ const ContainerMonitoring = () => { } = useQuery({ queryKey: ['docker-containers', serverId], queryFn: () => { - console.log('Query function called with serverId:', serverId); + // console.log('Query function called with serverId:', serverId); return serverId ? dockerService.getContainersByServerId(serverId) : dockerService.getContainers(); }, refetchInterval: 30000 // Refetch every 30 seconds }); - console.log('Query state:', { containers, isLoading, error }); + // console.log('Query state:', { containers, isLoading, error }); useEffect(() => { - console.log('Containers changed:', containers); + // console.log('Containers changed:', containers); if (containers.length > 0) { dockerService.getContainerStats(containers).then(newStats => { - console.log('Stats calculated:', newStats); + // console.log('Stats calculated:', newStats); setStats(newStats); }); } }, [containers]); const handleRefresh = () => { - console.log('Manual refresh triggered'); + // console.log('Manual refresh triggered'); refetch(); }; @@ -72,7 +72,7 @@ const ContainerMonitoring = () => { }; if (error) { - console.error('Container monitoring error:', error); + // console.error('Container monitoring error:', error); return (
diff --git a/application/src/pages/Dashboard.tsx b/application/src/pages/Dashboard.tsx index a61dd8e..2ab2c52 100644 --- a/application/src/pages/Dashboard.tsx +++ b/application/src/pages/Dashboard.tsx @@ -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) { diff --git a/application/src/pages/InstanceMonitoring.tsx b/application/src/pages/InstanceMonitoring.tsx index 7ad1fc4..8ab5641 100644 --- a/application/src/pages/InstanceMonitoring.tsx +++ b/application/src/pages/InstanceMonitoring.tsx @@ -7,11 +7,14 @@ import { Sidebar } from "@/components/dashboard/Sidebar"; import { Header } from "@/components/dashboard/Header"; import { ServerStatsCards } from "@/components/servers/ServerStatsCards"; import { ServerTable } from "@/components/servers/ServerTable"; +import { AddServerAgentDialog } from "@/components/servers/AddServerAgentDialog"; import { serverService } from "@/services/serverService"; import { Server, ServerStats } from "@/types/server.types"; import { useSidebar } from "@/contexts/SidebarContext"; import { authService } from "@/services/authService"; import { useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; const InstanceMonitoring = () => { const { theme } = useTheme(); @@ -27,6 +30,7 @@ const InstanceMonitoring = () => { }); const [currentUser, setCurrentUser] = useState(authService.getCurrentUser()); + const [addDialogOpen, setAddDialogOpen] = useState(false); const { data: servers = [], isLoading, error, refetch } = useQuery({ queryKey: ['servers'], @@ -48,12 +52,16 @@ const InstanceMonitoring = () => { authService.logout(); navigate('/login'); }; + + const handleAgentAdded = () => { + refetch(); + }; if (error) { return (
-
+
{ return (
-
+
{ toggleSidebar={toggleSidebar} />
-
+
{/* Header Section */} -
+
-

+

Instance Monitoring

-

+

Monitor and manage your server instances in real-time

+
{/* Stats Cards Section */} -
+
{/* Server Table Section */} -
+
+ +
); }; diff --git a/application/src/pages/ServerDetail.tsx b/application/src/pages/ServerDetail.tsx index 3dee273..9b714e7 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"; @@ -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/dockerService.ts b/application/src/services/dockerService.ts index d01e98e..8047240 100644 --- a/application/src/services/dockerService.ts +++ b/application/src/services/dockerService.ts @@ -5,25 +5,25 @@ import { DockerContainer, DockerMetrics, DockerStats } from "@/types/docker.type class DockerService { async getContainers(): Promise { try { - console.log('Fetching all Docker containers...'); + // console.log('Fetching all Docker containers...'); const records = await pb.collection('dockers').getFullList({ sort: '-created', }); - console.log('Docker containers fetched:', records); + // console.log('Docker containers fetched:', records); return records as DockerContainer[]; } catch (error) { - console.error('Error fetching Docker containers:', error); + // console.error('Error fetching Docker containers:', error); throw error; } } async getContainersByServerId(serverId: string): Promise { try { - console.log('Fetching Docker containers for server ID:', serverId); + // console.log('Fetching Docker containers for server ID:', serverId); // First, try to get the server details to find the correct hostname const server = await pb.collection('servers').getOne(serverId); - console.log('Server details:', server); + // console.log('Server details:', server); // Try multiple filter approaches to find containers const filters = [ @@ -36,57 +36,57 @@ class DockerService { let containers: DockerContainer[] = []; for (const filter of filters) { - console.log('Trying filter:', filter); + // console.log('Trying filter:', filter); try { const records = await pb.collection('dockers').getFullList({ filter: filter, sort: '-created', }); - console.log(`Filter "${filter}" returned:`, records); + // console.log(`Filter "${filter}" returned:`, records); if (records.length > 0) { containers = records as DockerContainer[]; break; } } catch (filterError) { - console.warn(`Filter "${filter}" failed:`, filterError); + // console.warn(`Filter "${filter}" failed:`, filterError); continue; } } // If no containers found with filters, get all and log for debugging if (containers.length === 0) { - console.log('No containers found with filters, fetching all for debugging...'); + // console.log('No containers found with filters, fetching all for debugging...'); const allContainers = await pb.collection('dockers').getFullList({ sort: '-created', }); - console.log('All available Docker containers:', allContainers); - console.log('Looking for containers that might match server:', { - serverId, - serverHostname: server.hostname, - serverIp: server.ip_address - }); + // console.log('All available Docker containers:', allContainers); + // console.log('Looking for containers that might match server:', { + //// serverId, + // serverHostname: server.hostname, + // serverIp: server.ip_address + // }); } return containers; } catch (error) { - console.error('Error fetching Docker containers by server ID:', error); + // console.error('Error fetching Docker containers by server ID:', error); throw error; } } async getContainerMetrics(dockerId: string): Promise { try { - console.log('Fetching metrics for docker ID:', dockerId); + // console.log('Fetching metrics for docker ID:', dockerId); const records = await pb.collection('docker_metrics').getFullList({ filter: `docker_id = "${dockerId}"`, sort: '-timestamp', perPage: 100, }); - console.log('Docker metrics fetched:', records); + // console.log('Docker metrics fetched:', records); return records as DockerMetrics[]; } catch (error) { - console.error('Error fetching Docker metrics:', error); + // console.error('Error fetching Docker metrics:', error); throw error; } } diff --git a/application/src/services/monitoring/service-status/startMonitoring.ts b/application/src/services/monitoring/service-status/startMonitoring.ts index a1d7a11..5cfb9e8 100644 --- a/application/src/services/monitoring/service-status/startMonitoring.ts +++ b/application/src/services/monitoring/service-status/startMonitoring.ts @@ -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 { 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 { 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 { }); // 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); } } \ No newline at end of file diff --git a/application/src/services/serverService.ts b/application/src/services/serverService.ts index c808783..1bdda23 100644 --- a/application/src/services/serverService.ts +++ b/application/src/services/serverService.ts @@ -24,183 +24,144 @@ export const serverService = { 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); - } + // 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 + }); - // 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); + // 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 + // }); + + // 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)) + // }))); } - // 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'); + // 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)); } - // console.log('serverService.getServerMetrics: Final filter:', filter); + // Try to get filtered records + const searchStrategies = [ + server?.server_id, + serverId, + server?.id + ].filter(Boolean); - // Fetch filtered records with proper sorting - let records = await pb.collection('server_metrics').getFullList({ - filter: filter, - sort: '-created', - requestKey: null - }); + let filteredRecords: any[] = []; - // 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; + for (const strategy of searchStrategies) { + try { + const cutoffISO = cutoffTime.toISOString(); + const filter = `server_id = "${strategy}" && created >= "${cutoffISO}"`; - 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)); + // 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; } - - const cutoffISO = cutoffTime.toISOString(); - fallbackFilter += ` && created >= "${cutoffISO}"`; + } 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...'); - // 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('serverService.getServerMetrics: Fallback found', records.length, 'records'); + const last24h = new Date(now.getTime() - (24 * 60 * 60 * 1000)); - // 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); - - 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('serverService.getServerMetrics: Trying alternative ID filter:', altFilter); - const altRecords = await pb.collection('server_metrics').getFullList({ - filter: altFilter, - sort: '-created', - requestKey: null - }); + 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)) + // )); - 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..4a2804c --- /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/ssl/notification/sslCheckNotifier.ts b/application/src/services/ssl/notification/sslCheckNotifier.ts index 5657118..e35a494 100644 --- a/application/src/services/ssl/notification/sslCheckNotifier.ts +++ b/application/src/services/ssl/notification/sslCheckNotifier.ts @@ -1,120 +1,53 @@ import { pb } from "@/lib/pocketbase"; -import { SSLCertificate } from "../types"; -import { determineSSLStatus } from "../sslStatusUtils"; -import { sendSSLNotification } from "./sslNotificationSender"; -import { toast } from "sonner"; +import { SSLCertificate } from "@/types/ssl.types"; /** - * Checks all SSL certificates and sends notifications for expiring ones - * This should be called once per day + * Check a single SSL certificate - notifications are now handled by the backend */ -export async function checkAllCertificatesAndNotify(): Promise { - // console.log("Starting daily SSL certificates check..."); - +export const checkCertificateAndNotify = async (certificate: SSLCertificate): Promise => { try { - // Fetch all SSL certificates from database - const response = await pb.collection('ssl_certificates').getList(1, 100, {}); - // Properly cast the items as SSLCertificate - const certificates = response.items as unknown as SSLCertificate[]; + // console.log(`Checking certificate for ${certificate.domain}...`); - // console.log(`Found ${certificates.length} certificates to check`); + // The actual SSL checking and notifications are now handled by the Go service + // We just need to trigger a check by updating the check_at timestamp - // Check each certificate - for (const cert of certificates) { - await checkCertificateAndNotify(cert); - } + const now = new Date(); - // console.log("Daily SSL certificates check completed"); + // Update check_at to trigger backend check + await pb.collection('ssl_certificates').update(certificate.id, { + check_at: now.toISOString() + }); + } catch (error) { - // console.error("Error during SSL certificates daily check:", error); + throw error; } -} +}; /** - * Checks a specific SSL certificate and sends notification if needed - * This respects the Warning and Expiry Thresholds set on the certificate - * Note: SSL checking is now handled by the Go service, this function focuses on notifications + * Check all SSL certificates - backend handles the actual checking and notifications */ -export async function checkCertificateAndNotify(certificate: SSLCertificate): Promise { -// console.log(`Checking certificate for ${certificate.domain}...`); - +export const checkAllCertificatesAndNotify = async (): Promise<{ success: number; failed: number }> => { try { - // Use the current certificate data (updated by Go service) - const daysLeft = certificate.days_left || 0; - - // Get threshold values (ensure they are numbers) - const warningThreshold = Number(certificate.warning_threshold) || 30; - const expiryThreshold = Number(certificate.expiry_threshold) || 7; - - // console.log(`Certificate ${certificate.domain} thresholds: warning=${warningThreshold}, expiry=${expiryThreshold}, days left=${daysLeft}`); - - // Update status based on thresholds - const status = determineSSLStatus(daysLeft, warningThreshold, expiryThreshold); - - // Check if we should send a notification based on thresholds - let shouldNotify = false; - let isCritical = false; - - // Critical notifications - when days left is less than or equal to expiry threshold - if (daysLeft <= expiryThreshold) { - shouldNotify = true; - isCritical = true; - } - // Warning notifications - when days left is less than or equal to warning threshold but greater than expiry threshold - else if (daysLeft <= warningThreshold) { - shouldNotify = true; - isCritical = false; - } - - // console.log(`${certificate.domain}: ${daysLeft} days left, status: ${status}, should notify: ${shouldNotify}, critical: ${isCritical}`); - - // Update certificate status in database - await pb.collection('ssl_certificates').update(certificate.id, { - status: status - }); - - // Send notification if needed - if (shouldNotify && certificate.notification_channel) { - // console.log(`Sending notification for ${certificate.domain}`); - - // Different message based on expiry threshold - const message = isCritical - ? `🚨 CRITICAL: SSL Certificate for ${certificate.domain} will expire in ${daysLeft} days!` - : `⚠️ WARNING: SSL Certificate for ${certificate.domain} will expire in ${daysLeft} days.`; - - // Send the notification using our specialized SSL notification sender - const notificationSent = await sendSSLNotification(certificate, message, isCritical); - - if (notificationSent) { - // Update last_notified timestamp - await pb.collection('ssl_certificates').update(certificate.id, { - last_notified: new Date().toISOString() - }); + + const response = await pb.collection('ssl_certificates').getList(1, 100); + const certificates = response.items as unknown as SSLCertificate[]; - // console.log(`Notification sent for ${certificate.domain}`); - // Show toast for manual checks - toast.success(`Notification sent for ${certificate.domain}`); - return true; - } else { - // console.error(`Failed to send notification for ${certificate.domain}`); - // Show error toast for manual checks - toast.error(`Failed to send notification for ${certificate.domain}`); - return false; + let success = 0; + let failed = 0; + + for (const cert of certificates) { + try { + await checkCertificateAndNotify(cert); + success++; + } catch (error) { + failed++; } - } else if (shouldNotify && !certificate.notification_channel) { - // console.log(`No notification channel set for ${certificate.domain}, skipping notification`); - toast.info(`No notification channel set for ${certificate.domain}, skipping notification`); - } else { - // console.log(`No notification needed for ${certificate.domain} (${daysLeft} days left)`); - // For manual checks, inform the user that thresholds weren't met - toast.info(`Certificate for ${certificate.domain} is valid (${daysLeft} days left)`); } - return true; + return { success, failed }; + } catch (error) { - // console.error(`Error checking certificate for ${certificate.domain}:`, error); - toast.error(`Error checking certificate: ${error instanceof Error ? error.message : "Unknown error"}`); - return false; + throw error; } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/application/src/services/templateService.ts b/application/src/services/templateService.ts index 85d5536..f1c37fe 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 de0c42d..90e0f2b 100644 --- a/application/src/types/server.types.ts +++ b/application/src/types/server.types.ts @@ -8,7 +8,7 @@ export interface Server { hostname: string; ip_address: string; os_type: string; - status: 'up' | 'down' | 'warning'; + status: 'up' | 'down' | 'warning' | 'paused'; uptime: string; ram_total: number; ram_used: number; @@ -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; diff --git a/application/src/utils/copyUtils.ts b/application/src/utils/copyUtils.ts new file mode 100644 index 0000000..24483d4 --- /dev/null +++ b/application/src/utils/copyUtils.ts @@ -0,0 +1,60 @@ + +import { toast } from "@/hooks/use-toast"; + +export const copyToClipboard = async (text: string) => { + console.log('copyToClipboard called with text:', text); // Debug log + + try { + // Try modern clipboard API first + if (navigator.clipboard && window.isSecureContext) { + console.log('Using modern clipboard API'); // Debug log + await navigator.clipboard.writeText(text); + toast({ + title: "Copied!", + description: "Content copied to clipboard successfully.", + }); + return; + } + + console.log('Using fallback clipboard method'); // Debug log + + // Fallback for older browsers or non-secure contexts + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position = "fixed"; + textArea.style.left = "-9999px"; + textArea.style.top = "-9999px"; + textArea.style.opacity = "0"; + textArea.style.pointerEvents = "none"; + textArea.style.zIndex = "-1"; + document.body.appendChild(textArea); + + // Focus and select the text + textArea.focus(); + textArea.select(); + textArea.setSelectionRange(0, textArea.value.length); + + // Use execCommand as fallback + const successful = document.execCommand('copy'); + document.body.removeChild(textArea); + + if (successful) { + console.log('Copy successful with execCommand'); // Debug log + toast({ + title: "Copied!", + description: "Content copied to clipboard successfully.", + }); + } else { + throw new Error('Copy command failed'); + } + } catch (error) { + console.error('Failed to copy to clipboard:', error); + + // Show error toast + toast({ + title: "Copy Failed", + description: "Unable to copy automatically. Please select and copy the text manually.", + variant: "destructive", + }); + } +}; \ No newline at end of file diff --git a/scripts/server-agent.sh b/scripts/server-agent.sh new file mode 100644 index 0000000..3435b3e --- /dev/null +++ b/scripts/server-agent.sh @@ -0,0 +1,460 @@ + +#!/bin/bash + +# CheckCle Server Monitoring Agent - One-Click Installation Script +# This script provides fully automated installation using environment variables + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="/etc/monitoring-agent/monitoring-agent.env" + +# GitHub release base URL +GITHUB_BASE_URL="https://github.com/operacle/checke-server-agent/releases/download/v1.0.0" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if running as root +check_root() { + if [[ $EUID -ne 0 ]]; then + log_error "This script must be run as root (use sudo)" + exit 1 + fi +} + +# Detect system architecture and package format +detect_system() { + log_info "Detecting system architecture and package format..." + + # Detect architecture + ARCH=$(uname -m) + case $ARCH in + x86_64) + PACKAGE_ARCH="amd64" + ;; + aarch64|arm64) + PACKAGE_ARCH="arm64" + ;; + *) + log_error "Unsupported architecture: $ARCH" + log_info "Supported architectures: x86_64 (amd64), aarch64/arm64" + exit 1 + ;; + esac + + # Detect package format preference + if command -v dpkg >/dev/null 2>&1; then + PACKAGE_FORMAT="deb" + PACKAGE_MANAGER="dpkg" + elif command -v rpm >/dev/null 2>&1; then + PACKAGE_FORMAT="rpm" + if command -v yum >/dev/null 2>&1; then + PACKAGE_MANAGER="yum" + elif command -v dnf >/dev/null 2>&1; then + PACKAGE_MANAGER="dnf" + else + PACKAGE_MANAGER="rpm" + fi + else + log_error "No supported package manager found (dpkg or rpm required)" + log_info "This script supports Debian/Ubuntu (.deb) and RHEL/CentOS/Fedora (.rpm) systems" + exit 1 + fi + + # Construct package filename and URL + PACKAGE_FILENAME="monitoring-agent_1.0.0_${PACKAGE_ARCH}.${PACKAGE_FORMAT}" + PACKAGE_URL="${GITHUB_BASE_URL}/${PACKAGE_FILENAME}" + + log_success "System detection complete:" + log_info " Architecture: $ARCH -> $PACKAGE_ARCH" + log_info " Package format: $PACKAGE_FORMAT" + log_info " Package manager: $PACKAGE_MANAGER" + log_info " Package URL: $PACKAGE_URL" +} + +# Validate required environment variables - check both current and sudo environments +validate_environment() { + # Check if SERVER_TOKEN is available in current environment or passed as argument + if [[ -z "$SERVER_TOKEN" ]]; then + log_error "SERVER_TOKEN environment variable is required" + log_info "Usage examples:" + log_info " SERVER_TOKEN=your-token sudo -E bash $0" + log_info " sudo SERVER_TOKEN=your-token bash $0" + log_info " curl -L script-url | SERVER_TOKEN=your-token sudo bash" + log_info "" + log_info "Required: SERVER_TOKEN" + log_info "Optional: POCKETBASE_URL, SERVER_NAME, AGENT_ID, HEALTH_CHECK_PORT" + exit 1 + fi + + log_success "Environment validation passed" + log_info "SERVER_TOKEN: ${SERVER_TOKEN:0:8}..." + [[ -n "$POCKETBASE_URL" ]] && log_info "POCKETBASE_URL: $POCKETBASE_URL" + [[ -n "$SERVER_NAME" ]] && log_info "SERVER_NAME: $SERVER_NAME" + [[ -n "$AGENT_ID" ]] && log_info "AGENT_ID: $AGENT_ID" +} + +# Download package based on detected system +download_package() { + local temp_dir="/tmp/monitoring-agent-install" + mkdir -p "$temp_dir" + + log_info "Downloading monitoring agent package..." + log_info "URL: $PACKAGE_URL" + + if curl -L -f -o "$temp_dir/$PACKAGE_FILENAME" "$PACKAGE_URL"; then + DOWNLOADED_PACKAGE="$temp_dir/$PACKAGE_FILENAME" + log_success "Package downloaded successfully: $PACKAGE_FILENAME" + else + log_error "Failed to download package from: $PACKAGE_URL" + log_info "Please check:" + log_info " 1. Internet connectivity" + log_info " 2. Package availability for your architecture ($PACKAGE_ARCH)" + log_info " 3. GitHub repository access" + exit 1 + fi +} + +# Install package based on detected package manager +install_package() { + log_info "Installing monitoring agent package using $PACKAGE_MANAGER..." + + case $PACKAGE_MANAGER in + dpkg) + # Update package lists + apt-get update -qq + + # Install the package + if dpkg -i "$DOWNLOADED_PACKAGE" 2>/dev/null; then + log_success "DEB package installed successfully" + else + log_warning "Package installation had dependency issues, fixing..." + apt-get install -f -y + log_success "Dependencies resolved and package installed" + fi + ;; + + rpm) + # Install the package directly + if rpm -ivh "$DOWNLOADED_PACKAGE" 2>/dev/null; then + log_success "RPM package installed successfully" + else + log_error "RPM package installation failed" + log_info "Try installing manually: sudo rpm -ivh $DOWNLOADED_PACKAGE" + exit 1 + fi + ;; + + yum) + if yum localinstall -y "$DOWNLOADED_PACKAGE"; then + log_success "Package installed successfully via YUM" + else + log_error "YUM package installation failed" + exit 1 + fi + ;; + + dnf) + if dnf localinstall -y "$DOWNLOADED_PACKAGE"; then + log_success "Package installed successfully via DNF" + else + log_error "DNF package installation failed" + exit 1 + fi + ;; + + *) + log_error "Unsupported package manager: $PACKAGE_MANAGER" + exit 1 + ;; + esac +} + +# Auto-detect system information +detect_system_info() { + log_info "Auto-detecting system information..." + + # Detect hostname + DETECTED_HOSTNAME=$(hostname) + log_info "Detected hostname: $DETECTED_HOSTNAME" + + # Detect IP address + DETECTED_IP=$(ip route get 8.8.8.8 2>/dev/null | head -1 | awk '{print $7}' | head -1) + if [[ -z "$DETECTED_IP" ]]; then + DETECTED_IP=$(hostname -I | awk '{print $1}') + fi + log_info "Detected IP address: $DETECTED_IP" + + # Detect OS + if [[ -f /etc/os-release ]]; then + DETECTED_OS=$(grep "^ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"') + else + DETECTED_OS="linux" + fi + log_info "Detected OS: $DETECTED_OS" +} + +# Configure agent using environment variables +configure_agent() { + log_info "Configuring agent with provided settings..." + + # Use environment variables or auto-detected defaults + SERVER_NAME="${SERVER_NAME:-$DETECTED_HOSTNAME}" + POCKETBASE_URL="${POCKETBASE_URL:-http://localhost:8090}" + IP_ADDRESS="${IP_ADDRESS:-$DETECTED_IP}" + HOSTNAME="${HOSTNAME:-$DETECTED_HOSTNAME}" + OS_TYPE="${OS_TYPE:-$DETECTED_OS}" + AGENT_ID="${AGENT_ID:-monitoring-agent-$(hostname -s)}" + HEALTH_CHECK_PORT="${HEALTH_CHECK_PORT:-8081}" + + log_info "Final configuration:" + log_info " Server Name: $SERVER_NAME" + log_info " Agent ID: $AGENT_ID" + log_info " PocketBase URL: $POCKETBASE_URL" + log_info " IP Address: $IP_ADDRESS" + log_info " OS Type: $OS_TYPE" + log_info " Health Check Port: $HEALTH_CHECK_PORT" + + write_config +} + +# Write configuration to file +write_config() { + log_info "Writing configuration to $CONFIG_FILE..." + + # Create directory if it doesn't exist + mkdir -p "$(dirname "$CONFIG_FILE")" + + cat > "$CONFIG_FILE" << EOF +# CheckCle Server Monitoring Agent Configuration +# Generated on $(date) + +# Basic Configuration +AGENT_ID=$AGENT_ID +CHECK_INTERVAL=30s +HEALTH_CHECK_PORT=$HEALTH_CHECK_PORT + +# PocketBase Configuration +POCKETBASE_ENABLED=true +POCKETBASE_URL=$POCKETBASE_URL + +# Server Configuration +SERVER_NAME=$SERVER_NAME +HOSTNAME=$HOSTNAME +IP_ADDRESS=$IP_ADDRESS +OS_TYPE=$OS_TYPE +SERVER_TOKEN=$SERVER_TOKEN + +# Remote Control +REMOTE_CONTROL_ENABLED=true +COMMAND_CHECK_INTERVAL=10s + +# Monitoring Settings +REPORT_INTERVAL=5m +MAX_RETRIES=3 +REQUEST_TIMEOUT=10s +EOF + + # Set proper permissions + chown root:monitoring-agent "$CONFIG_FILE" 2>/dev/null || chown root:root "$CONFIG_FILE" + chmod 640 "$CONFIG_FILE" + + log_success "Configuration written successfully" +} + +# Start and enable service +start_service() { + log_info "Starting monitoring agent service..." + + # Reload systemd + systemctl daemon-reload + + # Enable service + systemctl enable monitoring-agent + log_success "Service enabled for auto-start" + + # Start service + if systemctl start monitoring-agent; then + log_success "Service started successfully" + else + log_error "Failed to start service" + log_info "Check logs with: journalctl -u monitoring-agent -f" + return 1 + fi + + # Check service status + sleep 2 + if systemctl is-active --quiet monitoring-agent; then + log_success "Service is running" + else + log_warning "Service may have issues, checking status..." + systemctl status monitoring-agent --no-pager + return 1 + fi +} + +# Test installation +test_installation() { + log_info "Testing installation..." + + # Test health endpoint + local health_port=${HEALTH_CHECK_PORT:-8081} + log_info "Testing health endpoint at http://localhost:$health_port/health" + + # Wait a moment for service to fully start + sleep 3 + + if curl -s "http://localhost:$health_port/health" > /dev/null; then + log_success "Health endpoint is responding" + else + log_warning "Health endpoint not responding yet (service may still be starting)" + fi + + # Show recent logs + log_info "Recent service logs:" + journalctl -u monitoring-agent --no-pager -n 5 +} + +# Show post-installation information +show_post_install_info() { + echo + echo "=============================================" + echo " Installation Complete!" + echo "=============================================" + echo + log_success "CheckCle Monitoring Agent installed and configured successfully" + echo + echo "System Information:" + echo " Architecture: $ARCH ($PACKAGE_ARCH)" + echo " Package: $PACKAGE_FILENAME" + echo " Package Manager: $PACKAGE_MANAGER" + echo + echo "Configuration: $CONFIG_FILE" + echo "Service status: systemctl status monitoring-agent" + echo "Service logs: journalctl -u monitoring-agent -f" + echo "Health check: curl http://localhost:${HEALTH_CHECK_PORT:-8081}/health" + echo + echo "The monitoring agent is now running and will appear in your dashboard." + echo +} + +# Clean up temporary files +cleanup() { + if [[ -n "$DOWNLOADED_PACKAGE" && -f "$DOWNLOADED_PACKAGE" ]]; then + rm -f "$DOWNLOADED_PACKAGE" + rm -rf "$(dirname "$DOWNLOADED_PACKAGE")" + fi +} + +# Main installation function +main() { + echo "=============================================" + echo " CheckCle Server Monitoring Agent" + echo " One-Click Installation" + echo "=============================================" + echo + + check_root + validate_environment + detect_system + detect_system_info + + log_info "Starting automated installation..." + + # Set up cleanup trap + trap cleanup EXIT + + # Download and install package + download_package + install_package + + # Configure and start service + configure_agent + + if start_service; then + test_installation + show_post_install_info + else + log_error "Service failed to start properly" + log_info "Check configuration: sudo nano $CONFIG_FILE" + log_info "Restart service: sudo systemctl restart monitoring-agent" + exit 1 + fi +} + +# Handle script arguments +case "${1:-}" in + --help|-h) + echo "CheckCle Server Monitoring Agent - One-Click Installer" + echo + echo "Usage: SERVER_TOKEN=your-token [OPTIONS] sudo bash $0" + echo " or: sudo SERVER_TOKEN=your-token [OPTIONS] bash $0" + echo " or: curl -L script-url | SERVER_TOKEN=your-token [OPTIONS] sudo bash" + echo + echo "System Requirements:" + echo " - Linux with systemd" + echo " - Supported architectures: x86_64 (amd64), aarch64/arm64" + echo " - Supported distributions: Debian/Ubuntu (.deb), RHEL/CentOS/Fedora (.rpm)" + echo + echo "Required Environment Variables:" + echo " SERVER_TOKEN Server authentication token" + echo + echo "Optional Environment Variables:" + echo " POCKETBASE_URL PocketBase URL (default: http://localhost:8090)" + echo " SERVER_NAME Server name (default: hostname)" + echo " AGENT_ID Agent identifier (default: monitoring-agent-hostname)" + echo " HEALTH_CHECK_PORT Health check port (default: 8081)" + echo + echo "Examples:" + echo " SERVER_TOKEN=abc123 sudo -E bash $0" + echo " sudo SERVER_TOKEN=abc123 POCKETBASE_URL=https://pb.example.com bash $0" + echo + echo "Options:" + echo " --help, -h Show this help message" + echo " --uninstall Uninstall the monitoring agent" + echo + exit 0 + ;; + --uninstall) + check_root + log_info "Uninstalling monitoring agent..." + systemctl stop monitoring-agent 2>/dev/null || true + systemctl disable monitoring-agent 2>/dev/null || true + + # Remove based on detected package manager + if command -v dpkg >/dev/null 2>&1; then + dpkg -r monitoring-agent 2>/dev/null || true + elif command -v rpm >/dev/null 2>&1; then + rpm -e monitoring-agent 2>/dev/null || true + fi + + rm -rf /etc/monitoring-agent 2>/dev/null || true + log_success "Monitoring agent uninstalled" + exit 0 + ;; + *) + main "$@" + ;; +esac \ No newline at end of file diff --git a/server/pb_migrations/1752916282_updated_webhook_configs.js b/server/pb_migrations/1752916282_updated_webhook_configs.js new file mode 100644 index 0000000..79f94a7 --- /dev/null +++ b/server/pb_migrations/1752916282_updated_webhook_configs.js @@ -0,0 +1,125 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_45665081") + + // remove field + collection.fields.removeById("select4246785570") + + // add field + collection.fields.addAt(9, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + })) + + // add field + collection.fields.addAt(10, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "text1582905952", + "max": 0, + "min": 0, + "name": "method", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + })) + + // add field + collection.fields.addAt(11, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "text1414600993", + "max": 0, + "min": 0, + "name": "payload_template", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + })) + + // update field + collection.fields.addAt(7, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "text2168550802", + "max": 0, + "min": 0, + "name": "trigger_events", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_45665081") + + // add field + collection.fields.addAt(4, new Field({ + "hidden": false, + "id": "select4246785570", + "maxSelect": 1, + "name": "event_filters", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "down", + "up", + "ssl_expired", + "warning", + "ssl_ok", + "high_cpu", + "high_memory", + "agent_offline", + "custom_alert" + ] + })) + + // remove field + collection.fields.removeById("text1579384326") + + // remove field + collection.fields.removeById("text1582905952") + + // remove field + collection.fields.removeById("text1414600993") + + // update field + collection.fields.addAt(8, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "text2168550802", + "max": 0, + "min": 0, + "name": "timeout", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + })) + + return app.save(collection) +}) diff --git a/server/pb_migrations/1752921327_updated_alert_configurations.js b/server/pb_migrations/1752921327_updated_alert_configurations.js new file mode 100644 index 0000000..728ff90 --- /dev/null +++ b/server/pb_migrations/1752921327_updated_alert_configurations.js @@ -0,0 +1,42 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1938176441") + + // update field + collection.fields.addAt(10, new Field({ + "hidden": false, + "id": "select1358543748", + "maxSelect": 1, + "name": "status", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "disabled", + "enabled" + ] + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1938176441") + + // update field + collection.fields.addAt(10, new Field({ + "hidden": false, + "id": "select1358543748", + "maxSelect": 1, + "name": "enabled", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "true", + "false" + ] + })) + + return app.save(collection) +}) diff --git a/server/pb_migrations/1752921397_updated_alert_configurations.js b/server/pb_migrations/1752921397_updated_alert_configurations.js new file mode 100644 index 0000000..fad5689 --- /dev/null +++ b/server/pb_migrations/1752921397_updated_alert_configurations.js @@ -0,0 +1,29 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1938176441") + + // add field + collection.fields.addAt(14, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "text1553704459", + "max": 0, + "min": 0, + "name": "webhook_id", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1938176441") + + // remove field + collection.fields.removeById("text1553704459") + + return app.save(collection) +}) diff --git a/server/pb_migrations/1753085447_updated_server_metrics.js b/server/pb_migrations/1753085447_updated_server_metrics.js new file mode 100644 index 0000000..c0ecf6c --- /dev/null +++ b/server/pb_migrations/1753085447_updated_server_metrics.js @@ -0,0 +1,29 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1998570700") + + // add field + collection.fields.addAt(19, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "text3065852031", + "max": 0, + "min": 0, + "name": "message", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1998570700") + + // remove field + collection.fields.removeById("text3065852031") + + return app.save(collection) +})