Merge branch 'operacle:develop' into develop

This commit is contained in:
gnsworks
2025-08-09 08:54:50 +09:00
committed by GitHub
24 changed files with 615 additions and 409 deletions
+13 -5
View File
@@ -126,18 +126,26 @@ docker run -d \
### 🥈 Silver Uptime Ally ### 🥈 Silver Uptime Ally
<a href="https://github.com/sponsors/tolaleng"> <a href="https://github.com/michaelortnerit">
<img src="https://avatars.githubusercontent.com/u/135371107?v=4" width="75" height="75" style="border-radius: 50%" /> <img src="https://avatars.githubusercontent.com/u/135371107?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
</a> </a>
### 🧡 Ping Supporter ### 🧡 Ping Supporter
<a href="https://github.com/sponsors/tolaleng"> <a href="https://github.com/samang-dauth">
<img src="https://avatars.githubusercontent.com/u/4575656?v=4" width="75" height="75" style="border-radius: 50%" /> <img src="https://avatars.githubusercontent.com/u/4575656?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
</a>
### 🤝 Ecosystem & Community Partner
<a href="https://github.com/gitbookio">
<img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4"
width="75" height="75"
style="border-radius: 50%; display: block;" />
</a> </a>
--- ---
## 👥 Contributors ## 👥 Contributors
Thank you for contributing and continuously making CheckCle better, you're awesome 🫶 Thank you for contributing and continuously making CheckCle better, you're awesome 🫶
@@ -159,7 +167,7 @@ Here are some ways you can help improve CheckCle:
## 🌍 Stay Connected ## 🌍 Stay Connected
- Website: [checkcle.io](https://checkcle.io) - Website: [checkcle.io](https://checkcle.io)
- Documentation: [docs.checkcle.io](https://docs.checkcle.io) - Documentation: [docs.checkcle.io](https://docs.checkcle.io) | Big thanks to [GitBook](https://github.com/gitbookio) for sponsoring the OSS site plan for CheckCle!
- Chat on Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX) - Chat on Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
- Follow us on X: [@checkcle_oss](https://x.com/checkcle_oss) - Follow us on X: [@checkcle_oss](https://x.com/checkcle_oss)
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -1,15 +1,13 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AuthUser } from "@/services/authService"; import { AuthUser } from "@/services/authService";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut, Grid3x3 } from "lucide-react"; import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useSystemSettings } from "@/hooks/useSystemSettings"; import { useSystemSettings } from "@/hooks/useSystemSettings";
import QuickActionsDialog from "./QuickActionsDialog";
interface HeaderProps { interface HeaderProps {
currentUser: AuthUser | null; currentUser: AuthUser | null;
@@ -29,7 +27,6 @@ export const Header = ({
const [greeting, setGreeting] = useState<string>(""); const [greeting, setGreeting] = useState<string>("");
const { systemName } = useSystemSettings(); const { systemName } = useSystemSettings();
const navigate = useNavigate(); const navigate = useNavigate();
const [quickActionsOpen, setQuickActionsOpen] = useState(false);
// Set greeting based on time of day // Set greeting based on time of day
useEffect(() => { useEffect(() => {
@@ -53,7 +50,7 @@ export const Header = ({
// Log avatar data for debugging // Log avatar data for debugging
useEffect(() => { useEffect(() => {
if (currentUser) { if (currentUser) {
// console.log("Avatar URL in Header:", currentUser.avatar); //console.log("Avatar URL in Header:", currentUser.avatar);
} }
}, [currentUser]); }, [currentUser]);
@@ -66,7 +63,7 @@ export const Header = ({
} else { } else {
avatarUrl = currentUser.avatar; avatarUrl = currentUser.avatar;
} }
// console.log("Final avatar URL:", avatarUrl); console.log("Final avatar URL:", avatarUrl);
} }
return ( return (
@@ -91,16 +88,6 @@ export const Header = ({
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />} {sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
</Button> </Button>
{/* Quick Actions Button */}
<Button
variant="ghost"
size="icon"
onClick={() => setQuickActionsOpen(true)}
className="mr-2"
>
<Grid3x3 className="h-5 w-5 text-green-500" />
</Button>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 </h1> <h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 </h1>
</div> </div>
@@ -162,7 +149,7 @@ export const Header = ({
variant="outline" variant="outline"
size="icon" size="icon"
className="rounded-full w-8 h-8 border-border" className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://x.com/checkcle_oss", "_blank")} onClick={() => window.open("https://x.com/checkcle_oss)", "_blank")}
> >
<span className="sr-only">X (Twitter)</span> <span className="sr-only">X (Twitter)</span>
<Twitter className="w-4 h-4" /> <Twitter className="w-4 h-4" />
@@ -223,9 +210,6 @@ export const Header = ({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
{/* Quick Actions Dialog */}
<QuickActionsDialog isOpen={quickActionsOpen} setIsOpen={setQuickActionsOpen} />
</header> </header>
); );
}; };
@@ -1,145 +0,0 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogClose
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
interface QuickActionsDialogProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
}
export const QuickActionsDialog: React.FC<QuickActionsDialogProps> = ({ isOpen, setIsOpen }) => {
const { t } = useLanguage();
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-[800px] bg-background max-h-[90vh] overflow-y-auto">
<DialogClose className="absolute right-4 top-4">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogClose>
<DialogHeader>
<DialogTitle className="text-xl font-bold">{t('quickActions')}</DialogTitle>
<DialogDescription>
{t('quickActionsDescription')}
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
{/* Website Monitoring Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('monitorWebsite')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('monitorWebsiteDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('monitorMultipleEndpoints')}</li>
<li>{t('trackResponseTimes')}</li>
<li>{t('setCustomAlerts')}</li>
</ul>
</div>
</div>
{/* Server Monitoring Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('monitorServer')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('monitorServerDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('monitorCPURam')}</li>
<li>{t('trackNetworkMetrics')}</li>
<li>{t('viewProcesses')}</li>
</ul>
</div>
</div>
{/* SSL Certificate Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('sslCertificate')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('sslCertificateDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('trackSSLExpiration')}</li>
<li>{t('getEarlyRenewal')}</li>
<li>{t('monitorMultipleDomains')}</li>
</ul>
</div>
</div>
{/* Incidents Management Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('incidentsManagement')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('incidentsManagementDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('createTrackIncidents')}</li>
<li>{t('assignIncidents')}</li>
<li>{t('monitorResolution')}</li>
</ul>
</div>
</div>
{/* Operational Status Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('operationalStatus')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('operationalStatusDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('createCustomStatus')}</li>
<li>{t('displayRealTime')}</li>
<li>{t('shareIncidentUpdates')}</li>
</ul>
</div>
</div>
{/* Reports & Analytics Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('reportsAnalytics')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('reportsAnalyticsDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('generateUptimeReports')}</li>
<li>{t('analyzePerformance')}</li>
<li>{t('exportMonitoringData')}</li>
</ul>
</div>
</div>
</div>
<div className="flex justify-end mt-6">
<Button variant="outline" onClick={() => setIsOpen(false)}>
{t('close')}
</Button>
</div>
</DialogContent>
</Dialog>
);
};
export default QuickActionsDialog;
@@ -86,7 +86,7 @@ sudo -E bash ./server-docker-agent.sh`;
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-blue-700 dark:text-blue-400"> <CardTitle className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
<Container className="h-5 w-5" /> <Container className="h-5 w-5" />
Docker One-Click Install (Recommended) Docker One-Click Install
</CardTitle> </CardTitle>
<CardDescription className="text-blue-600 dark:text-blue-300"> <CardDescription className="text-blue-600 dark:text-blue-300">
Automated Docker container installation with system monitoring capabilities Automated Docker container installation with system monitoring capabilities
@@ -1,4 +1,3 @@
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";
@@ -79,7 +78,6 @@ 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);
// 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)
@@ -110,10 +108,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
// Load existing threshold data when thresholds are loaded and we have a server with threshold_id // Load existing threshold data when thresholds are loaded and we have a server with threshold_id
useEffect(() => { useEffect(() => {
if (server && server.threshold_id && thresholds.length > 0) { if (server && server.threshold_id && thresholds.length > 0) {
// console.log("Loading existing threshold data for server:", server.threshold_id);
const existingThreshold = thresholds.find(t => t.id === server.threshold_id); const existingThreshold = thresholds.find(t => t.id === server.threshold_id);
if (existingThreshold) { if (existingThreshold) {
// console.log("Found existing threshold:", existingThreshold);
setSelectedThreshold(existingThreshold); setSelectedThreshold(existingThreshold);
// Handle the API response format with proper field names and type conversion // Handle the API response format with proper field names and type conversion
setThresholdFormData({ setThresholdFormData({
@@ -167,7 +163,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const configs = await alertConfigService.getAlertConfigurations(); const configs = await alertConfigService.getAlertConfigurations();
setAlertConfigs(configs); setAlertConfigs(configs);
} catch (error) { } catch (error) {
// console.error('Error loading alert configurations:', error);
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error", title: "Error",
@@ -185,7 +180,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
// Cast to ServerNotificationTemplate[] since we know we're getting server templates // Cast to ServerNotificationTemplate[] since we know we're getting server templates
setTemplates(templateList as ServerNotificationTemplate[]); setTemplates(templateList as ServerNotificationTemplate[]);
} catch (error) { } catch (error) {
// console.error('Error loading templates:', error);
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error", title: "Error",
@@ -202,7 +196,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const thresholdList = await serverThresholdService.getServerThresholds(); const thresholdList = await serverThresholdService.getServerThresholds();
setThresholds(thresholdList); setThresholds(thresholdList);
} catch (error) { } catch (error) {
// console.error('Error loading server thresholds:', error);
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error", title: "Error",
@@ -213,47 +206,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
} }
}; };
const handleThresholdUpdate = async () => {
if (!selectedThreshold) return;
try {
// Use the correct field name for RAM threshold
const updateData = {
cpu_threshold: thresholdFormData.cpu_threshold,
ram_threshold_message: thresholdFormData.ram_threshold, // Use the correct field name
disk_threshold: thresholdFormData.disk_threshold,
network_threshold: thresholdFormData.network_threshold,
};
await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateData);
// Update local state
setSelectedThreshold({
...selectedThreshold,
...thresholdFormData,
});
// Update thresholds list
setThresholds(prev => prev.map(t =>
t.id === selectedThreshold.id
? { ...t, ...thresholdFormData }
: t
));
toast({
title: "Threshold updated",
description: "Server threshold values have been updated successfully.",
});
} catch (error) {
// console.error('Error updating threshold:', error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to update threshold values.",
});
}
};
const handleNotificationChannelToggle = (channelId: string, checked: boolean) => { const handleNotificationChannelToggle = (channelId: string, checked: boolean) => {
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
@@ -294,6 +246,40 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
await pb.collection('servers').update(server.id, updateData); await pb.collection('servers').update(server.id, updateData);
// Update threshold if a threshold is selected and values have been modified
if (selectedThreshold && formData.threshold_id !== "none") {
try {
const updateThresholdData = {
cpu_threshold: thresholdFormData.cpu_threshold,
ram_threshold_message: thresholdFormData.ram_threshold, // Use the correct field name
disk_threshold: thresholdFormData.disk_threshold,
network_threshold: thresholdFormData.network_threshold,
};
await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateThresholdData);
// Update local state
setSelectedThreshold({
...selectedThreshold,
...thresholdFormData,
});
// Update thresholds list
setThresholds(prev => prev.map(t =>
t.id === selectedThreshold.id
? { ...t, ...thresholdFormData }
: t
));
} catch (error) {
toast({
variant: "destructive",
title: "Warning",
description: "Server updated but failed to update threshold values.",
});
}
}
toast({ toast({
title: "Server updated", title: "Server updated",
description: `${formData.name} has been updated successfully.`, description: `${formData.name} has been updated successfully.`,
@@ -303,7 +289,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
onOpenChange(false); onOpenChange(false);
} catch (error) { } catch (error) {
// console.error('Error updating server:', error);
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error", title: "Error",
@@ -518,16 +503,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
{/* Editable Threshold Details */} {/* Editable Threshold Details */}
{selectedThreshold && ( {selectedThreshold && (
<Card className="bg-muted/50"> <Card className="bg-muted/50">
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader>
<CardTitle className="text-base">Threshold Details: {selectedThreshold.name}</CardTitle> <CardTitle className="text-base">Threshold Details: {selectedThreshold.name}</CardTitle>
<Button
type="button"
onClick={handleThresholdUpdate}
size="sm"
variant="outline"
>
Update Thresholds
</Button>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-3 text-sm"> <div className="grid grid-cols-2 gap-3 text-sm">
@@ -55,7 +55,7 @@ sudo -E bash ./server-agent.sh`;
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-green-700 dark:text-green-400"> <CardTitle className="flex items-center gap-2 text-green-700 dark:text-green-400">
<Download className="h-5 w-5" /> <Download className="h-5 w-5" />
One-Click Install One-Click Install (Recommended)
</CardTitle> </CardTitle>
<CardDescription className="text-green-600 dark:text-green-300"> <CardDescription className="text-green-600 dark:text-green-300">
Copy and paste this single command to install the monitoring agent instantly Copy and paste this single command to install the monitoring agent instantly
@@ -1,4 +1,3 @@
import { FormControl, FormField, FormItem, FormLabel, FormDescription } from "@/components/ui/form"; import { FormControl, FormField, FormItem, FormLabel, FormDescription } from "@/components/ui/form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
@@ -8,6 +7,7 @@ import { UseFormReturn } from "react-hook-form";
import { ServiceFormData } from "./types"; import { ServiceFormData } from "./types";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
import { serviceNotificationTemplateService, ServiceNotificationTemplate } from "@/services/serviceNotificationTemplateService";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
interface ServiceNotificationFieldsProps { interface ServiceNotificationFieldsProps {
@@ -22,18 +22,18 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
const notificationChannels = form.watch("notificationChannels") || []; const notificationChannels = form.watch("notificationChannels") || [];
const alertTemplate = form.watch("alertTemplate"); const alertTemplate = form.watch("alertTemplate");
// console.log("Current notification values:", {
// notificationStatus,
// notificationChannels,
// alertTemplate
// });
// Fetch alert configurations for notification channels // Fetch alert configurations for notification channels
const { data: alertConfigsData } = useQuery({ const { data: alertConfigsData } = useQuery({
queryKey: ['alertConfigs'], queryKey: ['alertConfigs'],
queryFn: () => alertConfigService.getAlertConfigurations(), queryFn: () => alertConfigService.getAlertConfigurations(),
}); });
// Fetch service notification templates
const { data: serviceTemplates, isLoading: isLoadingTemplates } = useQuery({
queryKey: ['serviceNotificationTemplates'],
queryFn: () => serviceNotificationTemplateService.getTemplates(),
});
// Update alert configs when data is loaded // Update alert configs when data is loaded
useEffect(() => { useEffect(() => {
if (alertConfigsData) { if (alertConfigsData) {
@@ -42,10 +42,16 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
setAlertConfigs(enabledChannels); setAlertConfigs(enabledChannels);
// Debug log to check what alert configs are loaded // Debug log to check what alert configs are loaded
// console.log("Loaded alert configurations:", enabledChannels);
} }
}, [alertConfigsData]); }, [alertConfigsData]);
// Debug log for service templates
useEffect(() => {
if (serviceTemplates) {
// console.log("Loaded service notification templates:", serviceTemplates);
}
}, [serviceTemplates]);
// Log when form values change to debug // Log when form values change to debug
useEffect(() => { useEffect(() => {
// console.log("Notification values changed:", { // console.log("Notification values changed:", {
@@ -159,9 +165,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
control={form.control} control={form.control}
name="alertTemplate" name="alertTemplate"
render={({ field }) => { render={({ field }) => {
// Don't convert existing values to "default" // console.log("Rendering alert template field with value:", field.value);
const displayValue = field.value || "default";
// console.log("Rendering alert template field with value:", displayValue);
return ( return (
<FormItem> <FormItem>
@@ -170,17 +174,20 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
<Select <Select
onValueChange={(value) => { onValueChange={(value) => {
console.log("Alert template changed to:", value); console.log("Alert template changed to:", value);
field.onChange(value === "default" ? "" : value); field.onChange(value);
}} }}
value={displayValue} value={field.value || ""}
disabled={notificationStatus !== "enabled"} disabled={notificationStatus !== "enabled" || isLoadingTemplates}
> >
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}> <SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
<SelectValue placeholder="Select an alert template" /> <SelectValue placeholder={isLoadingTemplates ? "Loading templates..." : "Select an alert template"} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="default">Default</SelectItem> {serviceTemplates?.map((template) => (
{/* Add templates here when available */} <SelectItem key={template.id} value={template.id}>
{template.name}
</SelectItem>
))}
</SelectContent> </SelectContent>
</Select> </Select>
</FormControl> </FormControl>
@@ -1,4 +1,3 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { import {
Dialog, Dialog,
@@ -38,7 +37,7 @@ interface NotificationChannelDialogProps {
const baseSchema = z.object({ 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", "google_chat", "email", "webhook"]),
enabled: z.boolean().default(true), enabled: z.boolean().default(true),
service_id: z.string().default("global"), service_id: z.string().default("global"),
template_id: z.string().optional(), template_id: z.string().optional(),
@@ -65,6 +64,11 @@ const signalSchema = baseSchema.extend({
signal_number: z.string().min(1, "Signal number is required"), signal_number: z.string().min(1, "Signal number is required"),
}); });
const googleChatSchema = baseSchema.extend({
notification_type: z.literal("google_chat"),
google_chat_webhook_url: z.string().url("Must be a valid URL"),
});
const emailSchema = baseSchema.extend({ const emailSchema = baseSchema.extend({
notification_type: z.literal("email"), notification_type: z.literal("email"),
email_address: z.string().email("Valid email is required"), email_address: z.string().email("Valid email is required"),
@@ -90,6 +94,7 @@ const formSchema = z.discriminatedUnion("notification_type", [
discordSchema, discordSchema,
slackSchema, slackSchema,
signalSchema, signalSchema,
googleChatSchema,
emailSchema, emailSchema,
webhookSchema webhookSchema
]); ]);
@@ -97,12 +102,48 @@ const formSchema = z.discriminatedUnion("notification_type", [
type FormValues = z.infer<typeof formSchema>; type FormValues = z.infer<typeof formSchema>;
const notificationTypeOptions = [ const notificationTypeOptions = [
{ value: "telegram", label: "Telegram", description: "Send notifications via Telegram bot" }, {
{ value: "discord", label: "Discord", description: "Send notifications to Discord webhook" }, value: "telegram",
{ value: "slack", label: "Slack", description: "Send notifications to Slack webhook" }, label: "Telegram",
{ value: "signal", label: "Signal", description: "Send notifications via Signal" }, description: "Send notifications via Telegram bot",
{ value: "email", label: "Email", description: "Send notifications via email" }, icon: "/upload/notification/telegram.png"
{ value: "webhook", label: "Webhook", description: "Send notifications to custom webhook" }, },
{
value: "discord",
label: "Discord",
description: "Send notifications to Discord webhook",
icon: "/upload/notification/discord.png"
},
{
value: "slack",
label: "Slack",
description: "Send notifications to Slack webhook",
icon: "/upload/notification/slack.png"
},
{
value: "signal",
label: "Signal",
description: "Send notifications via Signal",
icon: "/upload/notification/signal.png"
},
{
value: "google_chat",
label: "Google Chat",
description: "Send notifications to Google Chat webhook",
icon: "/upload/notification/google.png"
},
{
value: "email",
label: "Email",
description: "Send notifications via email",
icon: "/upload/notification/email.png"
},
{
value: "webhook",
label: "Webhook",
description: "Send notifications to custom webhook",
icon: "/upload/notification/webhook.png"
},
]; ];
const webhookPayloadTemplates = { const webhookPayloadTemplates = {
@@ -236,7 +277,7 @@ export const NotificationChannelDialog = ({
await webhookService.createWebhook(webhookData); await webhookService.createWebhook(webhookData);
} }
} else { } else {
// Handle other notification types with existing service // Handle all other notification types including Google Chat and Email
const configData = { const configData = {
...values, ...values,
service_id: values.service_id || "global", service_id: values.service_id || "global",
@@ -248,7 +289,14 @@ export const NotificationChannelDialog = ({
await alertConfigService.createAlertConfiguration(configData as any); await alertConfigService.createAlertConfiguration(configData as any);
} }
} }
onClose(true); // Close with refresh onClose(true); // Close with refresh
} catch (error) {
toast({
title: "Error",
description: "Failed to save notification channel",
variant: "destructive"
});
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@@ -297,10 +345,17 @@ export const NotificationChannelDialog = ({
<SelectContent> <SelectContent>
{notificationTypeOptions.map((option) => ( {notificationTypeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}> <SelectItem key={option.value} value={option.value}>
<div className="flex items-center space-x-3">
<img
src={option.icon}
alt={`${option.label} icon`}
className="w-5 h-5 object-contain"
/>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-medium">{option.label}</span> <span className="font-medium">{option.label}</span>
<span className="text-xs text-muted-foreground">{option.description}</span> <span className="text-xs text-muted-foreground">{option.description}</span>
</div> </div>
</div>
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -310,7 +365,6 @@ export const NotificationChannelDialog = ({
)} )}
/> />
{/* Show different fields based on notification type */}
{notificationType === "telegram" && ( {notificationType === "telegram" && (
<> <>
<FormField <FormField
@@ -405,6 +459,25 @@ export const NotificationChannelDialog = ({
/> />
)} )}
{notificationType === "google_chat" && (
<FormField
control={form.control}
name="google_chat_webhook_url"
render={({ field }) => (
<FormItem>
<FormLabel>Google Chat Webhook URL</FormLabel>
<FormControl>
<Input placeholder="https://chat.googleapis.com/v1/spaces/..." {...field} />
</FormControl>
<FormDescription>
Google Chat webhook URL from your Google Chat space
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{notificationType === "email" && ( {notificationType === "email" && (
<> <>
<FormField <FormField
@@ -597,7 +670,6 @@ export const NotificationChannelDialog = ({
)} )}
/> />
{/* Payload Template Section */}
<div className="space-y-4"> <div className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
@@ -13,9 +13,17 @@ import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { alertConfigService } from "@/services/alertConfigService"; import { alertConfigService } from "@/services/alertConfigService";
import { pb } from "@/lib/pocketbase";
interface CombinedChannel extends Partial<AlertConfiguration> {
isWebhook?: boolean;
url?: string;
method?: string;
description?: string;
}
interface NotificationChannelListProps { interface NotificationChannelListProps {
channels: AlertConfiguration[]; channels: CombinedChannel[];
onEdit: (config: AlertConfiguration) => void; onEdit: (config: AlertConfiguration) => void;
onDelete: (id: string) => void; onDelete: (id: string) => void;
} }
@@ -25,25 +33,63 @@ export const NotificationChannelList = ({
onEdit, onEdit,
onDelete onDelete
}: NotificationChannelListProps) => { }: NotificationChannelListProps) => {
const toggleEnabled = async (config: AlertConfiguration) => { const toggleEnabled = async (config: CombinedChannel) => {
if (!config.id) return; if (!config.id) return;
if (config.isWebhook) {
// Handle webhook toggle
try {
const newEnabled = config.enabled ? "off" : "on";
await pb.collection('webhook').update(config.id, {
enabled: newEnabled
});
// Trigger refresh by calling onEdit with empty config
onEdit({} as AlertConfiguration);
} catch (error) {
console.error("Error updating webhook:", error);
}
} else {
// Handle alert config toggle
await alertConfigService.updateAlertConfiguration(config.id, { await alertConfigService.updateAlertConfiguration(config.id, {
enabled: !config.enabled enabled: !config.enabled
}); });
// The parent component will refresh the list // The parent component will refresh the list
onEdit(config); onEdit(config as AlertConfiguration);
}
}; };
const getChannelTypeLabel = (type: string) => { const getChannelTypeLabel = (type: string | undefined) => {
switch(type) { switch(type) {
case "telegram": return "Telegram"; case "telegram": return "Telegram";
case "discord": return "Discord"; case "discord": return "Discord";
case "slack": return "Slack"; case "slack": return "Slack";
case "signal": return "Signal"; case "signal": return "Signal";
case "google_chat": return "Google Chat";
case "email": return "Email"; case "email": return "Email";
default: return type; case "webhook": return "Webhook";
default: return type || "Unknown";
}
};
const getChannelDetails = (config: CombinedChannel) => {
if (config.isWebhook) {
return `${config.method || 'POST'} ${config.url || ''}`;
}
switch(config.notification_type) {
case "telegram":
return config.telegram_chat_id || '';
case "discord":
case "slack":
case "google_chat":
return config.discord_webhook_url || config.slack_webhook_url || config.google_chat_webhook_url || '';
case "signal":
return config.signal_number || '';
case "email":
return config.email_address || '';
default:
return '';
} }
}; };
@@ -66,6 +112,7 @@ export const NotificationChannelList = ({
<TableRow> <TableRow>
<TableHead>Name</TableHead> <TableHead>Name</TableHead>
<TableHead>Type</TableHead> <TableHead>Type</TableHead>
<TableHead>Details</TableHead>
<TableHead>Status</TableHead> <TableHead>Status</TableHead>
<TableHead>Created</TableHead> <TableHead>Created</TableHead>
<TableHead className="text-right">Actions</TableHead> <TableHead className="text-right">Actions</TableHead>
@@ -78,11 +125,14 @@ export const NotificationChannelList = ({
<TableCell> <TableCell>
<Badge variant="outline">{getChannelTypeLabel(channel.notification_type)}</Badge> <Badge variant="outline">{getChannelTypeLabel(channel.notification_type)}</Badge>
</TableCell> </TableCell>
<TableCell className="max-w-xs truncate text-sm text-muted-foreground">
{getChannelDetails(channel)}
</TableCell>
<TableCell> <TableCell>
<Switch <Switch
checked={ checked={
typeof channel.enabled === 'string' typeof channel.enabled === 'string'
? channel.enabled === "true" ? channel.enabled === "true" || channel.enabled === "on"
: !!channel.enabled : !!channel.enabled
} }
onCheckedChange={() => toggleEnabled(channel)} onCheckedChange={() => toggleEnabled(channel)}
@@ -96,7 +146,8 @@ export const NotificationChannelList = ({
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => onEdit(channel)} onClick={() => onEdit(channel as AlertConfiguration)}
disabled={channel.isWebhook} // Disable edit for webhooks for now
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
@@ -5,28 +5,47 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Plus, Loader2 } from "lucide-react"; import { Plus, Loader2 } from "lucide-react";
import { AlertConfiguration, alertConfigService } from "@/services/alertConfigService"; import { AlertConfiguration, alertConfigService } from "@/services/alertConfigService";
import { WebhookConfiguration, webhookService } from "@/services/webhookService";
import { NotificationChannelDialog } from "./NotificationChannelDialog"; import { NotificationChannelDialog } from "./NotificationChannelDialog";
import { NotificationChannelList } from "./NotificationChannelList"; import { NotificationChannelList } from "./NotificationChannelList";
import { pb } from "@/lib/pocketbase";
interface CombinedChannel extends Partial<AlertConfiguration> {
isWebhook?: boolean;
url?: string;
method?: string;
description?: string;
}
const NotificationSettings = () => { const NotificationSettings = () => {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]); const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [webhookConfigs, setWebhookConfigs] = useState<WebhookConfiguration[]>([]);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [currentTab, setCurrentTab] = useState<string>("all"); const [currentTab, setCurrentTab] = useState<string>("all");
const [editingConfig, setEditingConfig] = useState<AlertConfiguration | null>(null); const [editingConfig, setEditingConfig] = useState<AlertConfiguration | null>(null);
const fetchAlertConfigurations = async () => { const fetchNotificationChannels = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
// Fetch alert configurations
const configs = await alertConfigService.getAlertConfigurations(); const configs = await alertConfigService.getAlertConfigurations();
setAlertConfigs(configs); setAlertConfigs(configs);
// Fetch webhooks
try {
const webhookResponse = await pb.collection('webhook').getList(1, 50);
setWebhookConfigs(webhookResponse.items as WebhookConfiguration[]);
} catch (webhookError) {
setWebhookConfigs([]);
}
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
useEffect(() => { useEffect(() => {
fetchAlertConfigurations(); fetchNotificationChannels();
}, []); }, []);
const handleAddNew = () => { const handleAddNew = () => {
@@ -40,22 +59,66 @@ const NotificationSettings = () => {
}; };
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
// Check if it's a webhook first
const isWebhook = webhookConfigs.find(w => w.id === id);
if (isWebhook) {
// Handle webhook deletion
if (confirm("Are you sure you want to delete this webhook?")) {
try {
await pb.collection('webhook').delete(id);
fetchNotificationChannels();
} catch (error) {
console.error("Error deleting webhook:", error);
}
}
} else {
// Handle alert config deletion
const success = await alertConfigService.deleteAlertConfiguration(id); const success = await alertConfigService.deleteAlertConfiguration(id);
if (success) { if (success) {
fetchAlertConfigurations(); fetchNotificationChannels();
}
} }
}; };
const handleDialogClose = (refreshList: boolean) => { const handleDialogClose = (refreshList: boolean) => {
setDialogOpen(false); setDialogOpen(false);
if (refreshList) { if (refreshList) {
fetchAlertConfigurations(); fetchNotificationChannels();
} }
}; };
const getCombinedChannels = (): CombinedChannel[] => {
const combined: CombinedChannel[] = [];
// Add alert configurations
alertConfigs.forEach(config => {
combined.push(config);
});
// Add webhooks as notification channels
webhookConfigs.forEach(webhook => {
combined.push({
id: webhook.id,
notify_name: webhook.name,
notification_type: "webhook" as const,
enabled: webhook.enabled === "on",
created: webhook.created,
updated: webhook.updated,
isWebhook: true,
url: webhook.url,
method: webhook.method,
description: webhook.description
});
});
return combined;
};
const getFilteredConfigs = () => { const getFilteredConfigs = () => {
if (currentTab === "all") return alertConfigs; const combined = getCombinedChannels();
return alertConfigs.filter(config => config.notification_type === currentTab); if (currentTab === "all") return combined;
return combined.filter(config => config.notification_type === currentTab);
}; };
return ( return (
@@ -86,6 +149,7 @@ const NotificationSettings = () => {
<TabsTrigger value="discord">Discord</TabsTrigger> <TabsTrigger value="discord">Discord</TabsTrigger>
<TabsTrigger value="slack">Slack</TabsTrigger> <TabsTrigger value="slack">Slack</TabsTrigger>
<TabsTrigger value="signal">Signal</TabsTrigger> <TabsTrigger value="signal">Signal</TabsTrigger>
<TabsTrigger value="google_chat">Google Chat</TabsTrigger>
<TabsTrigger value="email">Email</TabsTrigger> <TabsTrigger value="email">Email</TabsTrigger>
<TabsTrigger value="webhook">Webhook</TabsTrigger> <TabsTrigger value="webhook">Webhook</TabsTrigger>
</TabsList> </TabsList>
@@ -1,24 +1,28 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod"; import * as z from "zod";
import { toast } from "sonner"; import { toast } from "sonner";
import { Bell } from "lucide-react"; import { Bell, X } from "lucide-react";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { DialogFooter } from "@/components/ui/dialog"; import { DialogFooter } from "@/components/ui/dialog";
import { Badge } from "@/components/ui/badge";
import { AddSSLCertificateDto } from "@/types/ssl.types"; import { AddSSLCertificateDto } from "@/types/ssl.types";
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
import { sslNotificationTemplateService, SslNotificationTemplate } from "@/services/sslNotificationTemplateService";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
const formSchema = z.object({ const formSchema = z.object({
domain: z.string().min(1, "Domain is required"), domain: z.string().min(1, "Domain is required"),
warning_threshold: z.coerce.number().int().min(1).max(365), warning_threshold: z.coerce.number().int().min(1).max(365),
expiry_threshold: z.coerce.number().int().min(1).max(30), expiry_threshold: z.coerce.number().int().min(1).max(30),
notification_channel: z.string().optional(), // Make it optional to allow empty string for "None" notification_channels: z.array(z.string()).optional(),
alert_template: z.string().optional(),
check_interval: z.coerce.number().int().min(1).max(30).optional() check_interval: z.coerce.number().int().min(1).max(30).optional()
}); });
@@ -35,6 +39,7 @@ export const AddSSLCertificateForm = ({
}: AddSSLCertificateFormProps) => { }: AddSSLCertificateFormProps) => {
const { t } = useLanguage(); const { t } = useLanguage();
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]); const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [sslTemplates, setSslTemplates] = useState<SslNotificationTemplate[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@@ -43,58 +48,78 @@ export const AddSSLCertificateForm = ({
domain: "", domain: "",
warning_threshold: 30, warning_threshold: 30,
expiry_threshold: 7, expiry_threshold: 7,
notification_channel: "none", notification_channels: [],
alert_template: "none",
check_interval: 1 check_interval: 1
} }
}); });
// Fetch notification channels when form loads // Fetch notification channels and SSL templates when form loads
useEffect(() => { useEffect(() => {
const fetchNotificationChannels = async () => { const fetchData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
// Fetch notification channels
const configs = await alertConfigService.getAlertConfigurations(); const configs = await alertConfigService.getAlertConfigurations();
// console.log("Fetched notification channels:", configs);
// Only include enabled channels
const enabledConfigs = configs.filter(config => { const enabledConfigs = configs.filter(config => {
// Handle the possibility of enabled being a string
if (typeof config.enabled === 'string') { if (typeof config.enabled === 'string') {
return config.enabled === "true"; return config.enabled === "true";
} }
// Otherwise treat as boolean
return config.enabled === true; return config.enabled === true;
}); });
setAlertConfigs(enabledConfigs); setAlertConfigs(enabledConfigs);
// Fetch SSL notification templates
const templates = await sslNotificationTemplateService.getTemplates();
setSslTemplates(templates);
} catch (error) { } catch (error) {
// console.error("Error fetching notification channels:", error);
toast.error(t('failedToLoadCertificates')); toast.error(t('failedToLoadCertificates'));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
fetchNotificationChannels(); fetchData();
}, [form, t]); }, [t]);
const handleSubmit = async (values: z.infer<typeof formSchema>) => { const handleSubmit = async (values: z.infer<typeof formSchema>) => {
try { try {
// Convert the form values to the required DTO format with required properties // Convert the form values to the required DTO format
const certData: AddSSLCertificateDto = { const certData: AddSSLCertificateDto = {
domain: values.domain, domain: values.domain,
warning_threshold: values.warning_threshold, warning_threshold: values.warning_threshold,
expiry_threshold: values.expiry_threshold, expiry_threshold: values.expiry_threshold,
notification_channel: values.notification_channel === "none" ? "" : (values.notification_channel || ""), // Convert "none" to empty string notification_channel: values.notification_channels && values.notification_channels.length > 0
? values.notification_channels.join(',')
: '',
notification_id: values.notification_channels && values.notification_channels.length > 0
? values.notification_channels.join(',')
: '',
template_id: values.alert_template && values.alert_template !== 'none' ? values.alert_template : '',
check_interval: values.check_interval check_interval: values.check_interval
}; };
await onSubmit(certData); await onSubmit(certData);
form.reset(); form.reset();
} catch (error) { } catch (error) {
// console.error("Error adding SSL certificate:", error);
toast.error(t('failedToAddCertificate')); toast.error(t('failedToAddCertificate'));
} }
}; };
const handleAddNotificationChannel = (channelId: string) => {
const currentChannels = form.getValues("notification_channels") || [];
if (!currentChannels.includes(channelId)) {
form.setValue("notification_channels", [...currentChannels, channelId]);
}
};
const handleRemoveNotificationChannel = (channelId: string) => {
const currentChannels = form.getValues("notification_channels") || [];
form.setValue("notification_channels", currentChannels.filter(id => id !== channelId));
};
const selectedChannels = form.watch("notification_channels") || [];
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6"> <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
@@ -167,31 +192,60 @@ export const AddSSLCertificateForm = ({
<FormField <FormField
control={form.control} control={form.control}
name="notification_channel" name="notification_channels"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('notificationChannel')}</FormLabel> <FormLabel>{t('notificationChannel')} (Multi-select)</FormLabel>
<Select onValueChange={field.onChange} value={field.value || "none"}> <div className="space-y-3">
<Select
onValueChange={handleAddNotificationChannel}
value=""
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder={t('chooseChannel')} /> <SelectValue placeholder="Select channels to add..." />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem value="none">{t('none')}</SelectItem>
{alertConfigs.length > 0 ? ( {alertConfigs.length > 0 ? (
alertConfigs.map((config) => ( alertConfigs
<SelectItem key={config.id} value={config.id || ""}> .filter(config => config.id && !selectedChannels.includes(config.id))
.map((config) => (
<SelectItem key={config.id} value={config.id || "unknown"}>
{config.notify_name} ({config.notification_type}) {config.notify_name} ({config.notification_type})
</SelectItem> </SelectItem>
)) ))
) : isLoading ? ( ) : isLoading ? (
<SelectItem value="loading" disabled>{t('loadingChannels')}</SelectItem> <SelectItem value="loading-placeholder" disabled>{t('loadingChannels')}</SelectItem>
) : ( ) : (
<SelectItem value="none-disabled" disabled>{t('noChannelsFound')}</SelectItem> <SelectItem value="no-channels-placeholder" disabled>{t('noChannelsFound')}</SelectItem>
)} )}
</SelectContent> </SelectContent>
</Select> </Select>
{/* Display selected channels */}
{selectedChannels.length > 0 && (
<div className="flex flex-wrap gap-2">
{selectedChannels.map((channelId) => {
const channel = alertConfigs.find(config => config.id === channelId);
return (
<Badge key={channelId} variant="secondary" className="flex items-center gap-1">
{channel ? `${channel.notify_name} (${channel.notification_type})` : channelId}
<Button
type="button"
variant="ghost"
size="sm"
className="h-auto p-0 ml-1"
onClick={() => handleRemoveNotificationChannel(channelId)}
>
<X className="h-3 w-3" />
</Button>
</Badge>
);
})}
</div>
)}
</div>
<FormDescription className="flex items-center gap-1"> <FormDescription className="flex items-center gap-1">
<Bell className="h-4 w-4" /> <Bell className="h-4 w-4" />
{t('whereToSend')} {t('whereToSend')}
@@ -201,6 +255,41 @@ export const AddSSLCertificateForm = ({
)} )}
/> />
<FormField
control={form.control}
name="alert_template"
render={({ field }) => (
<FormItem>
<FormLabel>Alert Template</FormLabel>
<Select onValueChange={field.onChange} value={field.value || "none"}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Choose an alert template (optional)" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{sslTemplates.length > 0 ? (
sslTemplates.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.name}
</SelectItem>
))
) : isLoading ? (
<SelectItem value="loading-templates" disabled>Loading templates...</SelectItem>
) : (
<SelectItem value="no-templates-found" disabled>No templates found</SelectItem>
)}
</SelectContent>
</Select>
<FormDescription>
Template for SSL certificate alert messages
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter> <DialogFooter>
<Button type="button" variant="outline" onClick={onCancel}>{t('cancel')}</Button> <Button type="button" variant="outline" onClick={onCancel}>{t('cancel')}</Button>
<Button type="submit" disabled={isPending || isLoading}> <Button type="submit" disabled={isPending || isLoading}>
@@ -1,4 +1,3 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -7,17 +6,20 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Badge } from "@/components/ui/badge";
import { SSLCertificate } from "@/types/ssl.types"; import { SSLCertificate } from "@/types/ssl.types";
import { Loader2, Bell } from "lucide-react"; import { Loader2, Bell, X } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
import { sslNotificationTemplateService, SslNotificationTemplate } from "@/services/sslNotificationTemplateService";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
const formSchema = z.object({ const formSchema = z.object({
domain: z.string().min(1, "Domain is required"), domain: z.string().min(1, "Domain is required"),
warning_threshold: z.coerce.number().min(1, "Warning threshold must be at least 1 day"), warning_threshold: z.coerce.number().min(1, "Warning threshold must be at least 1 day"),
expiry_threshold: z.coerce.number().min(1, "Expiry threshold must be at least 1 day"), expiry_threshold: z.coerce.number().min(1, "Expiry threshold must be at least 1 day"),
notification_channel: z.string().min(1, "Notification channel is required"), notification_channels: z.array(z.string()).default([]),
alert_template: z.string().optional(),
check_interval: z.coerce.number().int().min(1).max(30).optional(), check_interval: z.coerce.number().int().min(1).max(30).optional(),
}); });
@@ -33,6 +35,7 @@ interface EditSSLCertificateFormProps {
export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPending }: EditSSLCertificateFormProps) => { export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPending }: EditSSLCertificateFormProps) => {
const { t } = useLanguage(); const { t } = useLanguage();
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]); const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [sslTemplates, setSslTemplates] = useState<SslNotificationTemplate[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const form = useForm<FormValues>({ const form = useForm<FormValues>({
@@ -41,58 +44,81 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
domain: certificate.domain, domain: certificate.domain,
warning_threshold: certificate.warning_threshold, warning_threshold: certificate.warning_threshold,
expiry_threshold: certificate.expiry_threshold, expiry_threshold: certificate.expiry_threshold,
notification_channel: certificate.notification_channel, notification_channels: certificate.notification_channel ? certificate.notification_channel.split(',') : [],
alert_template: certificate.alert_template || "none",
check_interval: certificate.check_interval || 1, check_interval: certificate.check_interval || 1,
}, },
}); });
// Fetch notification channels when form loads const notificationChannels = form.watch("notification_channels") || [];
// Fetch notification channels and SSL templates when form loads
useEffect(() => { useEffect(() => {
const fetchNotificationChannels = async () => { const fetchData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
// Fetch notification channels
const configs = await alertConfigService.getAlertConfigurations(); const configs = await alertConfigService.getAlertConfigurations();
// console.log("Fetched notification channels:", configs);
// Only include enabled channels
const enabledConfigs = configs.filter(config => { const enabledConfigs = configs.filter(config => {
// Handle the possibility of enabled being a string
if (typeof config.enabled === 'string') { if (typeof config.enabled === 'string') {
return config.enabled === "true"; return config.enabled === "true";
} }
// Otherwise treat as boolean
return config.enabled === true; return config.enabled === true;
}); });
setAlertConfigs(enabledConfigs); setAlertConfigs(enabledConfigs);
// Fetch SSL notification templates
const templates = await sslNotificationTemplateService.getTemplates();
setSslTemplates(templates);
} catch (error) { } catch (error) {
// console.error("Error fetching notification channels:", error);
toast.error(t('failedToLoadCertificates')); toast.error(t('failedToLoadCertificates'));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
fetchNotificationChannels(); fetchData();
}, [t]); }, [t]);
const handleChannelAdd = (channelId: string) => {
const currentChannels = form.getValues("notification_channels") || [];
if (!currentChannels.includes(channelId)) {
form.setValue("notification_channels", [...currentChannels, channelId]);
}
};
const handleChannelRemove = (channelId: string) => {
const currentChannels = form.getValues("notification_channels") || [];
form.setValue("notification_channels", currentChannels.filter(id => id !== channelId));
};
const getSelectedChannelNames = () => {
return (notificationChannels || []).map(channelId => {
const config = alertConfigs.find(c => c.id === channelId);
return config ? `${config.notify_name} (${config.notification_type})` : channelId;
});
};
const handleSubmit = (data: FormValues) => { const handleSubmit = (data: FormValues) => {
// Merge the updated values with the original certificate // Merge the updated values with the original certificate
const updatedCertificate: SSLCertificate = { const updatedCertificate: SSLCertificate = {
...certificate, ...certificate,
...data, ...data,
// Save notification channels as comma-separated string for notification_channel field
notification_channel: data.notification_channels.length > 0 ? data.notification_channels.join(',') : '',
// Save notification channels as comma-separated string for notification_id field (for PocketBase)
notification_id: data.notification_channels.length > 0 ? data.notification_channels.join(',') : '',
// Save alert template as template_id field (for PocketBase) - handle "none" value
template_id: data.alert_template && data.alert_template !== 'none' ? data.alert_template : '',
// Ensure values are correctly typed as numbers // Ensure values are correctly typed as numbers
warning_threshold: Number(data.warning_threshold), warning_threshold: Number(data.warning_threshold),
expiry_threshold: Number(data.expiry_threshold), expiry_threshold: Number(data.expiry_threshold),
check_interval: data.check_interval ? Number(data.check_interval) : undefined check_interval: data.check_interval ? Number(data.check_interval) : undefined
}; };
console.log("Submitting updated certificate:", updatedCertificate);
onSubmit(updatedCertificate); onSubmit(updatedCertificate);
}; };
// For debugging
console.log("Certificate data:", certificate);
console.log("Form default values:", form.getValues());
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
@@ -106,7 +132,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
<Input <Input
{...field} {...field}
placeholder="example.com" placeholder="example.com"
disabled={true} // Domain shouldn't be editable disabled={true}
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
@@ -188,41 +214,52 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
<FormField <FormField
control={form.control} control={form.control}
name="notification_channel" name="notification_channels"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('notificationChannel')}</FormLabel> <FormLabel>{t('notificationChannel')}</FormLabel>
<Select <FormDescription>
onValueChange={field.onChange} Select multiple notification channels for this SSL certificate
defaultValue={field.value} </FormDescription>
value={field.value}
disabled={isLoading} {/* Display selected channels as badges */}
> {notificationChannels && notificationChannels.length > 0 && (
<div className="flex flex-wrap gap-2 mb-2">
{getSelectedChannelNames().map((channelName, index) => (
<Badge key={notificationChannels[index]} variant="secondary" className="flex items-center gap-1">
{channelName}
<X
className="h-3 w-3 cursor-pointer"
onClick={() => handleChannelRemove(notificationChannels[index])}
/>
</Badge>
))}
</div>
)}
<FormControl> <FormControl>
<Select
onValueChange={handleChannelAdd}
disabled={isLoading}
value=""
>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder={t('chooseChannel')} /> <SelectValue placeholder={t('chooseChannel')} />
</SelectTrigger> </SelectTrigger>
</FormControl>
<SelectContent> <SelectContent>
{alertConfigs.length > 0 ? ( {alertConfigs
alertConfigs.map((config) => ( .filter(config => config.id && !notificationChannels?.includes(config.id))
<SelectItem key={config.id} value={config.id || ""}> .map((config) => (
<SelectItem key={config.id} value={config.id || "unknown"}>
{config.notify_name} ({config.notification_type}) {config.notify_name} ({config.notification_type})
</SelectItem> </SelectItem>
)) ))}
) : isLoading ? ( {alertConfigs.filter(config => config.id && !notificationChannels?.includes(config.id)).length === 0 && (
<SelectItem value="loading" disabled>{t('loadingChannels')}</SelectItem> <SelectItem value="no-available-channels" disabled>No available channels</SelectItem>
) : (
<>
<SelectItem value="email">Email</SelectItem>
<SelectItem value="telegram">Telegram</SelectItem>
<SelectItem value="slack">Slack</SelectItem>
<SelectItem value="webhook">Webhook</SelectItem>
<SelectItem value="none">None</SelectItem>
</>
)} )}
</SelectContent> </SelectContent>
</Select> </Select>
</FormControl>
<FormDescription className="flex items-center gap-1"> <FormDescription className="flex items-center gap-1">
<Bell className="h-4 w-4" /> <Bell className="h-4 w-4" />
{t('whereToSend')} {t('whereToSend')}
@@ -232,6 +269,45 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
)} )}
/> />
<FormField
control={form.control}
name="alert_template"
render={({ field }) => (
<FormItem>
<FormLabel>Alert Template</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
value={field.value || "none"}
disabled={isLoading}
>
<SelectTrigger>
<SelectValue placeholder={isLoading ? "Loading templates..." : "Select an alert template"} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{sslTemplates?.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.name}
</SelectItem>
))}
{sslTemplates?.length === 0 && !isLoading && (
<SelectItem value="no-templates-available" disabled>No templates found</SelectItem>
)}
{isLoading && (
<SelectItem value="loading-templates-placeholder" disabled>Loading templates...</SelectItem>
)}
</SelectContent>
</Select>
</FormControl>
<FormDescription>
Choose a template for SSL certificate alert messages
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-3 pt-4"> <div className="flex justify-end gap-3 pt-4">
<Button <Button
type="button" type="button"
@@ -1,4 +1,3 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -30,12 +29,9 @@ export const SSLDomainContent = () => {
queryKey: ['ssl-certificates'], queryKey: ['ssl-certificates'],
queryFn: async () => { queryFn: async () => {
try { try {
// console.log("Fetching SSL certificates from SSLDomainContent...");
const result = await fetchSSLCertificates(); const result = await fetchSSLCertificates();
// console.log("Received SSL certificates:", result);
return result; return result;
} catch (error) { } catch (error) {
// console.error("Error fetching certificates:", error);
toast.error(t('failedToLoadCertificates')); toast.error(t('failedToLoadCertificates'));
throw error; throw error;
} }
@@ -53,30 +49,28 @@ export const SSLDomainContent = () => {
toast.success(t('sslCertificateAdded')); toast.success(t('sslCertificateAdded'));
}, },
onError: (error) => { onError: (error) => {
console.error("Error adding SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToAddCertificate')); toast.error(error instanceof Error ? error.message : t('failedToAddCertificate'));
} }
}); });
// Edit certificate mutation - Updated to ensure thresholds are properly updated // Edit certificate mutation - Updated to include notification_id and template_id
const editMutation = useMutation({ const editMutation = useMutation({
mutationFn: async (certificate: SSLCertificate) => { mutationFn: async (certificate: SSLCertificate) => {
// console.log("Updating certificate with data:", certificate);
// Create the update data object // Create the update data object with new fields
const updateData = { const updateData = {
warning_threshold: Number(certificate.warning_threshold), warning_threshold: Number(certificate.warning_threshold),
expiry_threshold: Number(certificate.expiry_threshold), expiry_threshold: Number(certificate.expiry_threshold),
notification_channel: certificate.notification_channel, notification_channel: certificate.notification_channel,
notification_id: certificate.notification_id || '', // Multi notification channels
template_id: certificate.template_id || '', // Alert template ID
check_interval: certificate.check_interval,
}; };
// console.log("Update data to be sent:", updateData);
// Update certificate in the database using PocketBase directly // Update certificate in the database using PocketBase directly
const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData); const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData);
// console.log("PocketBase update response:", updated);
// After updating the settings, refresh the certificate to ensure it's up to date // After updating the settings, refresh the certificate to ensure it's up to date
// This will also check if notification needs to be sent based on updated thresholds // This will also check if notification needs to be sent based on updated thresholds
const refreshedCert = await checkAndUpdateCertificate(certificate.id); const refreshedCert = await checkAndUpdateCertificate(certificate.id);
@@ -90,7 +84,6 @@ export const SSLDomainContent = () => {
toast.success(t('sslCertificateUpdated')); toast.success(t('sslCertificateUpdated'));
}, },
onError: (error) => { onError: (error) => {
// console.error("Error updating SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate')); toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate'));
} }
}); });
@@ -103,7 +96,6 @@ export const SSLDomainContent = () => {
toast.success(t('sslCertificateDeleted')); toast.success(t('sslCertificateDeleted'));
}, },
onError: (error) => { onError: (error) => {
// console.error("Error deleting SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate')); toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate'));
} }
}); });
@@ -117,7 +109,6 @@ export const SSLDomainContent = () => {
// Removed individual success toast notification // Removed individual success toast notification
}, },
onError: (error) => { onError: (error) => {
// console.error("Error refreshing SSL certificate:", error);
setRefreshingId(null); setRefreshingId(null);
// Still refresh the data to show any partial information that was saved // Still refresh the data to show any partial information that was saved
@@ -142,7 +133,6 @@ export const SSLDomainContent = () => {
} }
}, },
onError: (error) => { onError: (error) => {
// console.error("Error refreshing all certificates:", error);
toast.error(t('failedToCheckCertificate')); toast.error(t('failedToCheckCertificate'));
setIsRefreshingAll(false); setIsRefreshingAll(false);
@@ -167,7 +157,6 @@ export const SSLDomainContent = () => {
}; };
const handleUpdateCertificate = (certificate: SSLCertificate) => { const handleUpdateCertificate = (certificate: SSLCertificate) => {
console.log("Handling certificate update with data:", certificate);
editMutation.mutate(certificate); editMutation.mutate(certificate);
}; };
+39 -14
View File
@@ -7,7 +7,7 @@ export interface AlertConfiguration {
collectionId?: string; collectionId?: string;
collectionName?: string; collectionName?: string;
service_id: string; service_id: string;
notification_type: "telegram" | "discord" | "signal" | "slack" | "email"; notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "webhook";
telegram_chat_id?: string; telegram_chat_id?: string;
discord_webhook_url?: string; discord_webhook_url?: string;
signal_number?: string; signal_number?: string;
@@ -15,6 +15,7 @@ export interface AlertConfiguration {
bot_token?: string; bot_token?: string;
template_id?: string; template_id?: string;
slack_webhook_url?: string; slack_webhook_url?: string;
google_chat_webhook_url?: string;
enabled: boolean; enabled: boolean;
created?: string; created?: string;
updated?: string; updated?: string;
@@ -29,13 +30,10 @@ export interface AlertConfiguration {
export const alertConfigService = { export const alertConfigService = {
async getAlertConfigurations(): Promise<AlertConfiguration[]> { async getAlertConfigurations(): Promise<AlertConfiguration[]> {
// 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);
return response.items as AlertConfiguration[]; return response.items as AlertConfiguration[];
} catch (error) { } catch (error) {
// console.error("Error fetching alert configurations:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to load notification settings", description: "Failed to load notification settings",
@@ -46,17 +44,50 @@ export const alertConfigService = {
}, },
async createAlertConfiguration(config: Omit<AlertConfiguration, 'id' | 'collectionId' | 'collectionName' | 'created' | 'updated'>): Promise<AlertConfiguration | null> { async createAlertConfiguration(config: Omit<AlertConfiguration, 'id' | 'collectionId' | 'collectionName' | 'created' | 'updated'>): Promise<AlertConfiguration | null> {
// console.info("Creating alert configuration:", config);
try { try {
const result = await pb.collection('alert_configurations').create(config); // Build the configuration object with proper field mapping
// console.info("Alert configuration created:", result); const cleanConfig: any = {
service_id: config.service_id || "global",
notification_type: config.notification_type,
notify_name: config.notify_name,
enabled: config.enabled,
template_id: config.template_id || "",
};
// Add type-specific fields based on notification type
if (config.notification_type === "telegram") {
cleanConfig.telegram_chat_id = config.telegram_chat_id || "";
cleanConfig.bot_token = config.bot_token || "";
} else if (config.notification_type === "discord") {
cleanConfig.discord_webhook_url = config.discord_webhook_url || "";
} else if (config.notification_type === "slack") {
cleanConfig.slack_webhook_url = config.slack_webhook_url || "";
} else if (config.notification_type === "signal") {
cleanConfig.signal_number = config.signal_number || "";
} else if (config.notification_type === "google_chat") {
cleanConfig.google_chat_webhook_url = config.google_chat_webhook_url || "";
} else if (config.notification_type === "email") {
cleanConfig.email_address = config.email_address || "";
cleanConfig.email_sender_name = config.email_sender_name || "";
cleanConfig.smtp_server = config.smtp_server || "";
cleanConfig.smtp_port = config.smtp_port || "";
}
const result = await pb.collection('alert_configurations').create(cleanConfig);
toast({ toast({
title: "Success", title: "Success",
description: "Notification channel created 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); // Try to get more details from the error
if (error && typeof error === 'object') {
}
toast({ toast({
title: "Error", title: "Error",
description: "Failed to create notification channel", description: "Failed to create notification channel",
@@ -67,17 +98,14 @@ 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);
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);
toast({ toast({
title: "Success", title: "Success",
description: "Notification channel 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);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to update notification channel", description: "Failed to update notification channel",
@@ -88,17 +116,14 @@ export const alertConfigService = {
}, },
async deleteAlertConfiguration(id: string): Promise<boolean> { async deleteAlertConfiguration(id: string): Promise<boolean> {
// 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");
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);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to remove notification channel", description: "Failed to remove notification channel",
@@ -32,6 +32,8 @@ export const addSSLCertificate = async (
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 || "",
notification_id: certificateData.notification_id || "", // Multi notification channels
template_id: certificateData.template_id || "", // Alert template ID
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 check_at: currentTime, // Set to current time to trigger immediate check
}; };
@@ -41,7 +43,6 @@ export const addSSLCertificate = async (
return record as unknown as SSLCertificate; return record as unknown as SSLCertificate;
} catch (error) { } catch (error) {
console.error("Error adding SSL certificate:", error);
throw error; throw error;
} }
}; };
@@ -70,7 +71,6 @@ export const checkAndUpdateCertificate = async (
// Return the current certificate data // Return the current certificate data
return typedCertificate; return typedCertificate;
} catch (error) { } catch (error) {
console.error("Error updating SSL certificate:", error);
throw error; throw error;
} }
}; };
@@ -87,10 +87,8 @@ export const triggerImmediateCheck = async (certificateId: string): Promise<void
check_at: currentTime check_at: currentTime
}); });
console.log(`Triggered immediate check for certificate ${certificateId} at ${currentTime}`);
toast.success("SSL check scheduled - certificate will be checked shortly"); toast.success("SSL check scheduled - certificate will be checked shortly");
} catch (error) { } catch (error) {
console.error("Error triggering immediate SSL check:", error);
toast.error("Failed to schedule SSL check"); toast.error("Failed to schedule SSL check");
throw error; throw error;
} }
@@ -104,7 +102,6 @@ export const deleteSSLCertificate = async (id: string): Promise<boolean> => {
await pb.collection("ssl_certificates").delete(id); await pb.collection("ssl_certificates").delete(id);
return true; return true;
} catch (error) { } catch (error) {
console.error("Error deleting SSL certificate:", error);
throw error; throw error;
} }
}; };
@@ -118,8 +115,6 @@ export const refreshAllCertificates = async (): Promise<{ success: number; faile
const response = await pb.collection("ssl_certificates").getList(1, 100); 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...`);
let success = 0; let success = 0;
let failed = 0; let failed = 0;
@@ -128,14 +123,12 @@ 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);
failed++; failed++;
} }
} }
return { success, failed }; return { success, failed };
} catch (error) { } catch (error) {
console.error("Error refreshing certificates:", error);
throw error; throw error;
} }
}; };
+8
View File
@@ -1,9 +1,13 @@
// SSL Certificate DTO for adding new certificates // SSL Certificate DTO for adding new certificates
export interface AddSSLCertificateDto { export interface AddSSLCertificateDto {
domain: string; domain: string;
warning_threshold: number; warning_threshold: number;
expiry_threshold: number; expiry_threshold: number;
notification_channel: string; notification_channel: string;
alert_template?: string; // New field for SSL alert template
notification_id?: string; // Multi notification channels as comma-separated string
template_id?: string; // Alert template ID for PocketBase
check_interval?: number; // New field for check interval in days check_interval?: number; // New field for check interval in days
} }
@@ -25,6 +29,10 @@ export interface SSLCertificate {
warning_threshold: number; warning_threshold: number;
expiry_threshold: number; expiry_threshold: number;
notification_channel: string; notification_channel: string;
alert_template?: string; // New field for SSL alert template
// PocketBase specific fields
notification_id?: string; // Multi notification channels as comma-separated string
template_id?: string; // Alert template ID for PocketBase
last_notified?: string; last_notified?: string;
created?: string; created?: string;
updated?: string; updated?: string;
+8
View File
@@ -1,3 +1,4 @@
export interface SSLCertificate { export interface SSLCertificate {
id: string; id: string;
domain: string; domain: string;
@@ -15,6 +16,10 @@ export interface SSLCertificate {
warning_threshold: number; warning_threshold: number;
expiry_threshold: number; expiry_threshold: number;
notification_channel: string; notification_channel: string;
alert_template?: string; // New field for SSL alert template
// PocketBase specific fields
notification_id?: string; // Multi notification channels as comma-separated string
template_id?: string; // Alert template ID for PocketBase
last_notified?: string; last_notified?: string;
created?: string; created?: string;
updated?: string; updated?: string;
@@ -33,5 +38,8 @@ export interface AddSSLCertificateDto {
warning_threshold: number; warning_threshold: number;
expiry_threshold: number; expiry_threshold: number;
notification_channel: string; notification_channel: string;
alert_template?: string; // New field for SSL alert template
notification_id?: string; // Multi notification channels as comma-separated string
template_id?: string; // Alert template ID for PocketBase
check_interval?: number; // New field for check interval in days check_interval?: number; // New field for check interval in days
} }