-
-
{service.name}
-
- {/* Pulsating Circle Animation */}
-
-
-
+ <>
+
+
+
+
-
-
-
-
- {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 (
-
- );
- case "paused":
- return (
-
- );
- 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.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) ? (
+
})
+ ) : (
+
+ )}
{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)
+})