Merge branch 'operacle:develop' into develop

This commit is contained in:
gnsworks
2025-08-03 16:03:21 +09:00
committed by GitHub
38 changed files with 2945 additions and 1253 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

@@ -114,7 +114,7 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
textarea.setSelectionRange(0, 99999); // For mobile devices textarea.setSelectionRange(0, 99999); // For mobile devices
} }
} catch (selectError) { } catch (selectError) {
console.error('Failed to select text:', selectError); // console.error('Failed to select text:', selectError);
} }
} }
}; };
@@ -14,6 +14,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getCurrentEndpoint } from "@/lib/pocketbase"; import { getCurrentEndpoint } from "@/lib/pocketbase";
import { ServerAgentConfigForm } from "./ServerAgentConfigForm"; import { ServerAgentConfigForm } from "./ServerAgentConfigForm";
import { OneClickInstallTab } from "./OneClickInstallTab"; import { OneClickInstallTab } from "./OneClickInstallTab";
import { DockerOneClickTab } from "./DockerOneClickTab";
import { ManualInstallTab } from "./ManualInstallTab"; import { ManualInstallTab } from "./ManualInstallTab";
interface AddServerAgentDialogProps { interface AddServerAgentDialogProps {
@@ -123,9 +124,10 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
</DialogHeader> </DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="configure">Configure Agent</TabsTrigger> <TabsTrigger value="configure">Configure Agent</TabsTrigger>
<TabsTrigger value="one-click">One-Click Install</TabsTrigger> <TabsTrigger value="one-click">One-Click Install</TabsTrigger>
<TabsTrigger value="docker-one-click">Docker One-Click</TabsTrigger>
<TabsTrigger value="manual">Manual Installation</TabsTrigger> <TabsTrigger value="manual">Manual Installation</TabsTrigger>
</TabsList> </TabsList>
@@ -151,6 +153,16 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
/> />
</TabsContent> </TabsContent>
<TabsContent value="docker-one-click" className="space-y-6">
<DockerOneClickTab
serverToken={serverToken}
currentPocketBaseUrl={currentPocketBaseUrl}
formData={formData}
serverId={serverId}
onDialogClose={handleDialogClose}
/>
</TabsContent>
<TabsContent value="manual" className="space-y-6"> <TabsContent value="manual" className="space-y-6">
<ManualInstallTab <ManualInstallTab
serverToken={serverToken} serverToken={serverToken}
@@ -0,0 +1,176 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Copy, Download, Container } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { copyToClipboard } from "@/utils/copyUtils";
interface DockerOneClickTabProps {
serverToken: string;
currentPocketBaseUrl: string;
formData: {
serverName: string;
osType: string;
checkInterval: string;
retryAttempt: string;
};
serverId: string;
onDialogClose: () => void;
}
export const DockerOneClickTab: React.FC<DockerOneClickTabProps> = ({
serverToken,
currentPocketBaseUrl,
formData,
serverId,
onDialogClose,
}) => {
const getDockerOneClickCommand = () => {
const scriptUrl = "https://cdn.checkcle.io/scripts/server-docker-agent.sh";
return `curl -L -o server-docker-agent.sh "${scriptUrl}"
chmod +x server-docker-agent.sh
SERVER_TOKEN="${serverToken}" \\
POCKETBASE_URL="${currentPocketBaseUrl}" \\
SERVER_NAME="${formData.serverName}" \\
AGENT_ID="${serverId}" \\
sudo -E bash ./server-docker-agent.sh`;
};
const getDirectDockerCommand = () => {
return `docker run -d \\
--name monitoring-agent \\
--restart unless-stopped \\
-p 8081:8081 \\
--group-add 999 \\
-e AGENT_ID="${serverId}" \\
-e SERVER_NAME="${formData.serverName}" \\
-e SERVER_TOKEN="${serverToken}" \\
-e POCKETBASE_URL="${currentPocketBaseUrl}" \\
-e POCKETBASE_ENABLED=true \\
-v /proc:/host/proc:ro \\
-v /etc:/host/etc:ro \\
-v /sys:/host/sys:ro \\
-v /:/host/root:ro \\
-v /var/run:/host/var/run:ro \\
-v /dev:/host/dev:ro \\
-v /var/run/docker.sock:/var/run/docker.sock:ro \\
-v monitoring_data:/var/lib/monitoring-agent \\
-v monitoring_logs:/var/log/monitoring-agent \\
operacle/checkcle-server-agent:latest`;
};
const handleCopyOneClickCommand = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// console.log('Copy one-click command button clicked');
const command = getDockerOneClickCommand();
// console.log('Copying one-click command:', command);
await copyToClipboard(command);
};
const handleCopyDockerCommand = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// console.log('Copy docker command button clicked');
const command = getDirectDockerCommand();
// console.log('Copying docker command:', command);
await copyToClipboard(command);
};
return (
<div className="space-y-6">
{/* One-Click Docker Installation */}
<Card className="border-blue-500/20 bg-blue-50/50 dark:bg-blue-950/20">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
<Container className="h-5 w-5" />
Docker One-Click Install (Recommended)
</CardTitle>
<CardDescription className="text-blue-600 dark:text-blue-300">
Automated Docker container installation with system monitoring capabilities
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="text-blue-700 dark:text-blue-400">Docker One-Click Command</Label>
<div className="relative">
<pre className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all text-blue-800 dark:text-blue-200">
<code>{getDockerOneClickCommand()}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2 bg-blue-50 dark:bg-blue-950/50 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900 text-blue-700 dark:text-blue-400"
onClick={handleCopyOneClickCommand}
>
<Copy className="h-4 w-4 mr-1" />
Copy
</Button>
</div>
</div>
<div className="text-sm text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 p-3 rounded-md">
<p className="font-medium mb-1">This script will automatically:</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>Download and setup the Docker monitoring agent</li>
<li>Configure all required environment variables</li>
<li>Start the container with proper system access</li>
<li>Setup monitoring data persistence</li>
</ol>
</div>
</CardContent>
</Card>
{/* Direct Docker Run Command */}
<Card className="border-gray-500/20">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Container className="h-5 w-5" />
Direct Docker Run Command
</CardTitle>
<CardDescription>
If you prefer to run the Docker container directly without the script
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Docker Run Command</Label>
<div className="relative">
<pre className="bg-muted border p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all">
<code>{getDirectDockerCommand()}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2"
onClick={handleCopyDockerCommand}
>
<Copy className="h-4 w-4 mr-1" />
Copy
</Button>
</div>
</div>
<div className="text-sm text-muted-foreground bg-muted/50 border p-3 rounded-md">
<p className="font-medium mb-1">Prerequisites for direct Docker run:</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>Docker must be installed and running</li>
<li>The operacle/checkcle-server-agent image must be available</li>
<li>Run as root or with sudo privileges</li>
</ol>
</div>
</CardContent>
</Card>
<div className="flex justify-end pt-4">
<Button onClick={onDialogClose}>
Done
</Button>
</div>
</div>
);
};
@@ -1,3 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -12,7 +13,7 @@ import { pb } from "@/lib/pocketbase";
import { Server } from "@/types/server.types"; import { Server } from "@/types/server.types";
import { RefreshCw, X } from "lucide-react"; import { RefreshCw, X } from "lucide-react";
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
import { templateService, NotificationTemplate } from "@/services/templateService"; import { templateService, ServerNotificationTemplate } from "@/services/templateService";
import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService"; import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService";
interface EditServerDialogProps { interface EditServerDialogProps {
@@ -66,9 +67,9 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]); const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [templates, setTemplates] = useState<NotificationTemplate[]>([]); const [templates, setTemplates] = useState<ServerNotificationTemplate[]>([]);
const [thresholds, setThresholds] = useState<ServerThreshold[]>([]); const [thresholds, setThresholds] = useState<ServerThreshold[]>([]);
const [selectedTemplate, setSelectedTemplate] = useState<NotificationTemplate | null>(null); const [selectedTemplate, setSelectedTemplate] = useState<ServerNotificationTemplate | null>(null);
const [selectedThreshold, setSelectedThreshold] = useState<ServerThreshold | null>(null); const [selectedThreshold, setSelectedThreshold] = useState<ServerThreshold | null>(null);
const [loadingAlertConfigs, setLoadingAlertConfigs] = useState(false); const [loadingAlertConfigs, setLoadingAlertConfigs] = useState(false);
const [loadingTemplates, setLoadingTemplates] = useState(false); const [loadingTemplates, setLoadingTemplates] = useState(false);
@@ -78,7 +79,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
// Initialize form data when server changes // Initialize form data when server changes
useEffect(() => { useEffect(() => {
if (server) { if (server) {
// console.log("Setting form data for server:", server); // console.log("Setting form data for server:", server);
// Parse comma-separated notification_id into array // Parse comma-separated notification_id into array
const notificationChannels = server.notification_id const notificationChannels = server.notification_id
? server.notification_id.split(',').map(id => id.trim()).filter(id => id) ? server.notification_id.split(',').map(id => id.trim()).filter(id => id)
@@ -180,8 +181,9 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const loadTemplates = async () => { const loadTemplates = async () => {
try { try {
setLoadingTemplates(true); setLoadingTemplates(true);
const templateList = await templateService.getTemplates(); const templateList = await templateService.getTemplates('server');
setTemplates(templateList); // Cast to ServerNotificationTemplate[] since we know we're getting server templates
setTemplates(templateList as ServerNotificationTemplate[]);
} catch (error) { } catch (error) {
// console.error('Error loading templates:', error); // console.error('Error loading templates:', error);
toast({ toast({
@@ -621,27 +623,39 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<div className="grid grid-cols-1 gap-3 text-sm"> <div className="grid grid-cols-1 gap-3 text-sm">
<div> <div>
<Label className="text-xs font-medium text-muted-foreground">RAM Threshold Message</Label> <Label className="text-xs font-medium text-muted-foreground">RAM Message</Label>
<p className="text-sm bg-background p-2 rounded border"> <p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.up_message || "No RAM threshold message defined"} {selectedTemplate.ram_message || "No RAM message defined"}
</p> </p>
</div> </div>
<div> <div>
<Label className="text-xs font-medium text-muted-foreground">CPU Threshold Message</Label> <Label className="text-xs font-medium text-muted-foreground">CPU Message</Label>
<p className="text-sm bg-background p-2 rounded border"> <p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.down_message || "No CPU threshold message defined"} {selectedTemplate.cpu_message || "No CPU message defined"}
</p> </p>
</div> </div>
<div> <div>
<Label className="text-xs font-medium text-muted-foreground">Disk Threshold Message</Label> <Label className="text-xs font-medium text-muted-foreground">Disk Message</Label>
<p className="text-sm bg-background p-2 rounded border"> <p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.incident_message || "No disk threshold message defined"} {selectedTemplate.disk_message || "No disk message defined"}
</p> </p>
</div> </div>
<div> <div>
<Label className="text-xs font-medium text-muted-foreground">Network Threshold Message</Label> <Label className="text-xs font-medium text-muted-foreground">Network Message</Label>
<p className="text-sm bg-background p-2 rounded border"> <p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.maintenance_message || "No network threshold message defined"} {selectedTemplate.network_message || "No network message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">Up Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.up_message || "No up message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">Down Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.down_message || "No down message defined"}
</p> </p>
</div> </div>
</div> </div>
@@ -25,7 +25,7 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
onDialogClose, onDialogClose,
}) => { }) => {
const getManualInstallSteps = () => { const getManualInstallSteps = () => {
const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh"; const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
return [ return [
{ {
@@ -27,7 +27,7 @@ export const OneClickInstallTab: React.FC<OneClickInstallTabProps> = ({
onDialogClose, onDialogClose,
}) => { }) => {
const getOneClickInstallCommand = () => { const getOneClickInstallCommand = () => {
const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh"; const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
return `curl -L -o server-agent.sh "${scriptUrl}" return `curl -L -o server-agent.sh "${scriptUrl}"
chmod +x server-agent.sh chmod +x server-agent.sh
@@ -125,7 +125,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
} else { } else {
// Fetch regional agent specific data // Fetch regional agent specific data
const [regionName, agentId] = currentAgent.split("|"); const [regionName, agentId] = currentAgent.split("|");
console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`); // console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`);
history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId); history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId);
// console.log(`Retrieved ${history.length} regional monitoring records`); // console.log(`Retrieved ${history.length} regional monitoring records`);
} }
@@ -5,7 +5,6 @@ import { Play, Pause } from "lucide-react";
import { Service } from "@/types/service.types"; import { Service } from "@/types/service.types";
import { serviceService } from "@/services/serviceService"; import { serviceService } from "@/services/serviceService";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { notificationService } from "@/services/notificationService";
interface ServiceMonitoringButtonProps { interface ServiceMonitoringButtonProps {
service: Service; service: Service;
@@ -34,16 +33,8 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni
if (onStatusChange) onStatusChange("paused"); if (onStatusChange) onStatusChange("paused");
// Send notification for paused status (only here, not in pauseMonitoring.ts) // Notification handling removed - will be handled by backend
if (service.alerts !== "muted") { // console.log("Service paused - notifications will be handled by backend");
// console.log("Sending pause notification from UI component");
// IMPORTANT: Direct call to the notification service to ensure a message is sent
await notificationService.sendNotification({
service: service,
status: "paused",
timestamp: new Date().toISOString(),
});
}
toast({ toast({
title: "Monitoring paused", title: "Monitoring paused",
@@ -51,7 +42,7 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni
}); });
} else { } else {
// Start/resume monitoring // Start/resume monitoring
// console.log(`Starting monitoring for service ${service.id} (${service.name})`); // console.log(`Starting monitoring for service ${service.id} (${service.name})`);
// First ensure we update the status in the database to not be paused anymore // First ensure we update the status in the database to not be paused anymore
await serviceService.resumeMonitoring(service.id); await serviceService.resumeMonitoring(service.id);
@@ -1,9 +1,10 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { templateService } from "@/services/templateService"; import { templateService, TemplateType } from "@/services/templateService";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Plus, RefreshCcw } from "lucide-react"; import { Plus, RefreshCcw } from "lucide-react";
import { TemplateList } from "./TemplateList"; import { TemplateList } from "./TemplateList";
import { TemplateDialog } from "./TemplateDialog"; import { TemplateDialog } from "./TemplateDialog";
@@ -11,8 +12,10 @@ import { useToast } from "@/hooks/use-toast";
export const AlertsTemplates = () => { export const AlertsTemplates = () => {
const { toast } = useToast(); const { toast } = useToast();
const [activeTab, setActiveTab] = useState<TemplateType>('service');
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<string | null>(null); const [editingTemplate, setEditingTemplate] = useState<string | null>(null);
const [editingTemplateType, setEditingTemplateType] = useState<TemplateType | null>(null);
const { const {
data: templates = [], data: templates = [],
@@ -20,17 +23,19 @@ export const AlertsTemplates = () => {
error, error,
refetch refetch
} = useQuery({ } = useQuery({
queryKey: ['notification_templates'], queryKey: ['notification_templates', activeTab],
queryFn: templateService.getTemplates, queryFn: () => templateService.getTemplates(activeTab),
}); });
const handleAddTemplate = () => { const handleAddTemplate = (templateType: TemplateType) => {
setEditingTemplate(null); setEditingTemplate(null);
setEditingTemplateType(templateType);
setIsDialogOpen(true); setIsDialogOpen(true);
}; };
const handleEditTemplate = (id: string) => { const handleEditTemplate = (id: string, templateType: TemplateType) => {
setEditingTemplate(id); setEditingTemplate(id);
setEditingTemplateType(templateType);
setIsDialogOpen(true); setIsDialogOpen(true);
}; };
@@ -51,33 +56,83 @@ export const AlertsTemplates = () => {
<RefreshCcw className="h-4 w-4 mr-2" /> <RefreshCcw className="h-4 w-4 mr-2" />
Refresh Refresh
</Button> </Button>
<Button onClick={handleAddTemplate}> <Button onClick={() => handleAddTemplate(activeTab)}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Add Template Add Template
</Button> </Button>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{error ? ( <Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TemplateType)}>
<div className="text-center p-6"> <TabsList className="grid w-full grid-cols-3">
<p className="text-destructive mb-4">Error loading templates</p> <TabsTrigger value="service">Service Uptime</TabsTrigger>
<Button variant="outline" onClick={() => refetch()}> <TabsTrigger value="server">Server Monitoring</TabsTrigger>
Try Again <TabsTrigger value="ssl">SSL Certificate</TabsTrigger>
</Button> </TabsList>
</div>
) : ( <TabsContent value="service" className="mt-4">
<TemplateList {error ? (
templates={templates} <div className="text-center p-6">
isLoading={isLoading} <p className="text-destructive mb-4">Error loading service templates</p>
onEdit={handleEditTemplate} <Button variant="outline" onClick={() => refetch()}>
refetchTemplates={refetch} Try Again
/> </Button>
)} </div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={(id) => handleEditTemplate(id, 'service')}
refetchTemplates={refetch}
templateType="service"
/>
)}
</TabsContent>
<TabsContent value="server" className="mt-4">
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading server templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={(id) => handleEditTemplate(id, 'server')}
refetchTemplates={refetch}
templateType="server"
/>
)}
</TabsContent>
<TabsContent value="ssl" className="mt-4">
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading SSL templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={(id) => handleEditTemplate(id, 'ssl')}
refetchTemplates={refetch}
templateType="ssl"
/>
)}
</TabsContent>
</Tabs>
</CardContent> </CardContent>
<TemplateDialog <TemplateDialog
open={isDialogOpen} open={isDialogOpen}
templateId={editingTemplate} templateId={editingTemplate}
templateType={editingTemplateType}
onOpenChange={setIsDialogOpen} onOpenChange={setIsDialogOpen}
onSuccess={() => { onSuccess={() => {
refetch(); refetch();
@@ -1,19 +1,25 @@
import React, { useEffect } from "react"; import React, { useEffect, useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form"; import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useTemplateForm } from "./hooks/useTemplateForm"; import { useTemplateForm } from "./hooks/useTemplateForm";
import { BasicTemplateFields } from "./form/BasicTemplateFields"; import { ServerTemplateFields } from "./form/ServerTemplateFields";
import { MessagesTabContent } from "./form/MessagesTabContent"; import { ServiceTemplateFields } from "./form/ServiceTemplateFields";
import { PlaceholdersTabContent } from "./form/PlaceholdersTabContent"; import { SslTemplateFields } from "./form/SslTemplateFields";
import { Loader2, ChevronDown } from "lucide-react"; import { Loader2, ChevronDown } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { TemplateType, templateTypeConfigs } from "@/services/templateService";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
interface TemplateDialogProps { interface TemplateDialogProps {
open: boolean; open: boolean;
templateId: string | null; templateId: string | null;
templateType: TemplateType | null;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onSuccess: () => void; onSuccess: () => void;
} }
@@ -21,9 +27,12 @@ interface TemplateDialogProps {
export const TemplateDialog: React.FC<TemplateDialogProps> = ({ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
open, open,
templateId, templateId,
templateType: initialTemplateType,
onOpenChange, onOpenChange,
onSuccess, onSuccess,
}) => { }) => {
const [selectedTemplateType, setSelectedTemplateType] = useState<TemplateType>(initialTemplateType || 'service');
const { const {
form, form,
isEditMode, isEditMode,
@@ -32,28 +41,70 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
onSubmit onSubmit
} = useTemplateForm({ } = useTemplateForm({
templateId, templateId,
templateType: selectedTemplateType,
open, open,
onOpenChange, onOpenChange,
onSuccess onSuccess
}); });
// For debugging purposes // Update template type when prop changes or dialog opens
useEffect(() => { useEffect(() => {
if (open) { if (initialTemplateType) {
// console.log("Template dialog opened. Edit mode:", isEditMode, "Template ID:", templateId); setSelectedTemplateType(initialTemplateType);
} else if (open && !isEditMode) {
// Log form values when they change setSelectedTemplateType('service');
const subscription = form.watch((value) => {
// console.log("Current form values:", value);
});
return () => subscription.unsubscribe();
} }
}, [open, isEditMode, templateId, form]); }, [initialTemplateType, open, isEditMode]);
// Handle template type change
const handleTemplateTypeChange = (newType: TemplateType) => {
if (!isEditMode) {
setSelectedTemplateType(newType);
form.setValue('templateType', newType);
}
};
const renderTemplateFields = () => {
switch (selectedTemplateType) {
case 'server':
return <ServerTemplateFields control={form.control} />;
case 'service':
return <ServiceTemplateFields control={form.control} />;
case 'ssl':
return <SslTemplateFields control={form.control} />;
default:
return null;
}
};
const renderPlaceholderGuide = () => {
const config = templateTypeConfigs[selectedTemplateType];
if (!config) return null;
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Available Placeholders</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-3">
{config.description}. Use these placeholders in your messages:
</p>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-sm">
{config.placeholders.map((placeholder) => (
<div key={placeholder} className="bg-muted/30 p-2 rounded">
<code className="text-xs">{placeholder}</code>
</div>
))}
</div>
</CardContent>
</Card>
);
};
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col overflow-hidden"> <DialogContent className="max-w-4xl max-h-[90vh] flex flex-col overflow-hidden">
<DialogHeader> <DialogHeader>
<DialogTitle>{isEditMode ? "Edit Template" : "Add Template"}</DialogTitle> <DialogTitle>{isEditMode ? "Edit Template" : "Add Template"}</DialogTitle>
</DialogHeader> </DialogHeader>
@@ -69,7 +120,69 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
<div className="relative flex-1"> <div className="relative flex-1">
<ScrollArea className="pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}> <ScrollArea className="pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}>
<div className="space-y-6 pb-6 pr-4"> <div className="space-y-6 pb-6 pr-4">
<BasicTemplateFields control={form.control} /> {/* Basic Fields */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Template Name</FormLabel>
<FormControl>
<Input
placeholder="Enter template name"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="templateType"
render={({ field }) => (
<FormItem>
<FormLabel>Template Type</FormLabel>
<FormControl>
<Select
onValueChange={handleTemplateTypeChange}
value={selectedTemplateType}
disabled={isEditMode}
>
<SelectTrigger>
<SelectValue placeholder="Select template type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="server">Server Monitoring</SelectItem>
<SelectItem value="service">Service Uptime</SelectItem>
<SelectItem value="ssl">SSL Certificate</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Placeholder</FormLabel>
<FormControl>
<Input
placeholder="Optional custom placeholder"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Tabs defaultValue="messages"> <Tabs defaultValue="messages">
<TabsList className="grid w-full grid-cols-2"> <TabsList className="grid w-full grid-cols-2">
@@ -78,11 +191,11 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
</TabsList> </TabsList>
<TabsContent value="messages" className="pt-4"> <TabsContent value="messages" className="pt-4">
<MessagesTabContent control={form.control} /> {renderTemplateFields()}
</TabsContent> </TabsContent>
<TabsContent value="placeholders" className="pt-4"> <TabsContent value="placeholders" className="pt-4">
<PlaceholdersTabContent control={form.control} /> {renderPlaceholderGuide()}
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>
@@ -1,16 +1,14 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { NotificationTemplate, templateService } from "@/services/templateService";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Edit, Trash2 } from "lucide-react"; import { Badge } from "@/components/ui/badge";
import { Trash2, Edit, MoreVertical } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -22,13 +20,15 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { Badge } from "@/components/ui/badge"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { templateService, AnyTemplate, TemplateType } from "@/services/templateService";
interface TemplateListProps { interface TemplateListProps {
templates: NotificationTemplate[]; templates: AnyTemplate[];
isLoading: boolean; isLoading: boolean;
onEdit: (id: string) => void; onEdit: (id: string) => void;
refetchTemplates: () => void; refetchTemplates: () => void;
templateType: TemplateType;
} }
export const TemplateList: React.FC<TemplateListProps> = ({ export const TemplateList: React.FC<TemplateListProps> = ({
@@ -36,49 +36,59 @@ export const TemplateList: React.FC<TemplateListProps> = ({
isLoading, isLoading,
onEdit, onEdit,
refetchTemplates, refetchTemplates,
templateType
}) => { }) => {
const { toast } = useToast(); const { toast } = useToast();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const queryClient = useQueryClient();
const [templateToDelete, setTemplateToDelete] = useState<NotificationTemplate | null>(null); const [deleteTemplateId, setDeleteTemplateId] = useState<string | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const handleDeletePrompt = (template: NotificationTemplate) => { // Delete mutation
setTemplateToDelete(template); const deleteMutation = useMutation({
setDeleteDialogOpen(true); mutationFn: (id: string) => templateService.deleteTemplate(id, templateType),
}; onSuccess: () => {
const handleDeleteTemplate = async () => {
if (!templateToDelete) return;
setIsDeleting(true);
try {
await templateService.deleteTemplate(templateToDelete.id);
toast({ toast({
title: "Template deleted", title: "Template deleted",
description: `Template "${templateToDelete.name}" has been removed.`, description: "The template has been deleted successfully.",
}); });
queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] });
refetchTemplates(); refetchTemplates();
} catch (error) { },
console.error("Error deleting template:", error); onError: (error) => {
// console.error("Error deleting template:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to delete template. Please try again.", description: "Failed to delete template. Please try again.",
variant: "destructive", variant: "destructive",
}); });
} finally { },
setIsDeleting(false); });
setDeleteDialogOpen(false);
setTemplateToDelete(null); const handleDelete = (id: string) => {
setDeleteTemplateId(id);
};
const confirmDelete = () => {
if (deleteTemplateId) {
deleteMutation.mutate(deleteTemplateId);
setDeleteTemplateId(null);
} }
}; };
if (isLoading) { if (isLoading) {
return ( return (
<div className="py-8 flex justify-center"> <div className="space-y-4">
<div className="animate-pulse flex flex-col items-center"> {[...Array(3)].map((_, i) => (
<div className="h-4 bg-gray-200 rounded w-32 mb-4"></div> <div key={i} className="flex items-center justify-between p-4 border border-border rounded-lg">
<div className="h-4 bg-gray-200 rounded w-64"></div> <div className="space-y-2">
</div> <div className="h-4 bg-muted animate-pulse rounded w-32"></div>
<div className="h-3 bg-muted animate-pulse rounded w-48"></div>
</div>
<div className="flex space-x-2">
<div className="h-8 w-8 bg-muted animate-pulse rounded"></div>
<div className="h-8 w-8 bg-muted animate-pulse rounded"></div>
</div>
</div>
))}
</div> </div>
); );
} }
@@ -86,79 +96,82 @@ export const TemplateList: React.FC<TemplateListProps> = ({
if (templates.length === 0) { if (templates.length === 0) {
return ( return (
<div className="text-center py-8"> <div className="text-center py-8">
<p className="text-muted-foreground mb-2">No templates found</p> <p className="text-muted-foreground">No templates found. Create your first template to get started.</p>
<p className="text-sm text-muted-foreground">
Create your first notification template to get started.
</p>
</div> </div>
); );
} }
const getTemplateTypeLabel = (type: TemplateType) => {
switch (type) {
case 'server': return 'Server';
case 'service': return 'Service';
case 'ssl': return 'SSL';
default: return 'Unknown';
}
};
return ( return (
<> <>
<div className="border rounded-md overflow-hidden"> <div className="space-y-4">
<Table> {templates.map((template) => (
<TableHeader> <div key={template.id} className="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors">
<TableRow> <div className="space-y-1">
<TableHead>Name</TableHead> <div className="flex items-center gap-2">
<TableHead>Type</TableHead> <h3 className="font-medium">{template.name}</h3>
<TableHead>Created</TableHead> <Badge variant="outline">{getTemplateTypeLabel(templateType)}</Badge>
<TableHead className="w-24">Actions</TableHead> </div>
</TableRow> <p className="text-sm text-muted-foreground">
</TableHeader> Created: {new Date(template.created).toLocaleDateString()}
<TableBody> {template.updated !== template.created &&
{templates.map((template) => ( ` • Updated: ${new Date(template.updated).toLocaleDateString()}`
<TableRow key={template.id}> }
<TableCell className="font-medium">{template.name}</TableCell> </p>
<TableCell> </div>
<Badge variant="outline">{template.type}</Badge> <div className="flex items-center space-x-2">
</TableCell> <Button
<TableCell> variant="outline"
{new Date(template.created).toLocaleDateString()} size="sm"
</TableCell> onClick={() => onEdit(template.id)}
<TableCell> >
<div className="flex space-x-2"> <Edit className="h-4 w-4 mr-1" />
<Button Edit
size="sm" </Button>
variant="ghost" <DropdownMenu>
onClick={() => onEdit(template.id)} <DropdownMenuTrigger asChild>
> <Button variant="outline" size="sm">
<Edit className="h-4 w-4" /> <MoreVertical className="h-4 w-4" />
</Button> </Button>
<Button </DropdownMenuTrigger>
size="sm" <DropdownMenuContent align="end">
variant="ghost" <DropdownMenuItem
onClick={() => handleDeletePrompt(template)} onClick={() => handleDelete(template.id)}
> className="text-destructive focus:text-destructive"
<Trash2 className="h-4 w-4 text-destructive" /> >
</Button> <Trash2 className="h-4 w-4 mr-2" />
</div> Delete
</TableCell> </DropdownMenuItem>
</TableRow> </DropdownMenuContent>
))} </DropdownMenu>
</TableBody> </div>
</Table> </div>
))}
</div> </div>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={!!deleteTemplateId} onOpenChange={() => setDeleteTemplateId(null)}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete Template</AlertDialogTitle> <AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Are you sure you want to delete the template "{templateToDelete?.name}"? This action cannot be undone. This action cannot be undone. This will permanently delete the template.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={(e) => { onClick={confirmDelete}
e.preventDefault();
handleDeleteTemplate();
}}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
> >
{isDeleting ? "Deleting..." : "Delete"} Delete
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@@ -0,0 +1,211 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { Control } from "react-hook-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface ServerTemplateFieldsProps {
control: Control<any>;
}
export const ServerTemplateFields: React.FC<ServerTemplateFieldsProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">System Resource Messages</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="cpu_message"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Alert Message</FormLabel>
<FormControl>
<Textarea
placeholder="CPU usage on ${server_name} is ${cpu_usage}% (threshold: ${threshold}%)"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="ram_message"
render={({ field }) => (
<FormItem>
<FormLabel>RAM Alert Message</FormLabel>
<FormControl>
<Textarea
placeholder="Memory usage on ${server_name} is ${ram_usage}% (threshold: ${threshold}%)"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="disk_message"
render={({ field }) => (
<FormItem>
<FormLabel>Disk Alert Message</FormLabel>
<FormControl>
<Textarea
placeholder="Disk usage on ${server_name} is ${disk_usage}% (threshold: ${threshold}%)"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="network_message"
render={({ field }) => (
<FormItem>
<FormLabel>Network Alert Message</FormLabel>
<FormControl>
<Textarea
placeholder="Network usage on ${server_name} is ${network_usage}% (threshold: ${threshold}%)"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="up_message"
render={({ field }) => (
<FormItem>
<FormLabel>Server Up Message</FormLabel>
<FormControl>
<Textarea
placeholder="Server ${server_name} is UP and responding"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="down_message"
render={({ field }) => (
<FormItem>
<FormLabel>Server Down Message</FormLabel>
<FormControl>
<Textarea
placeholder="Server ${server_name} is DOWN"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="warning_message"
render={({ field }) => (
<FormItem>
<FormLabel>Warning Message</FormLabel>
<FormControl>
<Textarea
placeholder="Warning: Server ${server_name} requires attention"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="paused_message"
render={({ field }) => (
<FormItem>
<FormLabel>Paused Message</FormLabel>
<FormControl>
<Textarea
placeholder="Monitoring for server ${server_name} is paused"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="cpu_temp_message"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Temperature Message</FormLabel>
<FormControl>
<Textarea
placeholder="CPU temperature on ${server_name} is ${cpu_temp}°C"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="disk_io_message"
render={({ field }) => (
<FormItem>
<FormLabel>Disk I/O Message</FormLabel>
<FormControl>
<Textarea
placeholder="Disk I/O on ${server_name} is ${disk_io} MB/s"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,133 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { Control } from "react-hook-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface ServiceTemplateFieldsProps {
control: Control<any>;
}
export const ServiceTemplateFields: React.FC<ServiceTemplateFieldsProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Service Status Messages</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="up_message"
render={({ field }) => (
<FormItem>
<FormLabel>Service Up Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} is UP. Response time: ${response_time}ms"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="down_message"
render={({ field }) => (
<FormItem>
<FormLabel>Service Down Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} is DOWN. Status: ${status}"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="maintenance_message"
render={({ field }) => (
<FormItem>
<FormLabel>Maintenance Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} is under maintenance"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="incident_message"
render={({ field }) => (
<FormItem>
<FormLabel>Incident Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} has an incident"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="resolved_message"
render={({ field }) => (
<FormItem>
<FormLabel>Resolved Message</FormLabel>
<FormControl>
<Textarea
placeholder="Issue with service ${service_name} has been resolved"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="warning_message"
render={({ field }) => (
<FormItem>
<FormLabel>Warning Message</FormLabel>
<FormControl>
<Textarea
placeholder="Warning: Service ${service_name} response time is high"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,79 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { Control } from "react-hook-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface SslTemplateFieldsProps {
control: Control<any>;
}
export const SslTemplateFields: React.FC<SslTemplateFieldsProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">SSL Certificate Messages</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<FormField
control={control}
name="expired"
render={({ field }) => (
<FormItem>
<FormLabel>Certificate Expired Message</FormLabel>
<FormControl>
<Textarea
placeholder="SSL certificate for ${domain} has EXPIRED on ${expiry_date}"
className="min-h-24"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="exiring_soon"
render={({ field }) => (
<FormItem>
<FormLabel>Certificate Expiring Soon Message</FormLabel>
<FormControl>
<Textarea
placeholder="SSL certificate for ${domain} will expire in ${days_left} days on ${expiry_date}"
className="min-h-24"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="warning"
render={({ field }) => (
<FormItem>
<FormLabel>Certificate Warning Message</FormLabel>
<FormControl>
<Textarea
placeholder="Warning: SSL certificate for ${domain} requires attention"
className="min-h-24"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -4,69 +4,143 @@ import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod"; import * as z from "zod";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { templateService, CreateUpdateTemplateData } from "@/services/templateService"; import { templateService, TemplateType, AnyTemplateData } from "@/services/templateService";
import { useEffect } from "react"; import { useEffect } from "react";
// Template form schema // Base schema
export const templateFormSchema = z.object({ const baseSchema = {
name: z.string().min(2, "Name is required and must be at least 2 characters"), name: z.string().min(2, "Name is required and must be at least 2 characters"),
type: z.string().min(1, "Type is required"), templateType: z.enum(['server', 'service', 'ssl'] as const),
placeholder: z.string().optional(),
};
// Server template schema
const serverTemplateSchema = z.object({
...baseSchema,
templateType: z.literal('server'),
ram_message: z.string().min(1, "RAM message is required"),
cpu_message: z.string().min(1, "CPU message is required"),
disk_message: z.string().min(1, "Disk message is required"),
network_message: z.string().min(1, "Network message is required"),
up_message: z.string().min(1, "Up message is required"),
down_message: z.string().min(1, "Down message is required"),
notification_id: z.string().optional(),
warning_message: z.string().min(1, "Warning message is required"),
paused_message: z.string().min(1, "Paused message is required"),
cpu_temp_message: z.string().min(1, "CPU temperature message is required"),
disk_io_message: z.string().min(1, "Disk I/O message is required"),
});
// Service template schema
const serviceTemplateSchema = z.object({
...baseSchema,
templateType: z.literal('service'),
up_message: z.string().min(1, "Up message is required"), up_message: z.string().min(1, "Up message is required"),
down_message: z.string().min(1, "Down message is required"), down_message: z.string().min(1, "Down message is required"),
maintenance_message: z.string().min(1, "Maintenance message is required"), maintenance_message: z.string().min(1, "Maintenance message is required"),
incident_message: z.string().min(1, "Incident message is required"), incident_message: z.string().min(1, "Incident message is required"),
resolved_message: z.string().min(1, "Resolved message is required"), resolved_message: z.string().min(1, "Resolved message is required"),
service_name_placeholder: z.string().min(1, "Service name placeholder is required"), warning_message: z.string().min(1, "Warning message is required"),
response_time_placeholder: z.string().min(1, "Response time placeholder is required"),
status_placeholder: z.string().min(1, "Status placeholder is required"),
threshold_placeholder: z.string().min(1, "Threshold placeholder is required"),
}); });
// Define the form data type from the schema // SSL template schema
const sslTemplateSchema = z.object({
...baseSchema,
templateType: z.literal('ssl'),
expired: z.string().min(1, "Expired message is required"),
exiring_soon: z.string().min(1, "Expiring soon message is required"),
warning: z.string().min(1, "Warning message is required"),
});
// Combined schema
export const templateFormSchema = z.discriminatedUnion("templateType", [
serverTemplateSchema,
serviceTemplateSchema,
sslTemplateSchema,
]);
export type TemplateFormData = z.infer<typeof templateFormSchema>; export type TemplateFormData = z.infer<typeof templateFormSchema>;
// Default form values // Default form values for each template type
const defaultFormValues: TemplateFormData = { const getDefaultValues = (templateType: TemplateType): TemplateFormData => {
name: "", const base = {
type: "default", name: "",
up_message: "Service ${service_name} is UP. Response time: ${response_time}ms", templateType,
down_message: "Service ${service_name} is DOWN. Status: ${status}", placeholder: "",
maintenance_message: "Service ${service_name} is under maintenance", };
incident_message: "Service ${service_name} has an incident",
resolved_message: "Service ${service_name} issue has been resolved", switch (templateType) {
service_name_placeholder: "${service_name}", case 'server':
response_time_placeholder: "${response_time}", return {
status_placeholder: "${status}", ...base,
threshold_placeholder: "${threshold}", templateType: 'server' as const,
ram_message: "Memory usage on ${server_name} is ${ram_usage}% (threshold: ${threshold}%)",
cpu_message: "CPU usage on ${server_name} is ${cpu_usage}% (threshold: ${threshold}%)",
disk_message: "Disk usage on ${server_name} is ${disk_usage}% (threshold: ${threshold}%)",
network_message: "Network usage on ${server_name} is ${network_usage}% (threshold: ${threshold}%)",
up_message: "Server ${server_name} is UP and responding",
down_message: "Server ${server_name} is DOWN",
notification_id: "",
warning_message: "Warning: Server ${server_name} requires attention",
paused_message: "Monitoring for server ${server_name} is paused",
cpu_temp_message: "CPU temperature on ${server_name} is ${cpu_temp}°C",
disk_io_message: "Disk I/O on ${server_name} is ${disk_io} MB/s",
};
case 'service':
return {
...base,
templateType: 'service' as const,
up_message: "Service ${service_name} is UP. Response time: ${response_time}ms",
down_message: "Service ${service_name} is DOWN. Status: ${status}",
maintenance_message: "Service ${service_name} is under maintenance",
incident_message: "Service ${service_name} has an incident",
resolved_message: "Issue with service ${service_name} has been resolved",
warning_message: "Warning: Service ${service_name} response time is high",
};
case 'ssl':
return {
...base,
templateType: 'ssl' as const,
expired: "SSL certificate for ${domain} has EXPIRED on ${expiry_date}",
exiring_soon: "SSL certificate for ${domain} will expire in ${days_left} days on ${expiry_date}",
warning: "Warning: SSL certificate for ${domain} requires attention",
};
default:
throw new Error(`Unknown template type: ${templateType}`);
}
}; };
export interface UseTemplateFormProps { export interface UseTemplateFormProps {
templateId: string | null; templateId: string | null;
templateType: TemplateType;
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onSuccess: () => void; onSuccess: () => void;
} }
export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: UseTemplateFormProps) => { export const useTemplateForm = ({ templateId, templateType, open, onOpenChange, onSuccess }: UseTemplateFormProps) => {
const { toast } = useToast(); const { toast } = useToast();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isEditMode = !!templateId; const isEditMode = !!templateId;
// console.log("Template form initialized with templateId:", templateId, "isEditMode:", isEditMode); // console.log("Template form initialized with templateId:", templateId, "templateType:", templateType, "isEditMode:", isEditMode);
const form = useForm<TemplateFormData>({ const form = useForm<TemplateFormData>({
resolver: zodResolver(templateFormSchema), resolver: zodResolver(templateFormSchema),
defaultValues: defaultFormValues, defaultValues: getDefaultValues(templateType),
mode: "onChange" mode: "onChange"
}); });
// Query to fetch template data for editing // Query to fetch template data for editing
const { isLoading: isLoadingTemplate, data: templateData } = useQuery({ const { isLoading: isLoadingTemplate, data: templateData } = useQuery({
queryKey: ['template', templateId], queryKey: ['template', templateId, templateType],
queryFn: () => { queryFn: () => {
if (!templateId) return null; if (!templateId) return null;
console.log("Fetching template data for ID:", templateId); // console.log("Fetching template data for ID:", templateId, "type:", templateType);
return templateService.getTemplate(templateId); return templateService.getTemplate(templateId, templateType);
}, },
enabled: !!templateId && open, enabled: !!templateId && open,
}); });
@@ -76,47 +150,32 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
if (templateData && open) { if (templateData && open) {
// console.log("Setting form values with template data:", templateData); // console.log("Setting form values with template data:", templateData);
form.reset({ const formData: any = {
name: templateData.name || "", name: templateData.name || "",
type: templateData.type || "default", templateType: templateType,
up_message: templateData.up_message || "", placeholder: (templateData as any).placeholder || "",
down_message: templateData.down_message || "", };
maintenance_message: templateData.maintenance_message || "",
incident_message: templateData.incident_message || "", // Add template-specific fields
resolved_message: templateData.resolved_message || "", Object.keys(templateData).forEach(key => {
service_name_placeholder: templateData.service_name_placeholder || "${service_name}", if (!['id', 'collectionId', 'collectionName', 'created', 'updated', 'name'].includes(key)) {
response_time_placeholder: templateData.response_time_placeholder || "${response_time}", formData[key] = (templateData as any)[key] || "";
status_placeholder: templateData.status_placeholder || "${status}", }
threshold_placeholder: templateData.threshold_placeholder || "${threshold}",
}); });
form.reset(formData);
} }
}, [templateData, open, form]); }, [templateData, open, form, templateType]);
// Handle form errors
useEffect(() => {
const subscription = form.formState.errors.root?.message &&
toast({
title: "Error",
description: form.formState.errors.root.message,
variant: "destructive",
});
return () => {
if (subscription) {
// Clean up if needed
}
};
}, [form.formState.errors.root, toast]);
// Create mutation // Create mutation
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: (data: CreateUpdateTemplateData) => templateService.createTemplate(data), mutationFn: (data: AnyTemplateData) => templateService.createTemplate(data, templateType),
onSuccess: () => { onSuccess: () => {
toast({ toast({
title: "Template created", title: "Template created",
description: "Your notification template has been created successfully.", description: "Your notification template has been created successfully.",
}); });
queryClient.invalidateQueries({ queryKey: ['notification_templates'] }); queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] });
onSuccess(); onSuccess();
}, },
onError: (error) => { onError: (error) => {
@@ -131,14 +190,14 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
// Update mutation // Update mutation
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: CreateUpdateTemplateData }) => mutationFn: ({ id, data }: { id: string; data: AnyTemplateData }) =>
templateService.updateTemplate(id, data), templateService.updateTemplate(id, data, templateType),
onSuccess: () => { onSuccess: () => {
toast({ toast({
title: "Template updated", title: "Template updated",
description: "Your notification template has been updated successfully.", description: "Your notification template has been updated successfully.",
}); });
queryClient.invalidateQueries({ queryKey: ['notification_templates'] }); queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] });
onSuccess(); onSuccess();
}, },
onError: (error) => { onError: (error) => {
@@ -157,20 +216,9 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
const onSubmit = (formData: TemplateFormData) => { const onSubmit = (formData: TemplateFormData) => {
// console.log("Submitting form data:", formData); // console.log("Submitting form data:", formData);
// Ensure all required fields are present // Remove templateType from the data before sending to API
const completeData: CreateUpdateTemplateData = { const { templateType: _, ...templateDataWithoutType } = formData;
name: formData.name, const completeData = templateDataWithoutType as AnyTemplateData;
type: formData.type,
up_message: formData.up_message,
down_message: formData.down_message,
maintenance_message: formData.maintenance_message,
incident_message: formData.incident_message,
resolved_message: formData.resolved_message,
service_name_placeholder: formData.service_name_placeholder,
response_time_placeholder: formData.response_time_placeholder,
status_placeholder: formData.status_placeholder,
threshold_placeholder: formData.threshold_placeholder,
};
if (isEditMode && templateId) { if (isEditMode && templateId) {
// console.log("Updating template with ID:", templateId); // console.log("Updating template with ID:", templateId);
@@ -181,13 +229,13 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
} }
}; };
// Reset form when dialog closes // Reset form when dialog closes or template type changes
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
// console.log("Dialog closed, resetting form"); // console.log("Dialog closed, resetting form");
form.reset(defaultFormValues); form.reset(getDefaultValues(templateType));
} }
}, [open, form]); }, [open, form, templateType]);
return { return {
form, form,
@@ -8,12 +8,14 @@ import {
DialogFooter DialogFooter
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { AlertConfiguration, alertConfigService } from "@/services/alertConfigService"; import { AlertConfiguration, alertConfigService } from "@/services/alertConfigService";
import { WebhookConfiguration, webhookService } from "@/services/webhookService";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { import {
Form, Form,
FormControl, FormControl,
@@ -24,7 +26,9 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Loader2 } from "lucide-react"; import { Loader2, Copy } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "@/hooks/use-toast";
interface NotificationChannelDialogProps { interface NotificationChannelDialogProps {
open: boolean; open: boolean;
@@ -36,7 +40,7 @@ const baseSchema = z.object({
notify_name: z.string().min(1, "Name is required"), notify_name: z.string().min(1, "Name is required"),
notification_type: z.enum(["telegram", "discord", "slack", "signal", "email"]), notification_type: z.enum(["telegram", "discord", "slack", "signal", "email"]),
enabled: z.boolean().default(true), enabled: z.boolean().default(true),
service_id: z.string().default("global"), // Assuming global for now, could be linked to specific services service_id: z.string().default("global"),
template_id: z.string().optional(), template_id: z.string().optional(),
}); });
@@ -63,7 +67,22 @@ const signalSchema = baseSchema.extend({
const emailSchema = baseSchema.extend({ const emailSchema = baseSchema.extend({
notification_type: z.literal("email"), notification_type: z.literal("email"),
// Email specific fields could be added here email_address: z.string().email("Valid email is required"),
email_sender_name: z.string().min(1, "Sender name is required"),
smtp_server: z.string().min(1, "SMTP server is required"),
smtp_port: z.string().min(1, "SMTP port is required"),
});
const webhookSchema = baseSchema.extend({
notification_type: z.literal("webhook"),
webhook_url: z.string().url("Must be a valid URL"),
webhook_method: z.enum(["GET", "POST", "PUT", "PATCH"]).default("POST"),
webhook_secret: z.string().optional(),
webhook_headers: z.string().optional(),
webhook_description: z.string().optional(),
webhook_payload_template: z.string().optional(),
webhook_retry_count: z.string().optional(),
webhook_trigger_events: z.string().optional(),
}); });
const formSchema = z.discriminatedUnion("notification_type", [ const formSchema = z.discriminatedUnion("notification_type", [
@@ -71,11 +90,68 @@ const formSchema = z.discriminatedUnion("notification_type", [
discordSchema, discordSchema,
slackSchema, slackSchema,
signalSchema, signalSchema,
emailSchema emailSchema,
webhookSchema
]); ]);
type FormValues = z.infer<typeof formSchema>; type FormValues = z.infer<typeof formSchema>;
const notificationTypeOptions = [
{ value: "telegram", label: "Telegram", description: "Send notifications via Telegram bot" },
{ value: "discord", label: "Discord", description: "Send notifications to Discord webhook" },
{ value: "slack", label: "Slack", description: "Send notifications to Slack webhook" },
{ value: "signal", label: "Signal", description: "Send notifications via Signal" },
{ value: "email", label: "Email", description: "Send notifications via email" },
{ value: "webhook", label: "Webhook", description: "Send notifications to custom webhook" },
];
const webhookPayloadTemplates = {
server: `{
"alert_type": "server",
"server_name": "\${server_name}",
"status": "\${status}",
"cpu_usage": "\${cpu_usage}",
"ram_usage": "\${ram_usage}",
"disk_usage": "\${disk_usage}",
"network_usage": "\${network_usage}",
"cpu_temp": "\${cpu_temp}",
"disk_io": "\${disk_io}",
"threshold": "\${threshold}",
"timestamp": "\${time}",
"message": "Server \${server_name} alert: \${status}"
}`,
service: `{
"alert_type": "service",
"service_name": "\${service_name}",
"status": "\${status}",
"response_time": "\${response_time}",
"url": "\${url}",
"uptime": "\${uptime}",
"downtime": "\${downtime}",
"timestamp": "\${time}",
"message": "Service \${service_name} is \${status}"
}`,
ssl: `{
"alert_type": "ssl",
"domain": "\${domain}",
"certificate_name": "\${certificate_name}",
"expiry_date": "\${expiry_date}",
"days_left": "\${days_left}",
"issuer": "\${issuer}",
"serial_number": "\${serial_number}",
"timestamp": "\${time}",
"message": "SSL certificate for \${domain} expires in \${days_left} days"
}`
};
const defaultPayloadTemplate = `{
"alert_type": "general",
"service_name": "\${service_name}",
"status": "\${status}",
"message": "\${message}",
"timestamp": "\${time}"
}`;
export const NotificationChannelDialog = ({ export const NotificationChannelDialog = ({
open, open,
onClose, onClose,
@@ -93,7 +169,7 @@ export const NotificationChannelDialog = ({
}, },
}); });
const { watch, reset } = form; const { watch, reset, setValue } = form;
const notificationType = watch("notification_type"); const notificationType = watch("notification_type");
const [isSubmitting, setIsSubmitting] = React.useState(false); const [isSubmitting, setIsSubmitting] = React.useState(false);
@@ -123,19 +199,54 @@ export const NotificationChannelDialog = ({
onClose(false); onClose(false);
}; };
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
toast({
title: "Copied",
description: "Template copied to clipboard",
});
};
const insertTemplate = (template: string) => {
setValue("webhook_payload_template", template);
};
const onSubmit = async (values: FormValues) => { const onSubmit = async (values: FormValues) => {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
// Ensure service_id is always present if (values.notification_type === "webhook") {
const configData = { // Handle webhook creation/update separately
...values, const webhookData: Omit<WebhookConfiguration, 'id' | 'collectionId' | 'collectionName' | 'created' | 'updated'> = {
service_id: values.service_id || "global", name: values.notify_name,
}; url: values.webhook_url,
enabled: values.enabled ? "on" : "off",
method: values.webhook_method || "POST",
secret: values.webhook_secret || "",
headers: values.webhook_headers || "",
description: values.webhook_description || "",
payload_template: values.webhook_payload_template || "",
retry_count: values.webhook_retry_count || "3",
trigger_events: values.webhook_trigger_events || "all",
user_id: "global", // or get current user id
};
if (isEditing && editingConfig?.id) { if (isEditing && editingConfig?.id) {
await alertConfigService.updateAlertConfiguration(editingConfig.id, configData); await webhookService.updateWebhook(editingConfig.id, webhookData);
} else {
await webhookService.createWebhook(webhookData);
}
} else { } else {
await alertConfigService.createAlertConfiguration(configData as any); // Handle other notification types with existing service
const configData = {
...values,
service_id: values.service_id || "global",
};
if (isEditing && editingConfig?.id) {
await alertConfigService.updateAlertConfiguration(editingConfig.id, configData);
} else {
await alertConfigService.createAlertConfiguration(configData as any);
}
} }
onClose(true); // Close with refresh onClose(true); // Close with refresh
} finally { } finally {
@@ -145,7 +256,7 @@ export const NotificationChannelDialog = ({
return ( return (
<Dialog open={open} onOpenChange={() => handleClose()}> <Dialog open={open} onOpenChange={() => handleClose()}>
<DialogContent className="sm:max-w-[500px]"> <DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{isEditing ? "Edit Notification Channel" : "Add Notification Channel"} {isEditing ? "Edit Notification Channel" : "Add Notification Channel"}
@@ -159,7 +270,7 @@ export const NotificationChannelDialog = ({
name="notify_name" name="notify_name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Channel Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="My Notification Channel" {...field} /> <Input placeholder="My Notification Channel" {...field} />
</FormControl> </FormControl>
@@ -175,57 +286,25 @@ export const NotificationChannelDialog = ({
control={form.control} control={form.control}
name="notification_type" name="notification_type"
render={({ field }) => ( render={({ field }) => (
<FormItem className="space-y-3"> <FormItem>
<FormLabel>Channel Type</FormLabel> <FormLabel>Channel Type</FormLabel>
<FormControl> <Select onValueChange={field.onChange} value={field.value}>
<RadioGroup <FormControl>
onValueChange={field.onChange} <SelectTrigger>
defaultValue={field.value} <SelectValue placeholder="Select notification type" />
value={field.value} </SelectTrigger>
className="flex flex-col space-y-1" </FormControl>
> <SelectContent>
<FormItem className="flex items-center space-x-3 space-y-0"> {notificationTypeOptions.map((option) => (
<FormControl> <SelectItem key={option.value} value={option.value}>
<RadioGroupItem value="telegram" /> <div className="flex flex-col">
</FormControl> <span className="font-medium">{option.label}</span>
<FormLabel className="font-normal"> <span className="text-xs text-muted-foreground">{option.description}</span>
Telegram </div>
</FormLabel> </SelectItem>
</FormItem> ))}
<FormItem className="flex items-center space-x-3 space-y-0"> </SelectContent>
<FormControl> </Select>
<RadioGroupItem value="discord" />
</FormControl>
<FormLabel className="font-normal">
Discord
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="slack" />
</FormControl>
<FormLabel className="font-normal">
Slack
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="signal" />
</FormControl>
<FormLabel className="font-normal">
Signal
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="email" />
</FormControl>
<FormLabel className="font-normal">
Email
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@@ -243,6 +322,9 @@ export const NotificationChannelDialog = ({
<FormControl> <FormControl>
<Input placeholder="Telegram Chat ID" {...field} /> <Input placeholder="Telegram Chat ID" {...field} />
</FormControl> </FormControl>
<FormDescription>
The Telegram chat ID to send notifications to
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@@ -256,6 +338,9 @@ export const NotificationChannelDialog = ({
<FormControl> <FormControl>
<Input placeholder="Telegram Bot Token" {...field} type="password" /> <Input placeholder="Telegram Bot Token" {...field} type="password" />
</FormControl> </FormControl>
<FormDescription>
Your Telegram bot token from @BotFather
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@@ -271,8 +356,11 @@ export const NotificationChannelDialog = ({
<FormItem> <FormItem>
<FormLabel>Webhook URL</FormLabel> <FormLabel>Webhook URL</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Discord Webhook URL" {...field} /> <Input placeholder="https://discord.com/api/webhooks/..." {...field} />
</FormControl> </FormControl>
<FormDescription>
Discord webhook URL from your server settings
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@@ -287,8 +375,11 @@ export const NotificationChannelDialog = ({
<FormItem> <FormItem>
<FormLabel>Webhook URL</FormLabel> <FormLabel>Webhook URL</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Slack Webhook URL" {...field} /> <Input placeholder="https://hooks.slack.com/services/..." {...field} />
</FormControl> </FormControl>
<FormDescription>
Slack incoming webhook URL
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@@ -305,12 +396,314 @@ export const NotificationChannelDialog = ({
<FormControl> <FormControl>
<Input placeholder="+1234567890" {...field} /> <Input placeholder="+1234567890" {...field} />
</FormControl> </FormControl>
<FormDescription>
Signal phone number to send notifications to
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
)} )}
{notificationType === "email" && (
<>
<FormField
control={form.control}
name="email_address"
render={({ field }) => (
<FormItem>
<FormLabel>Email Address</FormLabel>
<FormControl>
<Input placeholder="notifications@example.com" {...field} type="email" />
</FormControl>
<FormDescription>
Email address to send notifications to
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email_sender_name"
render={({ field }) => (
<FormItem>
<FormLabel>Sender Name</FormLabel>
<FormControl>
<Input placeholder="Alert System" {...field} />
</FormControl>
<FormDescription>
Display name for outgoing emails
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="smtp_server"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP Server</FormLabel>
<FormControl>
<Input placeholder="smtp.gmail.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtp_port"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP Port</FormLabel>
<FormControl>
<Input placeholder="587" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</>
)}
{notificationType === "webhook" && (
<>
<FormField
control={form.control}
name="webhook_url"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input placeholder="https://api.example.com/webhook" {...field} />
</FormControl>
<FormDescription>
The URL where webhook notifications will be sent
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="webhook_method"
render={({ field }) => (
<FormItem>
<FormLabel>HTTP Method</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select method" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="PATCH">PATCH</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="webhook_secret"
render={({ field }) => (
<FormItem>
<FormLabel>Secret (Optional)</FormLabel>
<FormControl>
<Input placeholder="webhook_secret_key" {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="webhook_retry_count"
render={({ field }) => (
<FormItem>
<FormLabel>Retry Count</FormLabel>
<FormControl>
<Input placeholder="3" {...field} />
</FormControl>
<FormDescription>
Number of retry attempts on failure
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="webhook_trigger_events"
render={({ field }) => (
<FormItem>
<FormLabel>Trigger Events</FormLabel>
<FormControl>
<Input placeholder="all" {...field} />
</FormControl>
<FormDescription>
Events that trigger this webhook (comma-separated)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="webhook_headers"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Headers (Optional)</FormLabel>
<FormControl>
<Textarea
placeholder='{"Authorization": "Bearer token", "Content-Type": "application/json"}'
className="min-h-20"
{...field}
/>
</FormControl>
<FormDescription>
JSON format for additional headers
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="webhook_description"
render={({ field }) => (
<FormItem>
<FormLabel>Description (Optional)</FormLabel>
<FormControl>
<Input placeholder="Description of this webhook" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Payload Template Section */}
<div className="space-y-4">
<FormField
control={form.control}
name="webhook_payload_template"
render={({ field }) => (
<FormItem>
<FormLabel>Payload Template</FormLabel>
<FormControl>
<Textarea
placeholder={defaultPayloadTemplate}
className="min-h-32 font-mono text-sm"
{...field}
/>
</FormControl>
<FormDescription>
JSON template for the webhook payload. Use placeholders like ${'{service_name}'}, ${'{status}'}, etc.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Payload Templates</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => insertTemplate(webhookPayloadTemplates.server)}
className="justify-start"
>
Server Monitoring
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => insertTemplate(webhookPayloadTemplates.service)}
className="justify-start"
>
Service Uptime
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => insertTemplate(webhookPayloadTemplates.ssl)}
className="justify-start"
>
SSL Certificate
</Button>
</div>
<div className="mt-4">
<h4 className="text-sm font-medium mb-2">Available Placeholders:</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
<div className="space-y-1">
<p className="font-medium text-muted-foreground">Server:</p>
<div className="space-y-0.5">
<code className="bg-muted px-1 rounded">${'{server_name}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{cpu_usage}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{ram_usage}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{disk_usage}'}</code>
</div>
</div>
<div className="space-y-1">
<p className="font-medium text-muted-foreground">Service:</p>
<div className="space-y-0.5">
<code className="bg-muted px-1 rounded">${'{service_name}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{response_time}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{url}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{uptime}'}</code>
</div>
</div>
<div className="space-y-1">
<p className="font-medium text-muted-foreground">SSL:</p>
<div className="space-y-0.5">
<code className="bg-muted px-1 rounded">${'{domain}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{expiry_date}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{days_left}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{issuer}'}</code>
</div>
</div>
<div className="space-y-1">
<p className="font-medium text-muted-foreground">Common:</p>
<div className="space-y-0.5">
<code className="bg-muted px-1 rounded">${'{status}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{time}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{message}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{threshold}'}</code>
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</>
)}
<FormField <FormField
control={form.control} control={form.control}
name="enabled" name="enabled"
@@ -319,7 +712,7 @@ export const NotificationChannelDialog = ({
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Enabled</FormLabel> <FormLabel>Enabled</FormLabel>
<FormDescription> <FormDescription>
Turn notifications on or off Enable or disable this notification channel
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>
@@ -338,7 +731,7 @@ export const NotificationChannelDialog = ({
</Button> </Button>
<Button type="submit" disabled={isSubmitting}> <Button type="submit" disabled={isSubmitting}>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isEditing ? "Update" : "Create"} {isEditing ? "Update Channel" : "Create Channel"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>
@@ -87,6 +87,7 @@ const NotificationSettings = () => {
<TabsTrigger value="slack">Slack</TabsTrigger> <TabsTrigger value="slack">Slack</TabsTrigger>
<TabsTrigger value="signal">Signal</TabsTrigger> <TabsTrigger value="signal">Signal</TabsTrigger>
<TabsTrigger value="email">Email</TabsTrigger> <TabsTrigger value="email">Email</TabsTrigger>
<TabsTrigger value="webhook">Webhook</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value={currentTab} className="mt-0"> <TabsContent value={currentTab} className="mt-0">
+55 -85
View File
@@ -1,18 +1,14 @@
import { useState, useEffect } from 'react'; import { useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { authService } from '@/services/authService'; import { authService } from '@/services/authService';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { toast } from '@/components/ui/use-toast'; import { toast } from '@/components/ui/use-toast';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { Eye, EyeOff, Mail, LogIn, Settings, AlertCircle } from "lucide-react"; import { Eye, EyeOff, Mail, LogIn, AlertCircle, Github } from "lucide-react";
import { useLanguage } from '@/contexts/LanguageContext'; import { useLanguage } from '@/contexts/LanguageContext';
import { useTheme } from '@/contexts/ThemeContext'; import { useTheme } from '@/contexts/ThemeContext';
import { API_ENDPOINTS, getCurrentEndpoint, setApiEndpoint } from '@/lib/pocketbase';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Label } from '@/components/ui/label';
import { ForgotPasswordDialog } from '@/components/auth/ForgotPasswordDialog'; import { ForgotPasswordDialog } from '@/components/auth/ForgotPasswordDialog';
const Login = () => { const Login = () => {
@@ -20,30 +16,13 @@ const Login = () => {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [currentEndpoint, setCurrentEndpoint] = useState(getCurrentEndpoint());
const [showForgotPassword, setShowForgotPassword] = useState(false); const [showForgotPassword, setShowForgotPassword] = useState(false);
const [loginError, setLoginError] = useState(''); const [loginError, setLoginError] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useLanguage(); const { t } = useLanguage();
const { theme } = useTheme(); const { theme } = useTheme();
// Add responsiveness check const handleSubmit = useCallback(async (e: React.FormEvent) => {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkScreenSize = () => {
setIsMobile(window.innerWidth < 640);
};
checkScreenSize();
window.addEventListener('resize', checkScreenSize);
return () => {
window.removeEventListener('resize', checkScreenSize);
};
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
setLoginError(''); // Clear previous errors setLoginError(''); // Clear previous errors
@@ -69,7 +48,7 @@ const Login = () => {
setLoginError(error.message); setLoginError(error.message);
} }
} else { } else {
setLoginError(`${t("authenticationFailed")}. Server: ${currentEndpoint}`); setLoginError(t("authenticationFailed"));
} }
toast({ toast({
@@ -82,60 +61,35 @@ const Login = () => {
error.message.includes('invalid email or password')) error.message.includes('invalid email or password'))
? t("invalidCredentials") ? t("invalidCredentials")
: error.message : error.message
: `${t("authenticationFailed")}. Server: ${currentEndpoint}`, : t("authenticationFailed"),
}); });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [email, password, navigate, t]);
const handleEndpointChange = (value: string) => { const togglePasswordVisibility = useCallback(() => {
setCurrentEndpoint(value); setShowPassword(prev => !prev);
setApiEndpoint(value); }, []);
};
const togglePasswordVisibility = () => { const handleEmailChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setShowPassword(!showPassword); setEmail(e.target.value);
}; setLoginError(''); // Clear error when user starts typing
}, []);
const handlePasswordChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
setLoginError(''); // Clear error when user starts typing
}, []);
const openExternalLink = useCallback((url: string) => {
window.open(url, '_blank');
}, []);
return ( return (
<div className="flex items-center justify-center min-h-screen bg-background p-4"> <div className="flex flex-col items-center justify-center min-h-screen bg-background p-4">
<div className="w-full max-w-md p-4 sm:p-8 space-y-6 rounded-xl bg-card shadow-xl border border-border/20"> <div className="w-full max-w-md p-4 sm:p-8 space-y-6 rounded-xl bg-card shadow-xl border border-border/20">
<div className="text-center relative"> <div className="text-center">
{/* Commented out API Endpoint Settings button
<div className="absolute right-0 top-0">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="icon" aria-label="API Settings">
<Settings className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[260px] sm:w-80">
<div className="space-y-4">
<h4 className="font-medium">API Endpoint Settings</h4>
<RadioGroup
value={currentEndpoint}
onValueChange={handleEndpointChange}
className="gap-2"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value={API_ENDPOINTS.LOCAL} id="local" />
<Label htmlFor="local" className="truncate text-sm">Local: {API_ENDPOINTS.LOCAL}</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value={API_ENDPOINTS.REMOTE} id="remote" />
<Label htmlFor="remote" className="truncate text-sm">Remote: {API_ENDPOINTS.REMOTE}</Label>
</div>
</RadioGroup>
<div className="text-xs text-muted-foreground truncate">
Current endpoint: {currentEndpoint}
</div>
</div>
</PopoverContent>
</Popover>
</div>
*/}
{/* Logo */} {/* Logo */}
<div className="mb-4"> <div className="mb-4">
<img <img
@@ -146,11 +100,8 @@ const Login = () => {
</div> </div>
<h1 className="text-xl sm:text-2xl font-bold tracking-tight text-foreground">{t("signInToYourAccount")}</h1> <h1 className="text-xl sm:text-2xl font-bold tracking-tight text-foreground">{t("signInToYourAccount")}</h1>
{/* Removed "Don't have an account? Create one" text */}
</div> </div>
{/* Removed Google Sign in button section */}
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6"> <form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
{/* Error Alert */} {/* Error Alert */}
{loginError && ( {loginError && (
@@ -171,10 +122,7 @@ const Login = () => {
placeholder="your.email@provider.com" placeholder="your.email@provider.com"
type="email" type="email"
value={email} value={email}
onChange={(e) => { onChange={handleEmailChange}
setEmail(e.target.value);
setLoginError(''); // Clear error when user starts typing
}}
required required
className="pl-10 text-sm sm:text-base h-9 sm:h-10" className="pl-10 text-sm sm:text-base h-9 sm:h-10"
/> />
@@ -204,10 +152,7 @@ const Login = () => {
placeholder="••••••••••••" placeholder="••••••••••••"
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={password} value={password}
onChange={(e) => { onChange={handlePasswordChange}
setPassword(e.target.value);
setLoginError(''); // Clear error when user starts typing
}}
required required
className="pl-10 text-sm sm:text-base h-9 sm:h-10" className="pl-10 text-sm sm:text-base h-9 sm:h-10"
/> />
@@ -234,13 +179,38 @@ const Login = () => {
{loading ? t("signingIn") : t("signIn")} {loading ? t("signingIn") : t("signIn")}
{!loading && <LogIn className="ml-2 h-4 w-4" />} {!loading && <LogIn className="ml-2 h-4 w-4" />}
</Button> </Button>
<p className="text-xxs sm:text-xs text-center text-muted-foreground">
{t("bySigningIn")} <a href="#" className="text-emerald-500">{t("termsAndConditions")}</a> {t("and")} <a href="#" className="text-emerald-500">{t("privacyPolicy")}</a>.
</p>
</form> </form>
</div> </div>
{/* Footer with Social Media Icons */}
<div className="mt-8 flex items-center justify-center space-x-6">
<button
onClick={() => openExternalLink('https://github.com/operacle/checkcle')}
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="GitHub"
>
<Github className="h-5 w-5" />
</button>
<button
onClick={() => openExternalLink('https://x.com/checkcle_oss')}
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="X (Twitter)"
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</button>
<button
onClick={() => openExternalLink('https://discord.gg/xs9gbubGwX')}
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Discord"
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.010c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
</button>
</div>
<ForgotPasswordDialog <ForgotPasswordDialog
open={showForgotPassword} open={showForgotPassword}
onOpenChange={setShowForgotPassword} onOpenChange={setShowForgotPassword}
+19 -12
View File
@@ -18,6 +18,13 @@ export interface AlertConfiguration {
enabled: boolean; enabled: boolean;
created?: string; created?: string;
updated?: string; updated?: string;
// Email specific fields
email_address?: string;
email_sender_name?: string;
smtp_server?: string;
smtp_port?: string;
webhook_id?: string;
channel_id?: string;
} }
export const alertConfigService = { export const alertConfigService = {
@@ -25,7 +32,7 @@ export const alertConfigService = {
// console.info("Fetching alert configurations"); // console.info("Fetching alert configurations");
try { try {
const response = await pb.collection('alert_configurations').getList(1, 50); const response = await pb.collection('alert_configurations').getList(1, 50);
// console.info("Alert configurations response:", response); // console.info("Alert configurations response:", response);
return response.items as AlertConfiguration[]; return response.items as AlertConfiguration[];
} catch (error) { } catch (error) {
// console.error("Error fetching alert configurations:", error); // console.error("Error fetching alert configurations:", error);
@@ -42,17 +49,17 @@ export const alertConfigService = {
// console.info("Creating alert configuration:", config); // console.info("Creating alert configuration:", config);
try { try {
const result = await pb.collection('alert_configurations').create(config); const result = await pb.collection('alert_configurations').create(config);
// console.info("Alert configuration created:", result); // console.info("Alert configuration created:", result);
toast({ toast({
title: "Success", title: "Success",
description: "Notification settings saved successfully", description: "Notification channel created successfully",
}); });
return result as AlertConfiguration; return result as AlertConfiguration;
} catch (error) { } catch (error) {
// console.error("Error creating alert configuration:", error); // console.error("Error creating alert configuration:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to save notification settings", description: "Failed to create notification channel",
variant: "destructive" variant: "destructive"
}); });
return null; return null;
@@ -60,20 +67,20 @@ export const alertConfigService = {
}, },
async updateAlertConfiguration(id: string, config: Partial<AlertConfiguration>): Promise<AlertConfiguration | null> { async updateAlertConfiguration(id: string, config: Partial<AlertConfiguration>): Promise<AlertConfiguration | null> {
// console.info(`Updating alert configuration ${id}:`, config); // console.info(`Updating alert configuration ${id}:`, config);
try { try {
const result = await pb.collection('alert_configurations').update(id, config); const result = await pb.collection('alert_configurations').update(id, config);
// console.info("Alert configuration updated:", result); // console.info("Alert configuration updated:", result);
toast({ toast({
title: "Success", title: "Success",
description: "Notification settings updated successfully", description: "Notification channel updated successfully",
}); });
return result as AlertConfiguration; return result as AlertConfiguration;
} catch (error) { } catch (error) {
// console.error("Error updating alert configuration:", error); // console.error("Error updating alert configuration:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to update notification settings", description: "Failed to update notification channel",
variant: "destructive" variant: "destructive"
}); });
return null; return null;
@@ -81,17 +88,17 @@ export const alertConfigService = {
}, },
async deleteAlertConfiguration(id: string): Promise<boolean> { async deleteAlertConfiguration(id: string): Promise<boolean> {
// console.info(`Deleting alert configuration ${id}`); // console.info(`Deleting alert configuration ${id}`);
try { try {
await pb.collection('alert_configurations').delete(id); await pb.collection('alert_configurations').delete(id);
// console.info("Alert configuration deleted"); // console.info("Alert configuration deleted");
toast({ toast({
title: "Success", title: "Success",
description: "Notification channel removed", description: "Notification channel removed",
}); });
return true; return true;
} catch (error) { } catch (error) {
// console.error("Error deleting alert configuration:", error); // console.error("Error deleting alert configuration:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to remove notification channel", description: "Failed to remove notification channel",
@@ -1,7 +1,6 @@
import { pb } from '@/lib/pocketbase'; import { pb } from '@/lib/pocketbase';
import { uptimeService } from '@/services/uptimeService'; import { uptimeService } from '@/services/uptimeService';
import { notificationService } from '@/services/notification'; // Import from the main notification service
import { prepareServiceForNotification } from '../utils/httpUtils'; import { prepareServiceForNotification } from '../utils/httpUtils';
import { UptimeData } from '@/types/service.types'; import { UptimeData } from '@/types/service.types';
@@ -9,7 +8,7 @@ import { UptimeData } from '@/types/service.types';
* Handle a service that is determined to be UP * Handle a service that is determined to be UP
*/ */
export async function handleServiceUp(service: any, responseTime: number, formattedTime: string): Promise<void> { export async function handleServiceUp(service: any, responseTime: number, formattedTime: string): Promise<void> {
console.log(`Service ${service.name} is UP! Response time: ${responseTime}ms`); // console.log(`Service ${service.name} is UP! Response time: ${responseTime}ms`);
// Create a history record of this check with a more accurate timestamp // Create a history record of this check with a more accurate timestamp
const uptimeData: UptimeData = { const uptimeData: UptimeData = {
@@ -41,47 +40,18 @@ export async function handleServiceUp(service: any, responseTime: number, format
try { try {
await uptimeService.recordUptimeData(uptimeData); await uptimeService.recordUptimeData(uptimeData);
} catch (error) { } catch (error) {
console.error("Failed to record uptime data on first try, retrying...", error); // console.error("Failed to record uptime data on first try, retrying...", error);
// Wait a short time and retry once // Wait a short time and retry once
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
await uptimeService.recordUptimeData(uptimeData); await uptimeService.recordUptimeData(uptimeData);
} }
// Reset notification count if service is recovered from DOWN status // Status change logging (notification logic removed - will be handled by backend)
if (previousStatus === "down") {
console.log(`Service ${service.name} recovered from DOWN status - resetting notification count`);
notificationService.resetNotificationCount(service.id);
}
// Send notification if status changed from down to up
if (statusChanged) { if (statusChanged) {
console.log(`Status changed from ${previousStatus} to UP - sending notification`); // console.log(`Status changed from ${previousStatus} to UP - notification will be handled by backend`);
// Convert PocketBase record to Service type for notification
const serviceForNotification = prepareServiceForNotification(service, "up", responseTime);
// Check if alerts are muted - STRICT check for "muted" string value
if (service.alerts === "muted" || serviceForNotification.alerts === "muted") {
console.log(`Alerts are MUTED for service ${service.name}, SKIPPING UP notification`);
return;
}
try {
// Send notification through main notification service
await notificationService.sendNotification({
service: serviceForNotification,
status: "up",
responseTime: responseTime,
timestamp: new Date().toISOString()
});
console.log("UP notification sent successfully");
} catch (error) {
console.error("Error sending UP notification:", error);
}
} }
} catch (error) { } catch (error) {
console.error("Error handling service UP state:", error); // console.error("Error handling service UP state:", error);
} }
} }
@@ -89,7 +59,7 @@ export async function handleServiceUp(service: any, responseTime: number, format
* Handle a service that is determined to be DOWN * Handle a service that is determined to be DOWN
*/ */
export async function handleServiceDown(service: any, formattedTime: string): Promise<void> { export async function handleServiceDown(service: any, formattedTime: string): Promise<void> {
console.log(`Service ${service.name} is DOWN!`); // console.log(`Service ${service.name} is DOWN!`);
// Create a history record of this check // Create a history record of this check
const uptimeData: UptimeData = { const uptimeData: UptimeData = {
@@ -106,7 +76,7 @@ export async function handleServiceDown(service: any, formattedTime: string): Pr
const previousStatus = service.status; const previousStatus = service.status;
const statusChanged = previousStatus !== "down"; const statusChanged = previousStatus !== "down";
console.log(`Service ${service.name} previous status: ${previousStatus}, statusChanged: ${statusChanged}`); // console.log(`Service ${service.name} previous status: ${previousStatus}, statusChanged: ${statusChanged}`);
try { try {
// Update service status // Update service status
@@ -123,44 +93,15 @@ export async function handleServiceDown(service: any, formattedTime: string): Pr
try { try {
await uptimeService.recordUptimeData(uptimeData); await uptimeService.recordUptimeData(uptimeData);
} catch (error) { } catch (error) {
console.error("Failed to record uptime data on first try, retrying...", error); // console.error("Failed to record uptime data on first try, retrying...", error);
// Wait a short time and retry once // Wait a short time and retry once
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
await uptimeService.recordUptimeData(uptimeData); await uptimeService.recordUptimeData(uptimeData);
} }
// Convert PocketBase record to Service type for notification // Status change logging (notification logic removed - will be handled by backend)
const serviceForNotification = prepareServiceForNotification(service, "down"); // console.log("Service DOWN status recorded - notification will be handled by backend");
// Check if alerts are muted - STRICT check for "muted" string value
if (service.alerts === "muted" || serviceForNotification.alerts === "muted") {
console.log(`Alerts are MUTED for service ${service.name}, SKIPPING DOWN notification`);
return;
}
console.log("Attempting to send DOWN notification for service:", service.name);
console.log("Service notification data:", {
name: serviceForNotification.name,
notificationChannel: serviceForNotification.notificationChannel,
alertTemplate: serviceForNotification.alertTemplate,
retries: serviceForNotification.retries || 3,
alerts: serviceForNotification.alerts
});
try {
// Use the main notification service - this handles retries internally
const result = await notificationService.sendNotification({
service: serviceForNotification,
status: "down",
responseTime: 0,
timestamp: new Date().toISOString()
});
console.log("DOWN notification sent result:", result);
} catch (error) {
console.error("Error sending DOWN notification:", error);
}
} catch (error) { } catch (error) {
console.error("Error handling service DOWN state:", error); // console.error("Error handling service DOWN state:", error);
} }
} }
@@ -1,7 +1,6 @@
import { pb } from '@/lib/pocketbase'; import { pb } from '@/lib/pocketbase';
import { monitoringIntervals } from '../monitoringIntervals'; import { monitoringIntervals } from '../monitoringIntervals';
import { notificationService } from '@/services/notificationService';
import { Service } from '@/types/service.types'; import { Service } from '@/types/service.types';
import { startMonitoringService } from './startMonitoring'; import { startMonitoringService } from './startMonitoring';
@@ -16,7 +15,7 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
// Fetch the current service to get its name for better logging // Fetch the current service to get its name for better logging
const service = await pb.collection('services').getOne(serviceId); const service = await pb.collection('services').getOne(serviceId);
// console.log(`Resuming service ${service.name} at ${now}`); // console.log(`Resuming service ${service.name} at ${now}`);
// First, clear any existing interval just to be safe // First, clear any existing interval just to be safe
const existingInterval = monitoringIntervals.get(serviceId); const existingInterval = monitoringIntervals.get(serviceId);
@@ -32,50 +31,14 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
last_checked: now last_checked: now
}); });
// Convert PocketBase record to Service type for notification
const serviceForNotification: Service = {
id: service.id,
name: service.name,
url: service.url || "",
host: service.host || "", // Include host property
type: service.service_type || service.type || "HTTP",
status: "up",
responseTime: service.response_time || 0,
uptime: service.uptime || 0,
lastChecked: now,
interval: service.heartbeat_interval || 60,
retries: service.max_retries || 3,
notificationChannel: service.notification_id,
alertTemplate: service.template_id,
muteAlerts: service.mute_alerts || false,
alerts: service.alerts || "unmuted",
muteChangedAt: service.mute_changed_at,
};
// Double check if alerts are muted before sending notification
// Ensure we're using the correct field - alerts should be "muted" or "unmuted"
const alertsMuted = service.alerts === "muted" || serviceForNotification.alerts === "muted";
if (!alertsMuted) {
// console.log(`Alerts NOT muted for service ${service.name}, sending resume notification`);
// Send notification that service has been resumed
await notificationService.sendNotification({
service: serviceForNotification,
status: "up",
timestamp: now
});
} else {
// console.log(`Alerts muted for service ${service.name}, skipping resume notification`);
}
// IMPORTANT: Wait a brief moment to ensure the status update is processed // IMPORTANT: Wait a brief moment to ensure the status update is processed
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise(resolve => setTimeout(resolve, 500));
// Now start the service monitoring with a clean slate // Now start the service monitoring with a clean slate
await startMonitoringService(serviceId); await startMonitoringService(serviceId);
console.log(`Service ${service.name} resumed and ready for monitoring`); // console.log(`Service ${service.name} resumed and ready for monitoring`);
} catch (error) { } catch (error) {
// console.error("Error resuming service:", error); // console.error("Error resuming service:", error);
} }
} }
+4 -232
View File
@@ -1,234 +1,6 @@
import { pb } from "@/lib/pocketbase";
import { toast } from "@/hooks/use-toast";
import { AlertConfiguration } from "../alertConfigService";
import { NotificationTemplate } from "../templateService";
import { NotificationPayload } from "./types";
import { processTemplate, generateDefaultMessage } from "./templateProcessor";
import { sendTelegramNotification } from "./telegramService";
import { sendSignalNotification } from "./signalService";
// Track last notification times for services to implement cooldown // Re-export template processor only
const lastNotifications: Record<string, { export { processTemplate, generateDefaultMessage } from './templateProcessor';
timestamp: Date;
count: number;
lastMessageTime: Date; // Track when the last message was actually sent
}> = {};
// Cooldown period in milliseconds (5 minutes) // Export types from template services
const NOTIFICATION_COOLDOWN = 5 * 60 * 1000; export type { AnyTemplate } from '../templateService';
/**
* Notification service responsible for sending notifications based on service status changes
*/
export const notificationService = {
/**
* Send notification based on service status change
*/
async sendNotification(payload: NotificationPayload): Promise<boolean> {
try {
console.log("=== NOTIFICATION SERVICE TRIGGER ===");
console.log("Sending notification for service:", payload.service.name, "Status:", payload.status);
console.log("Service type:", payload.service.type);
console.log("Service retries:", payload.service.retries || 3);
console.log("Alerts status:", payload.service.alerts);
// First check: Strictly check if alerts are muted for this specific service
if (payload.service.alerts === "muted") {
console.log(`BLOCKING NOTIFICATION: Alerts are explicitly muted for service ${payload.service.name} (ID: ${payload.service.id})`);
return true; // Return true as this is expected behavior, not a failure
}
// For DOWN status, implement the cooldown and max retries logic
if (payload.status === "down") {
const serviceId = payload.service.id;
// Get max retries from service config or default to 3
const maxRetries = typeof payload.service.retries === 'number' ? payload.service.retries : 3;
const now = new Date();
console.log(`DOWN notification for ${payload.service.name} - Max retries: ${maxRetries}`);
if (lastNotifications[serviceId]) {
const lastNotif = lastNotifications[serviceId];
const timeSinceLastNotif = now.getTime() - lastNotif.timestamp.getTime();
console.log(`Time since last notification tracking for ${payload.service.name}: ${Math.round(timeSinceLastNotif/1000)}s`);
console.log(`Current notification count: ${lastNotif.count}/${maxRetries}`);
// Check if we're within the cooldown period
if (timeSinceLastNotif < NOTIFICATION_COOLDOWN) {
// If max retries is set to 1, we should only send one notification during the cooldown period
if (maxRetries === 1) {
console.log(`Max retries is 1 - BLOCKING duplicate DOWN notification for ${payload.service.name}`);
return true; // Skip notification but return success
}
// Check if we've reached max retries
if (lastNotif.count >= maxRetries) {
console.log(`DOWN notification for ${payload.service.name} skipped: Max retries (${maxRetries}) reached. Next notification after cooldown.`);
return true; // Skip notification but return success
}
// Increment count since we're sending another notification
console.log(`DOWN notification for ${payload.service.name}: ${lastNotif.count + 1}/${maxRetries}`);
lastNotifications[serviceId].count += 1;
lastNotifications[serviceId].lastMessageTime = now;
} else {
// Reset count after cooldown period
console.log(`Cooldown period elapsed for ${payload.service.name}. Resetting notification count.`);
lastNotifications[serviceId] = { timestamp: now, count: 1, lastMessageTime: now };
}
} else {
// First notification for this service
console.log(`First DOWN notification for ${payload.service.name}: 1/${maxRetries}`);
lastNotifications[serviceId] = { timestamp: now, count: 1, lastMessageTime: now };
}
}
// Check if service has notification channel configured
const notificationChannel = payload.service.notificationChannel;
if (!notificationChannel || notificationChannel === "none") {
console.log("No notification channel configured for service", payload.service.name);
return false;
}
console.log("Using notification channel ID:", notificationChannel);
try {
// Get the alert configuration details
console.log("Fetching alert configuration with ID:", notificationChannel);
const alertConfig = await pb.collection('alert_configurations').getOne(notificationChannel);
console.log("Found alert configuration:", JSON.stringify({
...alertConfig,
bot_token: alertConfig.bot_token ? "[REDACTED]" : undefined
}, null, 2));
// Check enabled status - skip if explicitly disabled
if (alertConfig.enabled === false || alertConfig.enabled === "false") {
console.log("Alert configuration is disabled, skipping notification");
return false;
}
// Prepare message content
let messageContent = await this.prepareMessageContent(
payload.service,
payload.status,
payload.responseTime,
payload.message
);
// For DOWN status, add retry information to the message
if (payload.status === "down" && lastNotifications[payload.service.id]) {
const retryInfo = lastNotifications[payload.service.id];
const maxRetries = typeof payload.service.retries === 'number' ? payload.service.retries : 3;
if (maxRetries > 1) {
messageContent += `\n\nAlert ${retryInfo.count}/${maxRetries}`;
}
}
console.log(`Prepared message for ${payload.status} notification:`, messageContent);
// Send notification based on type
if (alertConfig.notification_type === "telegram") {
console.log("Sending Telegram notification for service status change");
return await sendTelegramNotification(alertConfig as AlertConfiguration, messageContent);
} else if (alertConfig.notification_type === "signal") {
console.log("Sending Signal notification with message:", messageContent);
return await sendSignalNotification(alertConfig as AlertConfiguration, messageContent);
}
// Add other notification types here
console.log("Notification type not supported:", alertConfig.notification_type);
toast({
title: "Notification Error",
description: `Notification type '${alertConfig.notification_type}' not supported`,
variant: "destructive"
});
return false;
} catch (error) {
console.error("Error processing notification channel:", error);
console.error("Error details:", error instanceof Error ? error.message : "Unknown error");
toast({
title: "Notification Error",
description: `Error processing notification: ${error instanceof Error ? error.message : "Unknown error"}`,
variant: "destructive"
});
return false;
}
} catch (error) {
console.error("Error sending notification:", error);
toast({
title: "Notification Error",
description: `Failed to send notification: ${error instanceof Error ? error.message : "Unknown error"}`,
variant: "destructive"
});
return false;
}
},
/**
* Prepare message content for notification
*/
async prepareMessageContent(
service: any,
status: string,
responseTime?: number,
defaultMessage?: string
): Promise<string> {
// Use provided message if available
if (defaultMessage) {
return defaultMessage;
}
// Check for a template
let templateId = service.alertTemplate;
if (templateId && templateId !== "default") {
try {
console.log("Using specified template ID:", templateId);
const template = await pb.collection('notification_templates').getOne(templateId);
console.log("Template data:", template);
return processTemplate(
template as unknown as NotificationTemplate,
service,
status,
responseTime
);
} catch (error) {
console.error("Error fetching template:", error);
// Fall back to default message format
}
}
console.log("Using default message template");
return generateDefaultMessage(service.name, status, responseTime);
},
/**
* Send a test notification for a specific service status change
*/
async testServiceStatusNotification(serviceName: string, status: "up" | "down", responseTime?: number): Promise<boolean> {
const emoji = status === "up" ? "🟢" : "🔴";
const rtText = responseTime ? ` (Response time: ${responseTime}ms)` : "";
const message = `${emoji} Test notification: Service ${serviceName} is ${status.toUpperCase()}${rtText}`;
console.log("Test notification would be sent:", message);
// Instead of calling testTelegramNotification which no longer exists, just log and return success
return true;
},
/**
* Reset notification count for a service
* This is called when a service recovers from DOWN to UP
*/
resetNotificationCount(serviceId: string): void {
if (lastNotifications[serviceId]) {
console.log(`Resetting notification count for service ${serviceId}`);
delete lastNotifications[serviceId];
}
}
};
// Re-export the types for easier imports
export * from "./types";
@@ -1,42 +1,42 @@
import { NotificationTemplate } from "../templateService"; import { AnyTemplate } from "../templateService";
/** /**
* Process a notification template with service data * Process a notification template with service data
*/ */
export function processTemplate( export function processTemplate(
template: NotificationTemplate, template: AnyTemplate,
service: any, service: any,
status: string, status: string,
responseTime?: number responseTime?: number
): string { ): string {
try { try {
console.log(`Processing template for status: ${status}`); // console.log(`Processing template for status: ${status}`);
let templateText = ""; let templateText = "";
// Select the appropriate message template based on status // Select the appropriate message template based on status
if (status === "up") { if (status === "up") {
templateText = template.up_message || `Service ${service.name} is now UP`; templateText = (template as any).up_message || `Service ${service.name} is now UP`;
} else if (status === "down") { } else if (status === "down") {
templateText = template.down_message || `Service ${service.name} is DOWN`; templateText = (template as any).down_message || `Service ${service.name} is DOWN`;
} else if (status === "warning") { } else if (status === "warning") {
templateText = template.incident_message || `Warning: Service ${service.name} has an incident`; templateText = (template as any).incident_message || `Warning: Service ${service.name} has an incident`;
} else if (status === "maintenance" || status === "paused") { } else if (status === "maintenance" || status === "paused") {
templateText = template.maintenance_message || `Service ${service.name} is in maintenance mode`; templateText = (template as any).maintenance_message || `Service ${service.name} is in maintenance mode`;
} else if (status === "resolved") { } else if (status === "resolved") {
templateText = template.resolved_message || `Issue with service ${service.name} has been resolved`; templateText = (template as any).resolved_message || `Issue with service ${service.name} has been resolved`;
} else { } else {
templateText = `Service ${service.name} status changed to: ${status}`; templateText = `Service ${service.name} status changed to: ${status}`;
} }
// Skip replacement if template is empty // Skip replacement if template is empty
if (!templateText) { if (!templateText) {
console.log("Empty template for status:", status); // console.log("Empty template for status:", status);
return generateDefaultMessage(service.name, status, responseTime); return generateDefaultMessage(service.name, status, responseTime);
} }
console.log("Using template text:", templateText); // console.log("Using template text:", templateText);
// Replace placeholders with actual values // Replace placeholders with actual values
let message = templateText let message = templateText
@@ -56,10 +56,10 @@ export function processTemplate(
.replace(/\${url}/g, service.url || 'N/A') .replace(/\${url}/g, service.url || 'N/A')
.replace(/\${time}/g, new Date().toLocaleString()); .replace(/\${time}/g, new Date().toLocaleString());
console.log("Processed template message:", message); // console.log("Processed template message:", message);
return message; return message;
} catch (error) { } catch (error) {
console.error("Error processing template:", error); // console.error("Error processing template:", error);
return generateDefaultMessage(service.name, status, responseTime); return generateDefaultMessage(service.name, status, responseTime);
} }
} }
@@ -1,194 +0,0 @@
import { Service } from "@/types/service.types";
import { processTemplate, generateDefaultMessage } from "./notification/templateProcessor";
import { sendTelegramNotification, testSendTelegramMessage } from "./notification/telegramService";
import { pb } from "@/lib/pocketbase";
import { templateService } from "./templateService";
import { AlertConfiguration } from "./alertConfigService";
interface NotificationData {
service: Service;
status: string;
responseTime?: number;
timestamp: string;
_notificationSource?: string;
}
// Track last notification times for services to implement cooldown
const lastNotifications: Record<string, {
timestamp: Date;
count: number;
}> = {};
// Cooldown period in milliseconds (5 minutes)
const NOTIFICATION_COOLDOWN = 5 * 60 * 1000;
export const notificationService = {
/**
* Send a notification for a service status change
*/
async sendNotification(data: NotificationData): Promise<boolean> {
try {
const { service, status, responseTime } = data;
// console.log(`Preparing to send notification for service: ${service.name}, status: ${status}`);
// console.log(`Service alerts status: ${service.alerts}`);
// First check if alerts are muted for this service
// STRICT equality check against "muted" string value
if (service.alerts === "muted") {
// console.log(`NOTIFICATION BLOCKED: Alerts are muted for service: ${service.name}`);
return true; // Return true as this is expected behavior
}
// For paused status, check if this is a duplicate notification from another source
// This helps prevent the double-notification issue
if (status === "paused" && data._notificationSource === "duplicate_check") {
// console.log("NOTIFICATION BLOCKED: Duplicate pause notification detected");
return true; // Return true as this is expected behavior
}
// For DOWN status, implement the cooldown and max retries logic
if (status === "down") {
const serviceId = service.id;
const maxRetries = service.retries || 3; // Default to 3 if not specified
const now = new Date();
if (lastNotifications[serviceId]) {
const lastNotif = lastNotifications[serviceId];
const timeSinceLastNotif = now.getTime() - lastNotif.timestamp.getTime();
// Check if we're within the cooldown period
if (timeSinceLastNotif < NOTIFICATION_COOLDOWN) {
// Increment count only if we haven't reached max retries
if (lastNotif.count < maxRetries) {
// console.log(`DOWN notification for ${service.name}: ${lastNotif.count + 1}/${maxRetries}`);
lastNotifications[serviceId].count += 1;
} else {
// console.log(`DOWN notification for ${service.name} skipped: Max retries (${maxRetries}) reached. Next notification after cooldown.`);
return true; // Skip notification but return success
}
} else {
// Reset count after cooldown period
// console.log(`Cooldown period elapsed for ${service.name}. Resetting notification count.`);
lastNotifications[serviceId] = { timestamp: now, count: 1 };
}
} else {
// First notification for this service
// console.log(`First DOWN notification for ${service.name}: 1/${maxRetries}`);
lastNotifications[serviceId] = { timestamp: now, count: 1 };
}
// Update timestamp for the current notification
lastNotifications[serviceId].timestamp = now;
}
// Check if notification channel is set
if (!service.notificationChannel) {
// console.log(`No notification channel set for service: ${service.name}`);
return false;
}
// Fetch the notification configuration
const alertConfigRecord = await pb.collection('alert_configurations').getOne(service.notificationChannel);
if (!alertConfigRecord) {
// console.error(`Alert configuration not found for ID: ${service.notificationChannel}`);
return false;
}
if (!alertConfigRecord.enabled) {
// console.log(`Alert configuration is disabled for service: ${service.name}`);
return false;
}
// Convert PocketBase record to AlertConfiguration
const alertConfig: AlertConfiguration = {
id: alertConfigRecord.id,
collectionId: alertConfigRecord.collectionId,
collectionName: alertConfigRecord.collectionName,
service_id: alertConfigRecord.service_id || "",
notification_type: alertConfigRecord.notification_type,
telegram_chat_id: alertConfigRecord.telegram_chat_id,
discord_webhook_url: alertConfigRecord.discord_webhook_url,
signal_number: alertConfigRecord.signal_number,
notify_name: alertConfigRecord.notify_name,
bot_token: alertConfigRecord.bot_token,
template_id: alertConfigRecord.template_id,
slack_webhook_url: alertConfigRecord.slack_webhook_url,
enabled: alertConfigRecord.enabled,
created: alertConfigRecord.created,
updated: alertConfigRecord.updated
};
// Select template if one is specified
let template;
if (service.alertTemplate) {
try {
template = await templateService.getTemplate(service.alertTemplate);
} catch (error) {
// console.error(`Error fetching template for ID: ${service.alertTemplate}`, error);
}
}
// Generate the notification message
let message;
if (template) {
message = processTemplate(template, service, status, responseTime);
} else {
message = generateDefaultMessage(service.name, status, responseTime);
}
// For DOWN status, add retry information to the message
if (status === "down" && lastNotifications[service.id]) {
const retryInfo = lastNotifications[service.id];
const maxRetries = service.retries || 3;
message += `\n\nAlert ${retryInfo.count}/${maxRetries}`;
}
// console.log(`Prepared notification message: ${message}`);
// Send notification based on notification type
const notificationType = alertConfig.notification_type;
if (notificationType === 'telegram') {
return await sendTelegramNotification(alertConfig, message);
}
// For other types like discord, slack, etc. (not implemented yet)
// console.log(`Notification type ${notificationType} not implemented yet`);
return false;
} catch (error) {
// console.error("Error sending notification:", error);
return false;
}
},
/**
* Send a test notification for a service status change
*/
async testServiceStatusNotification(serviceName: string, status: string, responseTime?: number): Promise<boolean> {
try {
const message = status === 'up'
? `Service ${serviceName} is UP${responseTime ? ` (Response time: ${responseTime}ms)` : ''}`
: `Service ${serviceName} is DOWN`;
// console.log(`Test notification would have been sent: ${message}`);
return true; // Just log, don't actually send
} catch (error) {
// console.error("Error in test notification:", error);
return false;
}
},
/**
* Reset notification count for a service
* This can be called when a service recovers from DOWN to UP
*/
resetNotificationCount(serviceId: string): void {
if (lastNotifications[serviceId]) {
// console.log(`Resetting notification count for service ${serviceId}`);
delete lastNotifications[serviceId];
}
}
};
@@ -0,0 +1,103 @@
import { pb } from "@/lib/pocketbase";
export interface ServerNotificationTemplate {
id: string;
collectionId: string;
collectionName: string;
name: string;
ram_message: string;
cpu_message: string;
disk_message: string;
network_message: string;
up_message: string;
down_message: string;
notification_id: string;
warning_message: string;
paused_message: string;
cpu_temp_message: string;
disk_io_message: string;
placeholder: string;
created: string;
updated: string;
}
export interface CreateUpdateServerNotificationTemplateData {
name: string;
ram_message: string;
cpu_message: string;
disk_message: string;
network_message: string;
up_message: string;
down_message: string;
notification_id: string;
warning_message: string;
paused_message: string;
cpu_temp_message: string;
disk_io_message: string;
placeholder: string;
}
export const serverNotificationTemplateService = {
async getTemplates(): Promise<ServerNotificationTemplate[]> {
try {
// console.log("Fetching server notification templates");
const response = await pb.collection('server_notification_templates').getList(1, 50, {
sort: '-created',
});
// console.log("Server notification templates response:", response);
return response.items as unknown as ServerNotificationTemplate[];
} catch (error) {
// console.error("Error fetching server notification templates:", error);
throw error;
}
},
async getTemplate(id: string): Promise<ServerNotificationTemplate> {
try {
// console.log(`Fetching server notification template with id: ${id}`);
const response = await pb.collection('server_notification_templates').getOne(id);
// console.log("Server notification template response:", response);
return response as unknown as ServerNotificationTemplate;
} catch (error) {
/// console.error(`Error fetching server notification template with id ${id}:`, error);
throw error;
}
},
async createTemplate(data: CreateUpdateServerNotificationTemplateData): Promise<ServerNotificationTemplate> {
try {
// console.log("Creating new server notification template with data:", data);
const response = await pb.collection('server_notification_templates').create(data);
// console.log("Create server notification template response:", response);
return response as unknown as ServerNotificationTemplate;
} catch (error) {
// console.error("Error creating server notification template:", error);
throw error;
}
},
async updateTemplate(id: string, data: Partial<CreateUpdateServerNotificationTemplateData>): Promise<ServerNotificationTemplate> {
try {
// console.log(`Updating server notification template with id: ${id}`, data);
const response = await pb.collection('server_notification_templates').update(id, data);
// console.log("Update server notification template response:", response);
return response as unknown as ServerNotificationTemplate;
} catch (error) {
// console.error(`Error updating server notification template with id ${id}:`, error);
throw error;
}
},
async deleteTemplate(id: string): Promise<boolean> {
try {
// console.log(`Deleting server notification template with id: ${id}`);
await pb.collection('server_notification_templates').delete(id);
// console.log("Server notification template deleted successfully");
return true;
} catch (error) {
// console.error(`Error deleting server notification template with id ${id}:`, error);
throw error;
}
}
};
@@ -0,0 +1,93 @@
import { pb } from "@/lib/pocketbase";
export interface ServiceNotificationTemplate {
id: string;
collectionId: string;
collectionName: string;
name: string;
up_message: string;
down_message: string;
maintenance_message: string;
incident_message: string;
resolved_message: string;
placeholder: string;
warning_message: string;
created: string;
updated: string;
}
export interface CreateUpdateServiceNotificationTemplateData {
name: string;
up_message: string;
down_message: string;
maintenance_message: string;
incident_message: string;
resolved_message: string;
placeholder: string;
warning_message: string;
}
export const serviceNotificationTemplateService = {
async getTemplates(): Promise<ServiceNotificationTemplate[]> {
try {
// console.log("Fetching service notification templates");
const response = await pb.collection('service_notification_templates').getList(1, 50, {
sort: '-created',
});
// console.log("Service notification templates response:", response);
return response.items as unknown as ServiceNotificationTemplate[];
} catch (error) {
// console.error("Error fetching service notification templates:", error);
throw error;
}
},
async getTemplate(id: string): Promise<ServiceNotificationTemplate> {
try {
// console.log(`Fetching service notification template with id: ${id}`);
const response = await pb.collection('service_notification_templates').getOne(id);
// console.log("Service notification template response:", response);
return response as unknown as ServiceNotificationTemplate;
} catch (error) {
// console.error(`Error fetching service notification template with id ${id}:`, error);
throw error;
}
},
async createTemplate(data: CreateUpdateServiceNotificationTemplateData): Promise<ServiceNotificationTemplate> {
try {
// console.log("Creating new service notification template with data:", data);
const response = await pb.collection('service_notification_templates').create(data);
// console.log("Create service notification template response:", response);
return response as unknown as ServiceNotificationTemplate;
} catch (error) {
// console.error("Error creating service notification template:", error);
throw error;
}
},
async updateTemplate(id: string, data: Partial<CreateUpdateServiceNotificationTemplateData>): Promise<ServiceNotificationTemplate> {
try {
// console.log(`Updating service notification template with id: ${id}`, data);
const response = await pb.collection('service_notification_templates').update(id, data);
// console.log("Update service notification template response:", response);
return response as unknown as ServiceNotificationTemplate;
} catch (error) {
// console.error(`Error updating service notification template with id ${id}:`, error);
throw error;
}
},
async deleteTemplate(id: string): Promise<boolean> {
try {
// console.log(`Deleting service notification template with id: ${id}`);
await pb.collection('service_notification_templates').delete(id);
// console.log("Service notification template deleted successfully");
return true;
} catch (error) {
// console.error(`Error deleting service notification template with id ${id}:`, error);
throw error;
}
}
};
@@ -13,6 +13,8 @@ export const addSSLCertificate = async (
certificateData: AddSSLCertificateDto certificateData: AddSSLCertificateDto
): Promise<SSLCertificate> => { ): Promise<SSLCertificate> => {
try { try {
const currentTime = new Date().toISOString();
// Prepare the data for saving to database // Prepare the data for saving to database
// The Go service will handle the actual SSL checking // The Go service will handle the actual SSL checking
const data = { const data = {
@@ -23,14 +25,15 @@ export const addSSLCertificate = async (
cert_sans: "", cert_sans: "",
cert_alg: "", cert_alg: "",
serial_number: "", serial_number: "",
valid_from: new Date().toISOString(), // Will be updated by Go service valid_from: currentTime, // Will be updated by Go service
valid_till: new Date().toISOString(), // Will be updated by Go service valid_till: currentTime, // Will be updated by Go service
validity_days: 0, // Will be updated by Go service validity_days: 0, // Will be updated by Go service
days_left: 0, // Will be updated by Go service days_left: 0, // Will be updated by Go service
warning_threshold: Number(certificateData.warning_threshold) || 30, warning_threshold: Number(certificateData.warning_threshold) || 30,
expiry_threshold: Number(certificateData.expiry_threshold) || 7, expiry_threshold: Number(certificateData.expiry_threshold) || 7,
notification_channel: certificateData.notification_channel || "", notification_channel: certificateData.notification_channel || "",
check_interval: Number(certificateData.check_interval) || 1, // New field check_interval: Number(certificateData.check_interval) || 1, // New field
check_at: currentTime, // Set to current time to trigger immediate check
}; };
// Save to database // Save to database
@@ -112,10 +115,10 @@ export const deleteSSLCertificate = async (id: string): Promise<boolean> => {
*/ */
export const refreshAllCertificates = async (): Promise<{ success: number; failed: number }> => { export const refreshAllCertificates = async (): Promise<{ success: number; failed: number }> => {
try { try {
const response = await pb.collection("ssl_certificates").getList(1, 200); const response = await pb.collection("ssl_certificates").getList(1, 100);
const certificates = response.items as unknown as SSLCertificate[]; const certificates = response.items as unknown as SSLCertificate[];
// console.log(`Refreshing ${certificates.length} certificates...`); console.log(`Refreshing ${certificates.length} certificates...`);
let success = 0; let success = 0;
let failed = 0; let failed = 0;
@@ -125,14 +128,14 @@ export const refreshAllCertificates = async (): Promise<{ success: number; faile
await checkCertificateAndNotify(cert); await checkCertificateAndNotify(cert);
success++; success++;
} catch (error) { } catch (error) {
// console.error(`Failed to refresh certificate ${cert.domain}:`, error); console.error(`Failed to refresh certificate ${cert.domain}:`, error);
failed++; failed++;
} }
} }
return { success, failed }; return { success, failed };
} catch (error) { } catch (error) {
// console.error("Error refreshing certificates:", error); console.error("Error refreshing certificates:", error);
throw error; throw error;
} }
}; };
@@ -0,0 +1,87 @@
import { pb } from "@/lib/pocketbase";
export interface SslNotificationTemplate {
id: string;
collectionId: string;
collectionName: string;
name: string;
expired: string;
exiring_soon: string;
warning: string;
placeholder: string;
created: string;
updated: string;
}
export interface CreateUpdateSslNotificationTemplateData {
name: string;
expired: string;
exiring_soon: string;
warning: string;
placeholder: string;
}
export const sslNotificationTemplateService = {
async getTemplates(): Promise<SslNotificationTemplate[]> {
try {
// console.log("Fetching SSL notification templates");
const response = await pb.collection('ssl_notification_templates').getList(1, 50, {
sort: '-created',
});
// console.log("SSL notification templates response:", response);
return response.items as unknown as SslNotificationTemplate[];
} catch (error) {
// console.error("Error fetching SSL notification templates:", error);
throw error;
}
},
async getTemplate(id: string): Promise<SslNotificationTemplate> {
try {
// console.log(`Fetching SSL notification template with id: ${id}`);
const response = await pb.collection('ssl_notification_templates').getOne(id);
// console.log("SSL notification template response:", response);
return response as unknown as SslNotificationTemplate;
} catch (error) {
// console.error(`Error fetching SSL notification template with id ${id}:`, error);
throw error;
}
},
async createTemplate(data: CreateUpdateSslNotificationTemplateData): Promise<SslNotificationTemplate> {
try {
// console.log("Creating new SSL notification template with data:", data);
const response = await pb.collection('ssl_notification_templates').create(data);
// console.log("Create SSL notification template response:", response);
return response as unknown as SslNotificationTemplate;
} catch (error) {
// console.error("Error creating SSL notification template:", error);
throw error;
}
},
async updateTemplate(id: string, data: Partial<CreateUpdateSslNotificationTemplateData>): Promise<SslNotificationTemplate> {
try {
// console.log(`Updating SSL notification template with id: ${id}`, data);
const response = await pb.collection('ssl_notification_templates').update(id, data);
// console.log("Update SSL notification template response:", response);
return response as unknown as SslNotificationTemplate;
} catch (error) {
// console.error(`Error updating SSL notification template with id ${id}:`, error);
throw error;
}
},
async deleteTemplate(id: string): Promise<boolean> {
try {
// console.log(`Deleting SSL notification template with id: ${id}`);
await pb.collection('ssl_notification_templates').delete(id);
// console.log("SSL notification template deleted successfully");
return true;
} catch (error) {
// console.error(`Error deleting SSL notification template with id ${id}:`, error);
throw error;
}
}
};
+100 -80
View File
@@ -1,100 +1,120 @@
import { pb } from "@/lib/pocketbase"; import { pb } from "@/lib/pocketbase";
import {
serverNotificationTemplateService,
ServerNotificationTemplate,
CreateUpdateServerNotificationTemplateData
} from "./serverNotificationTemplateService";
import {
serviceNotificationTemplateService,
ServiceNotificationTemplate,
CreateUpdateServiceNotificationTemplateData
} from "./serviceNotificationTemplateService";
import {
sslNotificationTemplateService,
SslNotificationTemplate,
CreateUpdateSslNotificationTemplateData
} from "./sslNotificationTemplateService";
export interface NotificationTemplate { export type TemplateType = 'server' | 'service' | 'ssl';
id: string;
collectionId: string;
collectionName: string;
name: string;
up_message: string;
down_message: string;
service_name_placeholder: string;
response_time_placeholder: string;
status_placeholder: string;
threshold_placeholder: string;
type: string;
maintenance_message: string;
incident_message: string;
resolved_message: string;
created: string;
updated: string;
}
export interface CreateUpdateTemplateData { export type AnyTemplate = ServerNotificationTemplate | ServiceNotificationTemplate | SslNotificationTemplate;
name: string; export type AnyTemplateData = CreateUpdateServerNotificationTemplateData | CreateUpdateServiceNotificationTemplateData | CreateUpdateSslNotificationTemplateData;
up_message: string;
down_message: string; // Export individual template types
service_name_placeholder: string; export type { ServerNotificationTemplate, ServiceNotificationTemplate, SslNotificationTemplate };
response_time_placeholder: string; export type { CreateUpdateServerNotificationTemplateData, CreateUpdateServiceNotificationTemplateData, CreateUpdateSslNotificationTemplateData };
status_placeholder: string;
threshold_placeholder: string;
type: string;
maintenance_message: string;
incident_message: string;
resolved_message: string;
}
export const templateService = { export const templateService = {
async getTemplates(): Promise<NotificationTemplate[]> { async getTemplates(type: TemplateType): Promise<AnyTemplate[]> {
try { switch (type) {
// console.log("Fetching server notification templates"); case 'server':
const response = await pb.collection('server_notification_templates').getList(1, 50, { return serverNotificationTemplateService.getTemplates();
sort: '-created', case 'service':
}); return serviceNotificationTemplateService.getTemplates();
// console.log("Server templates response:", response); case 'ssl':
return response.items as unknown as NotificationTemplate[]; return sslNotificationTemplateService.getTemplates();
} catch (error) { default:
// console.error("Error fetching server templates:", error); throw new Error(`Unknown template type: ${type}`);
throw error;
} }
}, },
async getTemplate(id: string): Promise<NotificationTemplate> { async getTemplate(id: string, type: TemplateType): Promise<AnyTemplate> {
try { switch (type) {
// console.log(`Fetching server template with id: ${id}`); case 'server':
const response = await pb.collection('server_notification_templates').getOne(id); return serverNotificationTemplateService.getTemplate(id);
// console.log("Server template response:", response); case 'service':
return response as unknown as NotificationTemplate; return serviceNotificationTemplateService.getTemplate(id);
} catch (error) { case 'ssl':
// console.error(`Error fetching server template with id ${id}:`, error); return sslNotificationTemplateService.getTemplate(id);
throw error; default:
throw new Error(`Unknown template type: ${type}`);
} }
}, },
async createTemplate(data: CreateUpdateTemplateData): Promise<NotificationTemplate> { async createTemplate(data: AnyTemplateData, type: TemplateType): Promise<AnyTemplate> {
try { switch (type) {
// console.log("Creating new server template with data:", data); case 'server':
const response = await pb.collection('server_notification_templates').create(data); return serverNotificationTemplateService.createTemplate(data as CreateUpdateServerNotificationTemplateData);
// console.log("Create server template response:", response); case 'service':
return response as unknown as NotificationTemplate; return serviceNotificationTemplateService.createTemplate(data as CreateUpdateServiceNotificationTemplateData);
} catch (error) { case 'ssl':
// console.error("Error creating server template:", error); return sslNotificationTemplateService.createTemplate(data as CreateUpdateSslNotificationTemplateData);
throw error; default:
throw new Error(`Unknown template type: ${type}`);
} }
}, },
async updateTemplate(id: string, data: Partial<CreateUpdateTemplateData>): Promise<NotificationTemplate> { async updateTemplate(id: string, data: Partial<AnyTemplateData>, type: TemplateType): Promise<AnyTemplate> {
try { switch (type) {
// console.log(`Updating server template with id: ${id}`, data); case 'server':
const response = await pb.collection('server_notification_templates').update(id, data); return serverNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateServerNotificationTemplateData>);
// console.log("Update server template response:", response); case 'service':
return response as unknown as NotificationTemplate; return serviceNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateServiceNotificationTemplateData>);
} catch (error) { case 'ssl':
// console.error(`Error updating server template with id ${id}:`, error); return sslNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateSslNotificationTemplateData>);
throw error; default:
throw new Error(`Unknown template type: ${type}`);
} }
}, },
async deleteTemplate(id: string): Promise<boolean> { async deleteTemplate(id: string, type: TemplateType): Promise<boolean> {
try { switch (type) {
// console.log(`Deleting server template with id: ${id}`); case 'server':
await pb.collection('server_notification_templates').delete(id); return serverNotificationTemplateService.deleteTemplate(id);
// console.log("Server template deleted successfully"); case 'service':
return true; return serviceNotificationTemplateService.deleteTemplate(id);
} catch (error) { case 'ssl':
// console.error(`Error deleting server template with id ${id}:`, error); return sslNotificationTemplateService.deleteTemplate(id);
throw error; default:
throw new Error(`Unknown template type: ${type}`);
} }
} }
}; };
// Template type configurations
export const templateTypeConfigs = {
server: {
label: 'Server Monitoring',
description: 'Templates for server resource monitoring alerts',
placeholders: [
'${server_name}', '${cpu_usage}', '${ram_usage}', '${disk_usage}',
'${network_usage}', '${cpu_temp}', '${disk_io}', '${threshold}', '${time}'
]
},
service: {
label: 'Service Uptime',
description: 'Templates for service uptime monitoring alerts',
placeholders: [
'${service_name}', '${status}', '${response_time}', '${url}',
'${uptime}', '${downtime}', '${time}'
]
},
ssl: {
label: 'SSL Certificate',
description: 'Templates for SSL certificate monitoring alerts',
placeholders: [
'${domain}', '${certificate_name}', '${expiry_date}', '${days_left}',
'${issuer}', '${serial_number}', '${time}'
]
}
};
@@ -0,0 +1,66 @@
import { pb } from "@/lib/pocketbase";
import { toast } from "@/hooks/use-toast";
export interface WebhookConfiguration {
id?: string;
collectionId?: string;
collectionName?: string;
user_id?: string;
url: string;
enabled: string;
secret?: string;
headers?: string;
retry_count?: string;
trigger_events?: string;
description?: string;
name: string;
method?: string;
payload_template?: string;
created?: string;
updated?: string;
}
export const webhookService = {
async createWebhook(config: Omit<WebhookConfiguration, 'id' | 'collectionId' | 'collectionName' | 'created' | 'updated'>): Promise<WebhookConfiguration | null> {
// console.info("Creating webhook configuration:", config);
try {
const result = await pb.collection('webhook').create(config);
// console.info("Webhook configuration created:", result);
toast({
title: "Success",
description: "Webhook created successfully",
});
return result as WebhookConfiguration;
} catch (error) {
// console.error("Error creating webhook configuration:", error);
toast({
title: "Error",
description: "Failed to create webhook",
variant: "destructive"
});
return null;
}
},
async updateWebhook(id: string, config: Partial<WebhookConfiguration>): Promise<WebhookConfiguration | null> {
// console.info(`Updating webhook configuration ${id}:`, config);
try {
const result = await pb.collection('webhook').update(id, config);
// console.info("Webhook configuration updated:", result);
toast({
title: "Success",
description: "Webhook updated successfully",
});
return result as WebhookConfiguration;
} catch (error) {
// console.error("Error updating webhook configuration:", error);
toast({
title: "Error",
description: "Failed to update webhook",
variant: "destructive"
});
return null;
}
}
};
+5 -5
View File
@@ -2,12 +2,12 @@
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/use-toast";
export const copyToClipboard = async (text: string) => { export const copyToClipboard = async (text: string) => {
console.log('copyToClipboard called with text:', text); // Debug log // console.log('copyToClipboard called with text:', text); // Debug log
try { try {
// Try modern clipboard API first // Try modern clipboard API first
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
console.log('Using modern clipboard API'); // Debug log // console.log('Using modern clipboard API'); // Debug log
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
toast({ toast({
title: "Copied!", title: "Copied!",
@@ -16,7 +16,7 @@ export const copyToClipboard = async (text: string) => {
return; return;
} }
console.log('Using fallback clipboard method'); // Debug log // console.log('Using fallback clipboard method'); // Debug log
// Fallback for older browsers or non-secure contexts // Fallback for older browsers or non-secure contexts
const textArea = document.createElement("textarea"); const textArea = document.createElement("textarea");
@@ -39,7 +39,7 @@ export const copyToClipboard = async (text: string) => {
document.body.removeChild(textArea); document.body.removeChild(textArea);
if (successful) { if (successful) {
console.log('Copy successful with execCommand'); // Debug log // console.log('Copy successful with execCommand'); // Debug log
toast({ toast({
title: "Copied!", title: "Copied!",
description: "Content copied to clipboard successfully.", description: "Content copied to clipboard successfully.",
@@ -48,7 +48,7 @@ export const copyToClipboard = async (text: string) => {
throw new Error('Copy command failed'); throw new Error('Copy command failed');
} }
} catch (error) { } catch (error) {
console.error('Failed to copy to clipboard:', error); // console.error('Failed to copy to clipboard:', error);
// Show error toast // Show error toast
toast({ toast({
+35
View File
@@ -0,0 +1,35 @@
name: checkcle-server-agent
services:
checkcle-server-agent:
container_name: monitoring-agent
restart: unless-stopped
ports:
- "8081:8081"
group_add:
- 999
environment:
AGENT_ID: agent_124343535
SERVER_NAME: "test"
SERVER_TOKEN: srv_123243434
POCKETBASE_URL: http://x.x.x.x:8090
POCKETBASE_ENABLED: "true"
volumes:
- /proc:/host/proc:ro
- /etc:/host/etc:ro
- /sys:/host/sys:ro
- /:/host/root:ro
- /var/run:/host/var/run:ro
- /dev:/host/dev:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- monitoring_data:/var/lib/monitoring-agent
- monitoring_logs:/var/log/monitoring-agent
image: operacle/checkcle-server-agent:latest
volumes:
monitoring_data:
external: true
name: monitoring_data
monitoring_logs:
external: true
name: monitoring_logs
+1 -1
View File
@@ -68,7 +68,7 @@ The roadmap is divided into the following stages:
#### Tentative Features: #### Tentative Features:
- [ ] ✅ Server and Service Table row clickable to detail page. - [ ] ✅ Server and Service Table row clickable to detail page.
- [ ] ✅ Implement pagination for the SSL dashboard table - [ ] ✅ Implement pagination for the SSL dashboard table
- [ ] 🔧 Server Agent (RPM, Docker container, and general binary package) - [ ] Server Agent (RPM, Docker container, and general binary package)
- [ ] 🔧 Notification System (Webhook, Telegram, Discord, Slack, Email, Google Chat) - [ ] 🔧 Notification System (Webhook, Telegram, Discord, Slack, Email, Google Chat)
- [ ] 🔧 Improve Uptime Service and Server connection update based on status and notification. - [ ] 🔧 Improve Uptime Service and Server connection update based on status and notification.
- [ ] 🔧 Improve SSL perform the initial check automatically after creation - [ ] 🔧 Improve SSL perform the initial check automatically after creation
+81 -148
View File
@@ -17,6 +17,30 @@ BASE_PACKAGE_URL="https://github.com/operacle/Distributed-Regional-Monitoring/re
PACKAGE_VERSION="1.0.0" PACKAGE_VERSION="1.0.0"
SERVICE_NAME="regional-check-agent" SERVICE_NAME="regional-check-agent"
# 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"
}
# Function to show usage # Function to show usage
show_usage() { show_usage() {
echo "Usage: $0 [OPTIONS]" echo "Usage: $0 [OPTIONS]"
@@ -77,38 +101,36 @@ done
# Validate required parameters # Validate required parameters
if [[ -z "$REGION_NAME" || -z "$AGENT_ID" || -z "$AGENT_IP_ADDRESS" || -z "$AGENT_TOKEN" || -z "$POCKETBASE_URL" ]]; then if [[ -z "$REGION_NAME" || -z "$AGENT_ID" || -z "$AGENT_IP_ADDRESS" || -z "$AGENT_TOKEN" || -z "$POCKETBASE_URL" ]]; then
echo "❌ Error: Missing required parameters" log_error "Missing required parameters"
echo "" echo ""
show_usage show_usage
exit 1 exit 1
fi fi
echo "🚀 CheckCle Regional Monitoring Agent - Universal Installation" echo "============================================="
echo "==============================================================" echo " CheckCle Regional Monitoring Agent"
echo "" echo " Universal Installation"
echo "============================================="
# Check if running as root # Check if running as root
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root (use sudo)" log_error "This script must be run as root (use sudo)"
echo " Usage: sudo bash $0 [options]" echo " Usage: sudo bash $0 [options]"
exit 1 exit 1
fi fi
# Detect operating system # Detect operating system
echo "🔍 Detecting system information..."
OS_TYPE=$(uname -s | tr '[:upper:]' '[:lower:]') OS_TYPE=$(uname -s | tr '[:upper:]' '[:lower:]')
echo " Operating System: $OS_TYPE"
# Check if it's a supported OS # Check if it's a supported OS
if [[ "$OS_TYPE" != "linux" ]]; then if [[ "$OS_TYPE" != "linux" ]]; then
echo "Unsupported operating system: $OS_TYPE" log_error "Unsupported operating system: $OS_TYPE"
echo " This installer only supports Linux systems" log_info "This installer only supports Linux systems"
exit 1 exit 1
fi fi
# Detect architecture # Detect architecture
ARCH=$(uname -m) ARCH=$(uname -m)
echo " Hardware Architecture: $ARCH"
# Map architecture to package architecture # Map architecture to package architecture
case $ARCH in case $ARCH in
@@ -120,33 +142,22 @@ case $ARCH in
;; ;;
armv7l|armv6l) armv7l|armv6l)
PKG_ARCH="arm64" PKG_ARCH="arm64"
echo "⚠️ ARM 32-bit detected, using ARM64 package (may require compatibility layer)" log_warning "ARM 32-bit detected, using ARM64 package (may require compatibility layer)"
;; ;;
*) *)
echo "Unsupported architecture: $ARCH" log_error "Unsupported architecture: $ARCH"
echo " Supported architectures: x86_64 (amd64), aarch64 (arm64)" log_info "Supported architectures: x86_64 (amd64), aarch64 (arm64)"
echo " Please contact support for your architecture: $ARCH"
exit 1 exit 1
;; ;;
esac esac
echo " Package Architecture: $PKG_ARCH" log_success "System: $OS_TYPE ($ARCH -> $PKG_ARCH)"
# Construct package URLs and names # Construct package URLs and names
PACKAGE_URL="$BASE_PACKAGE_URL/distributed-regional-check-agent_${PACKAGE_VERSION}_${PKG_ARCH}.deb" PACKAGE_URL="$BASE_PACKAGE_URL/distributed-regional-check-agent_${PACKAGE_VERSION}_${PKG_ARCH}.deb"
PACKAGE_NAME="distributed-regional-check-agent_${PACKAGE_VERSION}_${PKG_ARCH}.deb" PACKAGE_NAME="distributed-regional-check-agent_${PACKAGE_VERSION}_${PKG_ARCH}.deb"
echo ""
echo "📋 Installation Configuration:"
echo " Region Name: $REGION_NAME"
echo " Agent ID: $AGENT_ID"
echo " Agent IP: $AGENT_IP_ADDRESS"
echo " Package Architecture: $PKG_ARCH"
echo " Package URL: $PACKAGE_URL"
echo ""
# Check for required tools # Check for required tools
echo "🔧 Checking system requirements..."
MISSING_TOOLS=() MISSING_TOOLS=()
if ! command -v wget >/dev/null 2>&1 && ! command -v curl >/dev/null 2>&1; then if ! command -v wget >/dev/null 2>&1 && ! command -v curl >/dev/null 2>&1; then
@@ -162,129 +173,85 @@ if ! command -v systemctl >/dev/null 2>&1; then
fi fi
if [ ${#MISSING_TOOLS[@]} -ne 0 ]; then if [ ${#MISSING_TOOLS[@]} -ne 0 ]; then
echo "Missing required tools: ${MISSING_TOOLS[*]}" log_error "Missing required tools: ${MISSING_TOOLS[*]}"
echo " Please install missing tools and try again" log_info "On Debian/Ubuntu: sudo apt-get update && sudo apt-get install wget curl"
echo " On Debian/Ubuntu: sudo apt-get update && sudo apt-get install wget curl"
exit 1 exit 1
fi fi
echo "✅ System requirements satisfied"
# Create temporary directory # Create temporary directory
TEMP_DIR=$(mktemp -d) TEMP_DIR=$(mktemp -d)
echo "📁 Created temporary directory: $TEMP_DIR"
# Download the .deb package # Download the .deb package
echo "" log_info "Downloading package for $PKG_ARCH..."
echo "📥 Downloading Regional Monitoring Agent package for $PKG_ARCH..."
cd "$TEMP_DIR" cd "$TEMP_DIR"
# Test if package exists first - Accept both 200 and 302 (redirect) as success # Test if package exists first
echo "🔍 Checking package availability..."
if command -v curl >/dev/null 2>&1; then if command -v curl >/dev/null 2>&1; then
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -I "$PACKAGE_URL") HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -I "$PACKAGE_URL")
if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "302" ]; then if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "302" ]; then
echo "Package not found at $PACKAGE_URL (HTTP $HTTP_STATUS)" log_error "Package not found at $PACKAGE_URL (HTTP $HTTP_STATUS)"
echo " Available packages should be:" log_info "Check: https://github.com/operacle/Distributed-Regional-Monitoring/releases"
echo " - distributed-regional-check-agent_${PACKAGE_VERSION}_amd64.deb"
echo " - distributed-regional-check-agent_${PACKAGE_VERSION}_arm64.deb"
echo ""
echo " Please check the GitHub releases page:"
echo " https://github.com/operacle/Distributed-Regional-Monitoring/releases"
rm -rf "$TEMP_DIR" rm -rf "$TEMP_DIR"
exit 1 exit 1
fi fi
echo "✅ Package found (HTTP $HTTP_STATUS), proceeding with download..."
fi fi
# Try wget first, then curl as fallback # Try wget first, then curl as fallback
DOWNLOAD_SUCCESS=false DOWNLOAD_SUCCESS=false
if command -v wget >/dev/null 2>&1; then if command -v wget >/dev/null 2>&1; then
echo "📥 Downloading with wget..."
if wget -q --show-progress --timeout=30 --tries=3 "$PACKAGE_URL" -O "$PACKAGE_NAME"; then if wget -q --show-progress --timeout=30 --tries=3 "$PACKAGE_URL" -O "$PACKAGE_NAME"; then
DOWNLOAD_SUCCESS=true DOWNLOAD_SUCCESS=true
echo "✅ Package downloaded successfully using wget"
fi fi
fi fi
if [ "$DOWNLOAD_SUCCESS" = false ] && command -v curl >/dev/null 2>&1; then if [ "$DOWNLOAD_SUCCESS" = false ] && command -v curl >/dev/null 2>&1; then
echo "📥 Downloading with curl..."
if curl -L --connect-timeout 30 --max-time 300 --retry 3 --retry-delay 2 -o "$PACKAGE_NAME" "$PACKAGE_URL" --progress-bar; then if curl -L --connect-timeout 30 --max-time 300 --retry 3 --retry-delay 2 -o "$PACKAGE_NAME" "$PACKAGE_URL" --progress-bar; then
DOWNLOAD_SUCCESS=true DOWNLOAD_SUCCESS=true
echo "✅ Package downloaded successfully using curl"
fi fi
fi fi
if [ "$DOWNLOAD_SUCCESS" = false ]; then if [ "$DOWNLOAD_SUCCESS" = false ]; then
echo "Failed to download package from $PACKAGE_URL" log_error "Failed to download package from $PACKAGE_URL"
echo " Please check:" log_info "Check internet connection and package availability"
echo " - Internet connection"
echo " - Package availability for $PKG_ARCH architecture"
echo " - GitHub repository access: https://github.com/operacle/Distributed-Regional-Monitoring/releases"
echo " - Firewall/proxy settings"
echo ""
echo " Available packages should be:"
echo " - distributed-regional-check-agent_${PACKAGE_VERSION}_amd64.deb"
echo " - distributed-regional-check-agent_${PACKAGE_VERSION}_arm64.deb"
rm -rf "$TEMP_DIR" rm -rf "$TEMP_DIR"
exit 1 exit 1
fi fi
# Verify download was successful # Verify download was successful
if [ ! -f "$PACKAGE_NAME" ] || [ ! -s "$PACKAGE_NAME" ]; then if [ ! -f "$PACKAGE_NAME" ] || [ ! -s "$PACKAGE_NAME" ]; then
echo "Downloaded package is empty or missing" log_error "Downloaded package is empty or missing"
echo " File size: $(ls -lh "$PACKAGE_NAME" 2>/dev/null | awk '{print $5}' || echo 'file not found')"
rm -rf "$TEMP_DIR" rm -rf "$TEMP_DIR"
exit 1 exit 1
fi fi
# Verify package integrity # Verify package integrity
echo ""
echo "🔍 Verifying package..."
if dpkg-deb --info "$PACKAGE_NAME" > /dev/null 2>&1; then if dpkg-deb --info "$PACKAGE_NAME" > /dev/null 2>&1; then
echo "Package verification successful" log_success "Package verified"
# Show package info
echo "📦 Package Information:"
dpkg-deb --field "$PACKAGE_NAME" Package Version Architecture Description | head -4
else else
echo "Package verification failed - corrupted download" log_error "Package verification failed - corrupted download"
echo " File size: $(ls -lh "$PACKAGE_NAME" | awk '{print $5}')"
echo " Try downloading manually from: $PACKAGE_URL"
rm -rf "$TEMP_DIR" rm -rf "$TEMP_DIR"
exit 1 exit 1
fi fi
# Install the package # Install the package
echo "" log_info "Installing package..."
echo "📦 Installing Regional Monitoring Agent package..."
if dpkg -i "$PACKAGE_NAME" 2>/dev/null; then if dpkg -i "$PACKAGE_NAME" 2>/dev/null; then
echo "Package installed successfully" log_success "Package installed"
else else
echo "⚠️ Package installation had dependency issues, attempting to fix..." log_warning "Fixing dependencies..."
if apt-get update && apt-get install -f -y; then if apt-get update && apt-get install -f -y; then
echo "✅ Dependencies fixed and package installed successfully" log_success "Package installed with dependencies"
else else
echo "Failed to install package and fix dependencies" log_error "Failed to install package"
echo " This might be due to:" log_info "Try: sudo apt-get update && sudo apt-get install -f"
echo " - Missing system dependencies"
echo " - Architecture compatibility issues"
echo " - Package conflicts"
echo " - Insufficient disk space"
echo ""
echo " Manual resolution:"
echo " 1. Run: sudo apt-get update"
echo " 2. Run: sudo apt-get install -f"
echo " 3. Retry installation: sudo dpkg -i $PACKAGE_NAME"
rm -rf "$TEMP_DIR" rm -rf "$TEMP_DIR"
exit 1 exit 1
fi fi
fi fi
# Configure the agent # Configure the agent
echo "" log_info "Configuring agent..."
echo "⚙️ Configuring Regional Monitoring Agent..."
# Ensure configuration directory exists # Ensure configuration directory exists
mkdir -p /etc/regional-check-agent mkdir -p /etc/regional-check-agent
@@ -293,7 +260,6 @@ mkdir -p /etc/regional-check-agent
cat > /etc/regional-check-agent/regional-check-agent.env << EOF cat > /etc/regional-check-agent/regional-check-agent.env << EOF
# Distributed Regional Check Agent Configuration # Distributed Regional Check Agent Configuration
# Auto-generated on $(date) # Auto-generated on $(date)
# Architecture: $PKG_ARCH
# Server Configuration # Server Configuration
PORT=8091 PORT=8091
@@ -311,7 +277,7 @@ ENABLE_LOGGING=true
POCKETBASE_ENABLED=true POCKETBASE_ENABLED=true
POCKETBASE_URL=$POCKETBASE_URL POCKETBASE_URL=$POCKETBASE_URL
# Regional Agent Configuration - Auto-configured # Regional Agent Configuration
REGION_NAME=$REGION_NAME REGION_NAME=$REGION_NAME
AGENT_ID=$AGENT_ID AGENT_ID=$AGENT_ID
AGENT_IP_ADDRESS=$AGENT_IP_ADDRESS AGENT_IP_ADDRESS=$AGENT_IP_ADDRESS
@@ -323,112 +289,79 @@ MAX_RETRIES=3
REQUEST_TIMEOUT=10s REQUEST_TIMEOUT=10s
EOF EOF
echo "✅ Configuration file created at /etc/regional-check-agent/regional-check-agent.env"
# Set proper permissions # Set proper permissions
if id "regional-check-agent" &>/dev/null; then if id "regional-check-agent" &>/dev/null; then
chown root:regional-check-agent /etc/regional-check-agent/regional-check-agent.env chown root:regional-check-agent /etc/regional-check-agent/regional-check-agent.env
chmod 640 /etc/regional-check-agent/regional-check-agent.env chmod 640 /etc/regional-check-agent/regional-check-agent.env
echo "✅ Configuration file permissions set"
else else
echo "⚠️ regional-check-agent user not found, using root permissions"
chown root:root /etc/regional-check-agent/regional-check-agent.env chown root:root /etc/regional-check-agent/regional-check-agent.env
chmod 600 /etc/regional-check-agent/regional-check-agent.env chmod 600 /etc/regional-check-agent/regional-check-agent.env
fi fi
log_success "Configuration complete"
# Enable and start the service # Enable and start the service
echo "" log_info "Starting service..."
echo "🔧 Starting Regional Monitoring Agent service..."
# Reload systemd daemon # Reload systemd daemon
systemctl daemon-reload systemctl daemon-reload
# Enable the service for auto-start # Enable the service for auto-start
if systemctl enable $SERVICE_NAME; then if systemctl enable $SERVICE_NAME; then
echo "Service enabled for auto-start" log_success "Service enabled"
else else
echo "Failed to enable service" log_error "Failed to enable service"
echo " Check systemd configuration"
rm -rf "$TEMP_DIR" rm -rf "$TEMP_DIR"
exit 1 exit 1
fi fi
# Start the service # Start the service
if systemctl start $SERVICE_NAME; then if systemctl start $SERVICE_NAME; then
echo "Service started successfully" log_success "Service started"
else else
echo "Failed to start service" log_error "Failed to start service"
echo " Common issues:" log_info "Check logs: sudo journalctl -u $SERVICE_NAME -f"
echo " - Configuration errors"
echo " - Port 8091 already in use"
echo " - Permission issues"
echo ""
echo " Troubleshooting:"
echo " - Check logs: sudo journalctl -u $SERVICE_NAME -f"
echo " - Check config: sudo nano /etc/regional-check-agent/regional-check-agent.env"
echo " - Manual start: sudo systemctl start $SERVICE_NAME"
rm -rf "$TEMP_DIR" rm -rf "$TEMP_DIR"
exit 1 exit 1
fi fi
# Wait a moment for service to initialize # Wait a moment for service to initialize
echo "⏳ Waiting for service to initialize..."
sleep 5 sleep 5
# Check service status
echo ""
echo "📊 Service Status:"
systemctl status $SERVICE_NAME --no-pager -l --lines=5
# Test health endpoint # Test health endpoint
echo ""
echo "🩺 Testing agent health endpoint..."
HEALTH_CHECK_ATTEMPTS=3 HEALTH_CHECK_ATTEMPTS=3
HEALTH_CHECK_DELAY=2
for i in $(seq 1 $HEALTH_CHECK_ATTEMPTS); do for i in $(seq 1 $HEALTH_CHECK_ATTEMPTS); do
if curl -s -f --connect-timeout 5 http://localhost:8091/health > /dev/null; then if curl -s -f --connect-timeout 5 http://localhost:8091/health > /dev/null; then
echo "✅ Agent health endpoint is responding" log_success "Health endpoint responding"
HEALTH_OK=true
break break
else else
if [ $i -lt $HEALTH_CHECK_ATTEMPTS ]; then if [ $i -lt $HEALTH_CHECK_ATTEMPTS ]; then
echo "⏳ Health check attempt $i/$HEALTH_CHECK_ATTEMPTS failed, retrying in ${HEALTH_CHECK_DELAY}s..." sleep 2
sleep $HEALTH_CHECK_DELAY
else else
echo "⚠️ Agent health endpoint not responding after $HEALTH_CHECK_ATTEMPTS attempts" log_warning "Health endpoint not responding (service may still be starting)"
echo " This may be normal if the service is still starting up"
echo " Check status later with: curl http://localhost:8091/health"
fi fi
fi fi
done done
# Cleanup # Cleanup
rm -rf "$TEMP_DIR" rm -rf "$TEMP_DIR"
echo "" echo ""
echo "🎉 Regional Monitoring Agent Installation Complete!" echo "============================================="
echo " Installation Complete!"
echo "============================================="
echo "" echo ""
echo "📋 Installation Summary:" log_success "CheckCle Regional Monitoring Agent installed successfully"
echo " Agent ID: $AGENT_ID"
echo " Region: $REGION_NAME"
echo " Architecture: $PKG_ARCH ($ARCH)"
echo " Status: $(systemctl is-active $SERVICE_NAME 2>/dev/null || echo 'unknown')"
echo " Health URL: http://localhost:8091/health"
echo " Service endpoint: http://localhost:8091/operation"
echo " Config file: /etc/regional-check-agent/regional-check-agent.env"
echo "" echo ""
echo "📝 Useful commands:" echo "Agent Details:"
echo " Check status: sudo systemctl status $SERVICE_NAME" echo " Region: $REGION_NAME"
echo " View logs: sudo journalctl -u $SERVICE_NAME -f" echo " Agent ID: $AGENT_ID"
echo " Restart: sudo systemctl restart $SERVICE_NAME" echo " Status: $(systemctl is-active $SERVICE_NAME 2>/dev/null || echo 'unknown')"
echo " Stop: sudo systemctl stop $SERVICE_NAME" echo " Health: http://localhost:8091/health"
echo " Health check: curl http://localhost:8091/health"
echo "" echo ""
echo "🔧 Troubleshooting:" echo "Management Commands:"
echo " If the service fails to start:" echo " Status: sudo systemctl status $SERVICE_NAME"
echo " 1. Check logs: sudo journalctl -u $SERVICE_NAME -n 50" echo " Logs: sudo journalctl -u $SERVICE_NAME -f"
echo " 2. Verify config: cat /etc/regional-check-agent/regional-check-agent.env" echo " Restart: sudo systemctl restart $SERVICE_NAME"
echo " 3. Test connectivity: ping $(echo $POCKETBASE_URL | sed 's|https\?://||' | sed 's|/.*||')"
echo " 4. Check port availability: sudo netstat -tlnp | grep 8091"
echo "" echo ""
echo "✨ The agent is now monitoring and reporting to your CheckCle dashboard!" log_success "Agent is now monitoring and reporting to your dashboard!"
+330 -37
View File
@@ -1,4 +1,3 @@
#!/bin/bash #!/bin/bash
# CheckCle Server Monitoring Agent - One-Click Installation Script # CheckCle Server Monitoring Agent - One-Click Installation Script
@@ -44,7 +43,7 @@ check_root() {
fi fi
} }
# Detect system architecture and package format # Detect system architecture and package format with fallback
detect_system() { detect_system() {
log_info "Detecting system architecture and package format..." log_info "Detecting system architecture and package format..."
@@ -64,28 +63,89 @@ detect_system() {
;; ;;
esac esac
# Detect package format preference # Detect OS and preferred package format
if command -v dpkg >/dev/null 2>&1; then PREFERRED_FORMAT=""
PACKAGE_FORMAT="deb" PREFERRED_MANAGER=""
PACKAGE_MANAGER="dpkg"
elif command -v rpm >/dev/null 2>&1; then if [[ -f /etc/os-release ]]; then
PACKAGE_FORMAT="rpm" OS_ID=$(grep "^ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"')
if command -v yum >/dev/null 2>&1; then OS_LIKE=$(grep "^ID_LIKE=" /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "")
PACKAGE_MANAGER="yum"
elif command -v dnf >/dev/null 2>&1; then log_info "Detected OS: $OS_ID"
PACKAGE_MANAGER="dnf" [[ -n "$OS_LIKE" ]] && log_info "OS family: $OS_LIKE"
else
PACKAGE_MANAGER="rpm" # Determine preferred package format based on OS
fi case "$OS_ID" in
else debian|ubuntu|linuxmint|elementary|pop)
log_error "No supported package manager found (dpkg or rpm required)" PREFERRED_FORMAT="deb"
log_info "This script supports Debian/Ubuntu (.deb) and RHEL/CentOS/Fedora (.rpm) systems" PREFERRED_MANAGER="dpkg"
exit 1 ;;
rhel|centos|fedora|rocky|almalinux|oracle|amazonlinux)
PREFERRED_FORMAT="rpm"
if command -v dnf >/dev/null 2>&1; then
PREFERRED_MANAGER="dnf"
elif command -v yum >/dev/null 2>&1; then
PREFERRED_MANAGER="yum"
else
PREFERRED_MANAGER="rpm"
fi
;;
arch|manjaro|artix|endeavouros)
# Arch-based systems - use tar.gz fallback
log_info "Arch-based system detected, using tar.gz format"
PREFERRED_FORMAT="tar.gz"
PREFERRED_MANAGER="tar"
;;
*)
# Check OS_LIKE for family detection
if [[ "$OS_LIKE" == *"debian"* ]] || [[ "$OS_LIKE" == *"ubuntu"* ]]; then
PREFERRED_FORMAT="deb"
PREFERRED_MANAGER="dpkg"
elif [[ "$OS_LIKE" == *"rhel"* ]] || [[ "$OS_LIKE" == *"fedora"* ]]; then
PREFERRED_FORMAT="rpm"
if command -v dnf >/dev/null 2>&1; then
PREFERRED_MANAGER="dnf"
elif command -v yum >/dev/null 2>&1; then
PREFERRED_MANAGER="yum"
else
PREFERRED_MANAGER="rpm"
fi
elif [[ "$OS_LIKE" == *"arch"* ]]; then
PREFERRED_FORMAT="tar.gz"
PREFERRED_MANAGER="tar"
fi
;;
esac
fi fi
# Construct package filename and URL # If no preferred format detected, check available package managers
PACKAGE_FILENAME="monitoring-agent_1.0.0_${PACKAGE_ARCH}.${PACKAGE_FORMAT}" if [[ -z "$PREFERRED_FORMAT" ]]; then
PACKAGE_URL="${GITHUB_BASE_URL}/${PACKAGE_FILENAME}" if command -v dpkg >/dev/null 2>&1; then
PREFERRED_FORMAT="deb"
PREFERRED_MANAGER="dpkg"
elif command -v rpm >/dev/null 2>&1; then
PREFERRED_FORMAT="rpm"
if command -v dnf >/dev/null 2>&1; then
PREFERRED_MANAGER="dnf"
elif command -v yum >/dev/null 2>&1; then
PREFERRED_MANAGER="yum"
else
PREFERRED_MANAGER="rpm"
fi
else
# Fallback to tar.gz for unsupported systems
log_info "No native package manager found, using tar.gz fallback"
PREFERRED_FORMAT="tar.gz"
PREFERRED_MANAGER="tar"
fi
fi
# Set initial package format and manager
PACKAGE_FORMAT="$PREFERRED_FORMAT"
PACKAGE_MANAGER="$PREFERRED_MANAGER"
# Construct initial package filename and URL
construct_package_url
log_success "System detection complete:" log_success "System detection complete:"
log_info " Architecture: $ARCH -> $PACKAGE_ARCH" log_info " Architecture: $ARCH -> $PACKAGE_ARCH"
@@ -94,6 +154,16 @@ detect_system() {
log_info " Package URL: $PACKAGE_URL" log_info " Package URL: $PACKAGE_URL"
} }
# Construct package URL based on current format
construct_package_url() {
if [[ "$PACKAGE_FORMAT" == "tar.gz" ]]; then
PACKAGE_FILENAME="monitoring-agent_1.0.0_${PACKAGE_ARCH}.tar.gz"
else
PACKAGE_FILENAME="monitoring-agent_1.0.0_${PACKAGE_ARCH}.${PACKAGE_FORMAT}"
fi
PACKAGE_URL="${GITHUB_BASE_URL}/${PACKAGE_FILENAME}"
}
# Validate required environment variables - check both current and sudo environments # Validate required environment variables - check both current and sudo environments
validate_environment() { validate_environment() {
# Check if SERVER_TOKEN is available in current environment or passed as argument # Check if SERVER_TOKEN is available in current environment or passed as argument
@@ -116,7 +186,7 @@ validate_environment() {
[[ -n "$AGENT_ID" ]] && log_info "AGENT_ID: $AGENT_ID" [[ -n "$AGENT_ID" ]] && log_info "AGENT_ID: $AGENT_ID"
} }
# Download package based on detected system # Download package with fallback to tar.gz
download_package() { download_package() {
local temp_dir="/tmp/monitoring-agent-install" local temp_dir="/tmp/monitoring-agent-install"
mkdir -p "$temp_dir" mkdir -p "$temp_dir"
@@ -124,17 +194,211 @@ download_package() {
log_info "Downloading monitoring agent package..." log_info "Downloading monitoring agent package..."
log_info "URL: $PACKAGE_URL" log_info "URL: $PACKAGE_URL"
if curl -L -f -o "$temp_dir/$PACKAGE_FILENAME" "$PACKAGE_URL"; then # Try to download the preferred package format first
DOWNLOADED_PACKAGE="$temp_dir/$PACKAGE_FILENAME" if curl -L -f -s -o "$temp_dir/$PACKAGE_FILENAME" "$PACKAGE_URL" 2>/dev/null; then
log_success "Package downloaded successfully: $PACKAGE_FILENAME" # Verify the file was actually downloaded and has content
if [[ -s "$temp_dir/$PACKAGE_FILENAME" ]]; then
DOWNLOADED_PACKAGE="$temp_dir/$PACKAGE_FILENAME"
log_success "Package downloaded successfully: $PACKAGE_FILENAME"
return 0
else
log_warning "Downloaded file is empty, trying fallback..."
rm -f "$temp_dir/$PACKAGE_FILENAME"
fi
else else
log_error "Failed to download package from: $PACKAGE_URL" log_warning "Failed to download preferred package format: $PACKAGE_FORMAT"
log_info "Please check:" fi
log_info " 1. Internet connectivity"
log_info " 2. Package availability for your architecture ($PACKAGE_ARCH)" # If preferred format failed and it's not tar.gz, try tar.gz fallback
log_info " 3. GitHub repository access" if [[ "$PACKAGE_FORMAT" != "tar.gz" ]]; then
log_warning "Native package ($PACKAGE_FORMAT) not available, trying tar.gz fallback..."
# Switch to tar.gz format
PACKAGE_FORMAT="tar.gz"
PACKAGE_MANAGER="tar"
construct_package_url
log_info "Fallback URL: $PACKAGE_URL"
if curl -L -f -s -o "$temp_dir/$PACKAGE_FILENAME" "$PACKAGE_URL" 2>/dev/null; then
# Verify the file was actually downloaded and has content
if [[ -s "$temp_dir/$PACKAGE_FILENAME" ]]; then
DOWNLOADED_PACKAGE="$temp_dir/$PACKAGE_FILENAME"
log_success "Fallback package downloaded successfully: $PACKAGE_FILENAME"
return 0
else
log_error "Downloaded fallback file is empty"
rm -f "$temp_dir/$PACKAGE_FILENAME"
fi
else
log_error "Failed to download tar.gz fallback"
fi
fi
# Both attempts failed
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
}
# Install tar.gz package
install_tar_package() {
log_info "Installing monitoring agent from tar.gz package..."
local temp_extract_dir="/tmp/monitoring-agent-extract"
mkdir -p "$temp_extract_dir"
# Extract tar.gz package
if tar -xzf "$DOWNLOADED_PACKAGE" -C "$temp_extract_dir"; then
log_success "Package extracted successfully"
else
log_error "Failed to extract tar.gz package"
exit 1 exit 1
fi fi
# Install files to their proper locations
log_info "Installing files to system directories..."
# Create necessary directories
mkdir -p /usr/bin
mkdir -p /etc/monitoring-agent
mkdir -p /var/lib/monitoring-agent
mkdir -p /var/log/monitoring-agent
# Detect systemd service directory based on OS
if [[ -d /lib/systemd/system ]]; then
SYSTEMD_DIR="/lib/systemd/system"
elif [[ -d /usr/lib/systemd/system ]]; then
SYSTEMD_DIR="/usr/lib/systemd/system"
else
SYSTEMD_DIR="/etc/systemd/system"
fi
mkdir -p "$SYSTEMD_DIR"
# Copy files from extracted package
if [[ -f "$temp_extract_dir/usr/bin/monitoring-agent" ]]; then
cp "$temp_extract_dir/usr/bin/monitoring-agent" /usr/bin/
chmod +x /usr/bin/monitoring-agent
log_success "Binary installed to /usr/bin/monitoring-agent"
else
log_error "Binary not found in package"
exit 1
fi
if [[ -f "$temp_extract_dir/etc/monitoring-agent/monitoring-agent.env" ]]; then
cp "$temp_extract_dir/etc/monitoring-agent/monitoring-agent.env" /etc/monitoring-agent/
log_success "Configuration template installed"
fi
# Create monitoring-agent user and group if they don't exist
if ! getent group monitoring-agent >/dev/null 2>&1; then
groupadd --system monitoring-agent
log_success "Created monitoring-agent group"
else
log_info "monitoring-agent group already exists"
fi
if ! getent passwd monitoring-agent >/dev/null 2>&1; then
useradd --system --gid monitoring-agent --home-dir /var/lib/monitoring-agent --shell /bin/false --comment "Monitoring Agent" monitoring-agent
log_success "Created monitoring-agent user"
else
log_info "monitoring-agent user already exists"
fi
# Set proper ownership and permissions
chown -R monitoring-agent:monitoring-agent /var/lib/monitoring-agent
chown -R monitoring-agent:monitoring-agent /var/log/monitoring-agent
chown -R monitoring-agent:monitoring-agent /etc/monitoring-agent
chmod 755 /var/lib/monitoring-agent
chmod 755 /var/log/monitoring-agent
chmod 755 /etc/monitoring-agent
# Check if Docker is installed and configure service file accordingly
DOCKER_AVAILABLE=false
if command -v docker >/dev/null 2>&1 && getent group docker >/dev/null 2>&1; then
DOCKER_AVAILABLE=true
# Add monitoring-agent user to docker group if Docker is available
usermod -a -G docker monitoring-agent
log_info "Docker detected, added monitoring-agent user to docker group"
else
log_info "Docker not detected, skipping Docker group configuration"
fi
# Create the systemd service file with proper Docker configuration
create_systemd_service_file "$SYSTEMD_DIR" "$DOCKER_AVAILABLE"
# Clean up extraction directory
rm -rf "$temp_extract_dir"
log_success "Tar.gz package installation completed"
}
# Create systemd service file with Docker configuration
create_systemd_service_file() {
local systemd_dir="$1"
local docker_available="$2"
log_info "Creating systemd service file at $systemd_dir/monitoring-agent.service"
cat > "$systemd_dir/monitoring-agent.service" << 'EOF'
[Unit]
Description=CheckCle Server Monitoring Agent
Documentation=https://github.com/operacle/checkcle-server-agent
After=network.target
Wants=network.target
[Service]
Type=simple
User=monitoring-agent
Group=monitoring-agent
EOF
# Add Docker group only if Docker is available
if [[ "$docker_available" == "true" ]]; then
echo "# Add docker group for Docker monitoring access" >> "$systemd_dir/monitoring-agent.service"
echo "SupplementaryGroups=docker" >> "$systemd_dir/monitoring-agent.service"
else
echo "# Docker not available - skipping Docker group configuration" >> "$systemd_dir/monitoring-agent.service"
fi
# Continue with the rest of the service configuration
cat >> "$systemd_dir/monitoring-agent.service" << 'EOF'
ExecStart=/usr/bin/monitoring-agent
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
# Security settings
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RemoveIPC=yes
RestrictRealtime=yes
# Allow access to system information and configuration
ReadWritePaths=/var/log/monitoring-agent
ReadOnlyPaths=/proc /sys /etc/monitoring-agent
# Network access
PrivateNetwork=no
# Environment
EnvironmentFile=/etc/monitoring-agent/monitoring-agent.env
WorkingDirectory=/var/lib/monitoring-agent
[Install]
WantedBy=multi-user.target
EOF
log_success "Created systemd service file with Docker support: $docker_available"
} }
# Install package based on detected package manager # Install package based on detected package manager
@@ -185,6 +449,10 @@ install_package() {
fi fi
;; ;;
tar)
install_tar_package
;;
*) *)
log_error "Unsupported package manager: $PACKAGE_MANAGER" log_error "Unsupported package manager: $PACKAGE_MANAGER"
exit 1 exit 1
@@ -232,7 +500,7 @@ configure_agent() {
log_info "Final configuration:" log_info "Final configuration:"
log_info " Server Name: $SERVER_NAME" log_info " Server Name: $SERVER_NAME"
log_info " Agent ID: $AGENT_ID" log_info " Agent ID: $AGENT_ID"
log_info " PB API URL: $POCKETBASE_URL" log_info " PocketBase URL: $POCKETBASE_URL"
log_info " IP Address: $IP_ADDRESS" log_info " IP Address: $IP_ADDRESS"
log_info " OS Type: $OS_TYPE" log_info " OS Type: $OS_TYPE"
log_info " Health Check Port: $HEALTH_CHECK_PORT" log_info " Health Check Port: $HEALTH_CHECK_PORT"
@@ -341,7 +609,7 @@ test_installation() {
show_post_install_info() { show_post_install_info() {
echo echo
echo "=============================================" echo "============================================="
echo " CheckCle Server Agent Installation Complete!" echo " Installation Complete!"
echo "=============================================" echo "============================================="
echo echo
log_success "CheckCle Monitoring Agent installed and configured successfully" log_success "CheckCle Monitoring Agent installed and configured successfully"
@@ -350,6 +618,11 @@ show_post_install_info() {
echo " Architecture: $ARCH ($PACKAGE_ARCH)" echo " Architecture: $ARCH ($PACKAGE_ARCH)"
echo " Package: $PACKAGE_FILENAME" echo " Package: $PACKAGE_FILENAME"
echo " Package Manager: $PACKAGE_MANAGER" echo " Package Manager: $PACKAGE_MANAGER"
if [[ "$PACKAGE_FORMAT" == "tar.gz" ]]; then
echo " Installation Type: Manual (tar.gz)"
else
echo " Installation Type: Native package ($PACKAGE_FORMAT)"
fi
echo echo
echo "Configuration: $CONFIG_FILE" echo "Configuration: $CONFIG_FILE"
echo "Service status: systemctl status monitoring-agent" echo "Service status: systemctl status monitoring-agent"
@@ -416,7 +689,12 @@ case "${1:-}" in
echo "System Requirements:" echo "System Requirements:"
echo " - Linux with systemd" echo " - Linux with systemd"
echo " - Supported architectures: x86_64 (amd64), aarch64/arm64" echo " - Supported architectures: x86_64 (amd64), aarch64/arm64"
echo " - Supported distributions: Debian/Ubuntu (.deb), RHEL/CentOS/Fedora (.rpm)" echo " - Supported distributions: Debian/Ubuntu (.deb), RHEL/CentOS/Fedora (.rpm), or any Linux (tar.gz fallback)"
echo
echo "Package Installation Logic:"
echo " 1. Detects OS and tries native package format first (.deb for Debian-based, .rpm for RHEL-based)"
echo " 2. If native package is not available, automatically falls back to tar.gz"
echo " 3. tar.gz works on any Linux distribution with systemd"
echo echo
echo "Required Environment Variables:" echo "Required Environment Variables:"
echo " SERVER_TOKEN Server authentication token" echo " SERVER_TOKEN Server authentication token"
@@ -444,13 +722,28 @@ case "${1:-}" in
systemctl disable monitoring-agent 2>/dev/null || true systemctl disable monitoring-agent 2>/dev/null || true
# Remove based on detected package manager # Remove based on detected package manager
if command -v dpkg >/dev/null 2>&1; then if command -v dpkg >/dev/null 2>&1 && dpkg -l monitoring-agent >/dev/null 2>&1; then
dpkg -r monitoring-agent 2>/dev/null || true dpkg -r monitoring-agent 2>/dev/null || true
elif command -v rpm >/dev/null 2>&1; then log_success "DEB package removed"
elif command -v rpm >/dev/null 2>&1 && rpm -q monitoring-agent >/dev/null 2>&1; then
rpm -e monitoring-agent 2>/dev/null || true rpm -e monitoring-agent 2>/dev/null || true
log_success "RPM package removed"
else
# Manual removal for tar.gz installations
log_info "Performing manual cleanup for tar.gz installation..."
rm -f /usr/bin/monitoring-agent 2>/dev/null || true
rm -rf /etc/monitoring-agent 2>/dev/null || true
rm -f /lib/systemd/system/monitoring-agent.service 2>/dev/null || true
rm -f /usr/lib/systemd/system/monitoring-agent.service 2>/dev/null || true
rm -f /etc/systemd/system/monitoring-agent.service 2>/dev/null || true
userdel monitoring-agent 2>/dev/null || true
groupdel monitoring-agent 2>/dev/null || true
rm -rf /var/lib/monitoring-agent 2>/dev/null || true
rm -rf /var/log/monitoring-agent 2>/dev/null || true
log_success "Manual cleanup completed"
fi fi
rm -rf /etc/monitoring-agent 2>/dev/null || true systemctl daemon-reload 2>/dev/null || true
log_success "Monitoring agent uninstalled" log_success "Monitoring agent uninstalled"
exit 0 exit 0
;; ;;
+361
View File
@@ -0,0 +1,361 @@
#!/bin/bash
# CheckCle Server Monitoring Agent - Docker One-Click Installation Script
# This script provides fully automated Docker installation using environment variables
set -e
# 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
}
# Check if Docker is installed
check_docker() {
if ! command -v docker >/dev/null 2>&1; then
log_error "Docker is not installed"
log_info "Please install Docker first:"
log_info " Ubuntu/Debian: sudo apt-get update && sudo apt-get install docker.io"
log_info " CentOS/RHEL: sudo yum install docker"
log_info " Or visit: https://docs.docker.com/engine/install/"
exit 1
fi
# Check if Docker daemon is running
if ! docker info >/dev/null 2>&1; then
log_error "Docker daemon is not running"
log_info "Start Docker with: sudo systemctl start docker"
exit 1
fi
log_success "Docker is installed and running"
}
# Validate required environment variables
validate_environment() {
local missing_vars=()
# Check required variables
[[ -z "$SERVER_TOKEN" ]] && missing_vars+=("SERVER_TOKEN")
[[ -z "$POCKETBASE_URL" ]] && missing_vars+=("POCKETBASE_URL")
[[ -z "$SERVER_NAME" ]] && missing_vars+=("SERVER_NAME")
[[ -z "$AGENT_ID" ]] && missing_vars+=("AGENT_ID")
if [[ ${#missing_vars[@]} -ne 0 ]]; then
log_error "Missing required environment variables: ${missing_vars[*]}"
log_info ""
log_info "Usage examples:"
log_info " SERVER_TOKEN=your-token POCKETBASE_URL=http://host:8090 SERVER_NAME=server AGENT_ID=agent sudo -E bash $0"
log_info " sudo SERVER_TOKEN=your-token POCKETBASE_URL=http://host:8090 SERVER_NAME=server AGENT_ID=agent bash $0"
log_info ""
log_info "Required variables:"
log_info " SERVER_TOKEN Server authentication token"
log_info " POCKETBASE_URL PocketBase URL (e.g., http://194.233.80.126:8090)"
log_info " SERVER_NAME Server name (e.g., DB-Server01)"
log_info " AGENT_ID Agent identifier (e.g., agent_hogr3np88u7dq5xf9balzl)"
log_info ""
log_info "Optional variables:"
log_info " HEALTH_CHECK_PORT Health check port (default: 8081)"
log_info " CONTAINER_NAME Container name (default: monitoring-agent)"
log_info " DOCKER_IMAGE Docker image (default: operacle/checkcle-server-agent:latest)"
exit 1
fi
log_success "Environment validation passed"
log_info "SERVER_TOKEN: ${SERVER_TOKEN:0:8}..."
log_info "POCKETBASE_URL: $POCKETBASE_URL"
log_info "SERVER_NAME: $SERVER_NAME"
log_info "AGENT_ID: $AGENT_ID"
}
# Set default values for optional variables
set_defaults() {
HEALTH_CHECK_PORT="${HEALTH_CHECK_PORT:-8081}"
CONTAINER_NAME="${CONTAINER_NAME:-monitoring-agent}"
DOCKER_IMAGE="${DOCKER_IMAGE:-operacle/checkcle-server-agent:latest}"
POCKETBASE_ENABLED="${POCKETBASE_ENABLED:-true}"
log_info "Configuration:"
log_info " Container name: $CONTAINER_NAME"
log_info " Docker image: $DOCKER_IMAGE"
log_info " Health check port: $HEALTH_CHECK_PORT"
log_info " PocketBase enabled: $POCKETBASE_ENABLED"
}
# Stop and remove existing container if it exists
cleanup_existing_container() {
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
log_info "Stopping and removing existing container: $CONTAINER_NAME"
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true
log_success "Existing container removed"
fi
}
# Create Docker volumes
create_volumes() {
log_info "Creating Docker volumes..."
# Create volumes if they don't exist
if ! docker volume ls --format '{{.Name}}' | grep -q "^monitoring_data$"; then
docker volume create monitoring_data
log_success "Created monitoring_data volume"
else
log_info "monitoring_data volume already exists"
fi
if ! docker volume ls --format '{{.Name}}' | grep -q "^monitoring_logs$"; then
docker volume create monitoring_logs
log_success "Created monitoring_logs volume"
else
log_info "monitoring_logs volume already exists"
fi
}
# Get Docker group ID
get_docker_group_id() {
local docker_gid
docker_gid=$(getent group docker | cut -d: -f3)
if [[ -z "$docker_gid" ]]; then
log_warning "Docker group not found, using default GID 999"
docker_gid=999
fi
echo "$docker_gid"
}
# Run the Docker container
run_docker_container() {
log_info "Starting monitoring agent Docker container..."
local docker_gid
docker_gid=$(get_docker_group_id)
log_info "Docker group ID: $docker_gid"
# Run the Docker container with all the required parameters
docker run -d \
--name "$CONTAINER_NAME" \
--restart unless-stopped \
-p "$HEALTH_CHECK_PORT:8081" \
--group-add "$docker_gid" \
-e AGENT_ID="$AGENT_ID" \
-e SERVER_NAME="$SERVER_NAME" \
-e SERVER_TOKEN="$SERVER_TOKEN" \
-e POCKETBASE_URL="$POCKETBASE_URL" \
-e POCKETBASE_ENABLED="$POCKETBASE_ENABLED" \
-v /proc:/host/proc:ro \
-v /etc:/host/etc:ro \
-v /sys:/host/sys:ro \
-v /:/host/root:ro \
-v /var/run:/host/var/run:ro \
-v /dev:/host/dev:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v monitoring_data:/var/lib/monitoring-agent \
-v monitoring_logs:/var/log/monitoring-agent \
"$DOCKER_IMAGE"
log_success "Docker container started successfully"
}
# Wait for container to be ready
wait_for_container() {
log_info "Waiting for container to be ready..."
local max_attempts=30
local attempt=0
while [[ $attempt -lt $max_attempts ]]; do
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
log_success "Container is running"
return 0
fi
sleep 1
((attempt++))
done
log_error "Container failed to start within expected time"
return 1
}
# Test the installation
test_installation() {
log_info "Testing installation..."
# Check container status
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
log_error "Container is not running"
log_info "Check container logs: docker logs $CONTAINER_NAME"
return 1
fi
# Wait a moment for the service to start
sleep 3
# Test health endpoint
log_info "Testing health endpoint at http://localhost:$HEALTH_CHECK_PORT/health"
if curl -s -f "http://localhost:$HEALTH_CHECK_PORT/health" >/dev/null 2>&1; then
log_success "Health endpoint is responding"
else
log_warning "Health endpoint not responding yet (service may still be starting)"
fi
# Show recent container logs
log_info "Recent container logs:"
docker logs --tail 10 "$CONTAINER_NAME"
}
# Show post-installation information
show_post_install_info() {
echo
echo "============================================="
echo " Docker Installation Complete!"
echo "============================================="
echo
log_success "CheckCle Monitoring Agent Docker container installed and running successfully"
echo
echo "Container Information:"
echo " Container name: $CONTAINER_NAME"
echo " Docker image: $DOCKER_IMAGE"
echo " Health check port: $HEALTH_CHECK_PORT"
echo " Agent ID: $AGENT_ID"
echo " Server name: $SERVER_NAME"
echo
echo "Useful commands:"
echo " Container status: docker ps -f name=$CONTAINER_NAME"
echo " Container logs: docker logs -f $CONTAINER_NAME"
echo " Stop container: docker stop $CONTAINER_NAME"
echo " Start container: docker start $CONTAINER_NAME"
echo " Remove container: docker rm -f $CONTAINER_NAME"
echo " Health check: curl http://localhost:$HEALTH_CHECK_PORT/health"
echo
echo "The monitoring agent is now running in a Docker container and will appear in your dashboard."
echo
}
# Main installation function
main() {
echo "============================================="
echo " CheckCle Server Monitoring Agent"
echo " Docker One-Click Installation"
echo "============================================="
echo
check_root
check_docker
validate_environment
set_defaults
log_info "Starting Docker container installation..."
# Setup and run container
cleanup_existing_container
create_volumes
run_docker_container
if wait_for_container; then
test_installation
show_post_install_info
else
log_error "Container installation failed"
log_info "Check container logs: docker logs $CONTAINER_NAME"
exit 1
fi
}
# Handle script arguments
case "${1:-}" in
--help|-h)
echo "CheckCle Server Monitoring Agent - Docker One-Click Installer"
echo
echo "Usage: SERVER_TOKEN=token POCKETBASE_URL=url SERVER_NAME=name AGENT_ID=id sudo bash $0"
echo
echo "System Requirements:"
echo " - Docker installed and running"
echo " - Root privileges (sudo)"
echo " - Internet access to pull Docker image"
echo
echo "Required Environment Variables:"
echo " SERVER_TOKEN Server authentication token"
echo " POCKETBASE_URL PocketBase URL (e.g., http://194.233.80.126:8090)"
echo " SERVER_NAME Server name (e.g., DB-Server01)"
echo " AGENT_ID Agent identifier"
echo
echo "Optional Environment Variables:"
echo " HEALTH_CHECK_PORT Health check port (default: 8081)"
echo " CONTAINER_NAME Container name (default: monitoring-agent)"
echo " DOCKER_IMAGE Docker image (default: operacle/checkcle-server-agent:latest)"
echo " POCKETBASE_ENABLED Enable PocketBase (default: true)"
echo
echo "Examples:"
echo " SERVER_TOKEN=abc123 POCKETBASE_URL=http://host:8090 SERVER_NAME=server AGENT_ID=agent sudo -E bash $0"
echo " sudo SERVER_TOKEN=abc123 POCKETBASE_URL=http://host:8090 SERVER_NAME=server AGENT_ID=agent bash $0"
echo
echo "Options:"
echo " --help, -h Show this help message"
echo " --uninstall Remove the monitoring agent container"
echo
exit 0
;;
--uninstall)
check_root
check_docker
set_defaults
log_info "Uninstalling monitoring agent Docker container..."
# Stop and remove container
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true
log_success "Container removed"
else
log_info "Container not found"
fi
# Optionally remove volumes (ask user)
echo -n "Remove data volumes? (y/N): "
read -r response
if [[ "$response" =~ ^[Yy]$ ]]; then
docker volume rm monitoring_data monitoring_logs >/dev/null 2>&1 || true
log_success "Data volumes removed"
else
log_info "Data volumes preserved"
fi
log_success "Monitoring agent Docker container uninstalled"
exit 0
;;
*)
main "$@"
;;
esac