Merge pull request #129 from operacle/develop
feat: Implement comprehensive powerful multi-channel notification system with templating: - Add robust notification system supporting 7+ communication channels with intelligent message templating, resource monitoring, and SSL certificate alerts. Notification Channels: (Telegram, Discord, Slack, Signal, Email, Google Chat, Webhooks). - This notification system provides enterprise-grade alerting capabilities with extensive customization options and multi-channel redundancy for critical service monitoring. - Chinese language added - Server and Service Table row clickable to detail page. - Implement pagination for the SSL dashboard table - Server Agent (RPM, Docker container, and general binary package) - Improve Uptime Service and Server connection update based on status and notification. - Improve SSL perform the initial check automatically after creation - Backend Rate limiting and abuse protection
@@ -20,6 +20,12 @@
|
|||||||
<br/><strong>Japanese</strong>
|
<br/><strong>Japanese</strong>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="docs/README_zhcn.md">
|
||||||
|
<img src="https://flagcdn.com/24x18/cn.png" alt="Chinese" />
|
||||||
|
<br/><strong>Chinese</strong>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -111,7 +117,7 @@ docker run -d \
|
|||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## 🌟 CheckCle for Communities?
|
## 🌟 CheckCle for Communities?
|
||||||
@@ -126,18 +132,32 @@ 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>
|
</a>
|
||||||
|
|
||||||
|
### 🤝 Ecosystem & Community Partner
|
||||||
|
<a href="https://github.com/gitbookio" style="display: inline-block; margin-right: 10px; vertical-align: middle;">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4"
|
||||||
|
width="75" height="75"
|
||||||
|
style="border-radius: 50%;" />
|
||||||
|
</a>
|
||||||
|
<a href="https://m.do.co/c/0c27ef82475f" style="display: inline-block; vertical-align: middle;">
|
||||||
|
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg"
|
||||||
|
height="45"
|
||||||
|
alt="DigitalOcean Logo">
|
||||||
|
</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 +179,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)
|
||||||
|
|
||||||
@@ -168,5 +188,3 @@ Here are some ways you can help improve CheckCle:
|
|||||||
CheckCle is released under the MIT License.
|
CheckCle is released under the MIT License.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
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>
|
||||||
@@ -132,6 +119,9 @@ export const Header = ({
|
|||||||
<DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}>
|
<DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}>
|
||||||
{t("japanese")}
|
{t("japanese")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setLanguage("zhcn")} className={language === "zhcn" ? "bg-accent" : ""}>
|
||||||
|
{t("simplifiedChinese")}
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
@@ -162,7 +152,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 +213,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;
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,18 +64,18 @@ sudo -E bash ./server-docker-agent.sh`;
|
|||||||
const handleCopyOneClickCommand = async (e: React.MouseEvent) => {
|
const handleCopyOneClickCommand = async (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
console.log('Copy one-click command button clicked');
|
// console.log('Copy one-click command button clicked');
|
||||||
const command = getDockerOneClickCommand();
|
const command = getDockerOneClickCommand();
|
||||||
console.log('Copying one-click command:', command);
|
// console.log('Copying one-click command:', command);
|
||||||
await copyToClipboard(command);
|
await copyToClipboard(command);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyDockerCommand = async (e: React.MouseEvent) => {
|
const handleCopyDockerCommand = async (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
console.log('Copy docker command button clicked');
|
// console.log('Copy docker command button clicked');
|
||||||
const command = getDirectDockerCommand();
|
const command = getDirectDockerCommand();
|
||||||
console.log('Copying docker command:', command);
|
// console.log('Copying docker command:', command);
|
||||||
await copyToClipboard(command);
|
await copyToClipboard(command);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -12,7 +12,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 {
|
||||||
@@ -25,7 +25,7 @@ interface EditServerDialogProps {
|
|||||||
interface ServerFormData {
|
interface ServerFormData {
|
||||||
name: string;
|
name: string;
|
||||||
check_interval: number;
|
check_interval: number;
|
||||||
retry_attempts: number;
|
max_retries: number;
|
||||||
docker_monitoring: boolean;
|
docker_monitoring: boolean;
|
||||||
notification_enabled: boolean;
|
notification_enabled: boolean;
|
||||||
notification_channels: string[]; // Changed to array for multiple selections
|
notification_channels: string[]; // Changed to array for multiple selections
|
||||||
@@ -49,7 +49,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
|||||||
const [formData, setFormData] = useState<ServerFormData>({
|
const [formData, setFormData] = useState<ServerFormData>({
|
||||||
name: "",
|
name: "",
|
||||||
check_interval: 60,
|
check_interval: 60,
|
||||||
retry_attempts: 3,
|
max_retries: 3,
|
||||||
docker_monitoring: false,
|
docker_monitoring: false,
|
||||||
notification_enabled: false,
|
notification_enabled: false,
|
||||||
notification_channels: [], // Changed to array
|
notification_channels: [], // Changed to array
|
||||||
@@ -66,9 +66,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 +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)
|
||||||
@@ -87,9 +86,9 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
|||||||
setFormData({
|
setFormData({
|
||||||
name: server.name || "",
|
name: server.name || "",
|
||||||
check_interval: server.check_interval || 60,
|
check_interval: server.check_interval || 60,
|
||||||
retry_attempts: 3,
|
max_retries: server.max_retries || 3,
|
||||||
docker_monitoring: server.docker === "true",
|
docker_monitoring: server.docker === "true",
|
||||||
notification_enabled: notificationChannels.length > 0,
|
notification_enabled: server.notification_status === true,
|
||||||
notification_channels: notificationChannels,
|
notification_channels: notificationChannels,
|
||||||
threshold_id: server.threshold_id || "none",
|
threshold_id: server.threshold_id || "none",
|
||||||
template_id: server.template_id || "none",
|
template_id: server.template_id || "none",
|
||||||
@@ -109,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({
|
||||||
@@ -166,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",
|
||||||
@@ -180,10 +176,10 @@ 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);
|
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@@ -200,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",
|
||||||
@@ -211,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,
|
||||||
@@ -276,22 +230,56 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
// Convert notification channels array to comma-separated string
|
// Convert notification channels array to comma-separated string
|
||||||
const notificationChannelsString = formData.notification_enabled
|
const notificationChannelsString = formData.notification_channels.join(',');
|
||||||
? formData.notification_channels.join(',')
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
check_interval: formData.check_interval,
|
check_interval: formData.check_interval,
|
||||||
|
max_retries: formData.max_retries,
|
||||||
docker: formData.docker_monitoring ? "true" : "false",
|
docker: formData.docker_monitoring ? "true" : "false",
|
||||||
|
notification_status: formData.notification_enabled,
|
||||||
notification_id: notificationChannelsString,
|
notification_id: notificationChannelsString,
|
||||||
threshold_id: formData.notification_enabled && formData.threshold_id !== "none" ? formData.threshold_id : "",
|
threshold_id: formData.threshold_id !== "none" ? formData.threshold_id : "",
|
||||||
template_id: formData.notification_enabled && formData.template_id !== "none" ? formData.template_id : "",
|
template_id: formData.template_id !== "none" ? formData.template_id : "",
|
||||||
updated: new Date().toISOString(),
|
updated: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
||||||
|
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.`,
|
||||||
@@ -301,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",
|
||||||
@@ -321,9 +308,9 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
|||||||
setFormData({
|
setFormData({
|
||||||
name: server.name || "",
|
name: server.name || "",
|
||||||
check_interval: server.check_interval || 60,
|
check_interval: server.check_interval || 60,
|
||||||
retry_attempts: 3,
|
max_retries: server.max_retries || 3,
|
||||||
docker_monitoring: server.docker === "true",
|
docker_monitoring: server.docker === "true",
|
||||||
notification_enabled: notificationChannels.length > 0,
|
notification_enabled: server.notification_status === true,
|
||||||
notification_channels: notificationChannels,
|
notification_channels: notificationChannels,
|
||||||
threshold_id: server.threshold_id || "none",
|
threshold_id: server.threshold_id || "none",
|
||||||
template_id: server.template_id || "none",
|
template_id: server.template_id || "none",
|
||||||
@@ -372,20 +359,20 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="retryAttempts">Retry Attempts</Label>
|
<Label htmlFor="maxRetries">Max Retries</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.retry_attempts.toString()}
|
value={formData.max_retries.toString()}
|
||||||
onValueChange={(value) => setFormData(prev => ({ ...prev, retry_attempts: parseInt(value) }))}
|
onValueChange={(value) => setFormData(prev => ({ ...prev, max_retries: parseInt(value) }))}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select retry attempts" />
|
<SelectValue placeholder="Select max retries" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="1">1 attempt</SelectItem>
|
<SelectItem value="1">1 retry</SelectItem>
|
||||||
<SelectItem value="2">2 attempts</SelectItem>
|
<SelectItem value="2">2 retries</SelectItem>
|
||||||
<SelectItem value="3">3 attempts</SelectItem>
|
<SelectItem value="3">3 retries</SelectItem>
|
||||||
<SelectItem value="5">5 attempts</SelectItem>
|
<SelectItem value="5">5 retries</SelectItem>
|
||||||
<SelectItem value="10">10 attempts</SelectItem>
|
<SelectItem value="10">10 retries</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -416,10 +403,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
|||||||
checked={formData.notification_enabled}
|
checked={formData.notification_enabled}
|
||||||
onCheckedChange={(checked) => setFormData(prev => ({
|
onCheckedChange={(checked) => setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
notification_enabled: checked,
|
notification_enabled: checked
|
||||||
notification_channels: checked ? prev.notification_channels : [],
|
// Remove the automatic clearing of notification_channels, threshold_id, and template_id
|
||||||
threshold_id: checked ? prev.threshold_id : "none",
|
|
||||||
template_id: checked ? prev.template_id : "none"
|
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="notificationEnabled">Enable Notifications</Label>
|
<Label htmlFor="notificationEnabled">Enable Notifications</Label>
|
||||||
@@ -516,16 +501,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">
|
||||||
@@ -621,27 +598,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>
|
||||||
@@ -677,4 +666,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default EditServerDialog;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -100,4 +91,4 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
@@ -21,18 +21,18 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
const notificationStatus = form.watch("notificationStatus");
|
const notificationStatus = form.watch("notificationStatus");
|
||||||
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(() => {
|
||||||
@@ -42,13 +42,19 @@ 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:", {
|
||||||
// notificationStatus: form.getValues("notificationStatus"),
|
// notificationStatus: form.getValues("notificationStatus"),
|
||||||
// notificationChannels: form.getValues("notificationChannels")
|
// notificationChannels: form.getValues("notificationChannels")
|
||||||
// });
|
// });
|
||||||
@@ -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>
|
||||||
@@ -169,18 +173,20 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
console.log("Alert template changed to:", value);
|
field.onChange(value);
|
||||||
field.onChange(value === "default" ? "" : 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>
|
||||||
@@ -195,4 +201,4 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,6 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => {
|
|||||||
notificationChannels.push(...parsedChannels);
|
notificationChannels.push(...parsedChannels);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.warn("Failed to parse notification_channel JSON:", error);
|
|
||||||
// If parsing fails, treat as single channel ID
|
// If parsing fails, treat as single channel ID
|
||||||
notificationChannels.push(service.notification_channel);
|
notificationChannels.push(service.notification_channel);
|
||||||
}
|
}
|
||||||
@@ -91,16 +90,13 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log("Mapping service to form data:", {
|
// Handle notification_status - it can be boolean or string
|
||||||
// serviceName: service.name,
|
let notificationStatus: "enabled" | "disabled" = "disabled";
|
||||||
// notification_status: service.notification_status,
|
if (typeof service.notification_status === "boolean") {
|
||||||
// notification_channel: service.notification_channel,
|
notificationStatus = service.notification_status ? "enabled" : "disabled";
|
||||||
// notificationChannel: service.notificationChannel,
|
} else if (typeof service.notification_status === "string") {
|
||||||
// mappedChannels: notificationChannels,
|
notificationStatus = service.notification_status === "enabled" ? "enabled" : "disabled";
|
||||||
// regionalAgents: regionalAgents,
|
}
|
||||||
// region_name: service.region_name,
|
|
||||||
/// agent_id: service.agent_id
|
|
||||||
// });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: service.name || "",
|
name: service.name || "",
|
||||||
@@ -109,7 +105,7 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => {
|
|||||||
port: portValue,
|
port: portValue,
|
||||||
interval: String(service.interval || 60),
|
interval: String(service.interval || 60),
|
||||||
retries: String(service.retries || 3),
|
retries: String(service.retries || 3),
|
||||||
notificationStatus: service.notification_status || "disabled",
|
notificationStatus: notificationStatus,
|
||||||
notificationChannels: notificationChannels,
|
notificationChannels: notificationChannels,
|
||||||
alertTemplate: service.alertTemplate === "default" ? "" : service.alertTemplate || "",
|
alertTemplate: service.alertTemplate === "default" ? "" : service.alertTemplate || "",
|
||||||
regionalMonitoringEnabled: isRegionalEnabled,
|
regionalMonitoringEnabled: isRegionalEnabled,
|
||||||
@@ -152,7 +148,8 @@ export const mapFormDataToServiceData = (data: ServiceFormData) => {
|
|||||||
type: data.type,
|
type: data.type,
|
||||||
interval: parseInt(data.interval),
|
interval: parseInt(data.interval),
|
||||||
retries: parseInt(data.retries),
|
retries: parseInt(data.retries),
|
||||||
notificationStatus: data.notificationStatus || "disabled",
|
// Convert string status to boolean for notification_status field
|
||||||
|
notificationStatus: data.notificationStatus === "enabled",
|
||||||
notificationChannels: data.notificationChannels || [],
|
notificationChannels: data.notificationChannels || [],
|
||||||
alertTemplate: data.alertTemplate === "default" ? "" : data.alertTemplate,
|
alertTemplate: data.alertTemplate === "default" ? "" : data.alertTemplate,
|
||||||
// Use regional_status field and store multiple agents as comma-separated values
|
// Use regional_status field and store multiple agents as comma-separated values
|
||||||
|
|||||||
@@ -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,103 @@ 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-4">
|
||||||
<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>
|
<TabsTrigger value="server_threshold">Server Threshold</TabsTrigger>
|
||||||
</div>
|
</TabsList>
|
||||||
) : (
|
|
||||||
<TemplateList
|
<TabsContent value="service" className="mt-4">
|
||||||
templates={templates}
|
{error ? (
|
||||||
isLoading={isLoading}
|
<div className="text-center p-6">
|
||||||
onEdit={handleEditTemplate}
|
<p className="text-destructive mb-4">Error loading service templates</p>
|
||||||
refetchTemplates={refetch}
|
<Button variant="outline" onClick={() => 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>
|
||||||
|
|
||||||
|
<TabsContent value="server_threshold" className="mt-4">
|
||||||
|
{error ? (
|
||||||
|
<div className="text-center p-6">
|
||||||
|
<p className="text-destructive mb-4">Error loading server threshold templates</p>
|
||||||
|
<Button variant="outline" onClick={() => refetch()}>
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<TemplateList
|
||||||
|
templates={templates}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onEdit={(id) => handleEditTemplate(id, 'server_threshold')}
|
||||||
|
refetchTemplates={refetch}
|
||||||
|
templateType="server_threshold"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<TemplateDialog
|
<TemplateDialog
|
||||||
open={isDialogOpen}
|
open={isDialogOpen}
|
||||||
templateId={editingTemplate}
|
templateId={editingTemplate}
|
||||||
|
templateType={editingTemplateType}
|
||||||
onOpenChange={setIsDialogOpen}
|
onOpenChange={setIsDialogOpen}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
refetch();
|
refetch();
|
||||||
@@ -86,4 +161,4 @@ export const AlertsTemplates = () => {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,19 +1,26 @@
|
|||||||
|
|
||||||
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 { ServerThresholdFields } from "./form/ServerThresholdFields";
|
||||||
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 +28,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 +42,72 @@ 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} />;
|
||||||
|
case 'server_threshold':
|
||||||
|
return <ServerThresholdFields 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,20 +123,85 @@ 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>
|
||||||
|
<SelectItem value="server_threshold">Server Threshold</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectedTemplateType !== 'server_threshold' && (
|
||||||
|
<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">
|
||||||
<TabsTrigger value="messages">Messages</TabsTrigger>
|
<TabsTrigger value="messages">{selectedTemplateType === 'server_threshold' ? 'Thresholds' : 'Messages'}</TabsTrigger>
|
||||||
<TabsTrigger value="placeholders">Placeholders</TabsTrigger>
|
<TabsTrigger value="placeholders">Placeholders</TabsTrigger>
|
||||||
</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>
|
||||||
@@ -120,4 +239,4 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
|
|
||||||
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 {
|
import { Trash2, Edit, MoreVertical } from "lucide-react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
@@ -22,63 +20,75 @@ 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> = ({
|
||||||
templates,
|
templates,
|
||||||
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,83 +96,86 @@ 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>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -71,15 +71,168 @@ export const PlaceholdersTabContent: React.FC<PlaceholdersTabContentProps> = ({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={control}
|
control={control}
|
||||||
name="threshold_placeholder"
|
name="url_placeholder"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Threshold Placeholder</FormLabel>
|
<FormLabel>URL Placeholder</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="${threshold}" {...field} />
|
<Input placeholder="${url}" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription className="text-xs">
|
<FormDescription className="text-xs">
|
||||||
Used for threshold values in alerts
|
Used for service URL
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="host_placeholder"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Host/IP Placeholder</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="${host}" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-xs">
|
||||||
|
Used for service host or IP address
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="service_type_placeholder"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Service Type Placeholder</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="${service_type}" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-xs">
|
||||||
|
Used for service type (HTTP, PING, TCP, DNS)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="port_placeholder"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Port Placeholder</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="${port}" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-xs">
|
||||||
|
Used for service port number
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="domain_placeholder"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Domain Placeholder</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="${domain}" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-xs">
|
||||||
|
Used for domain name (DNS services)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="region_name_placeholder"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Region Name Placeholder</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="${region_name}" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-xs">
|
||||||
|
Used for regional agent name
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="agent_id_placeholder"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Agent ID Placeholder</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="${agent_id}" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-xs">
|
||||||
|
Used for regional agent ID
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="uptime_placeholder"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Uptime Placeholder</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="${uptime}" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-xs">
|
||||||
|
Used for service uptime percentage
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="time_placeholder"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Time Placeholder</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="${time}" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-xs">
|
||||||
|
Used for current date and time
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="error_message_placeholder"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Error Message Placeholder</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="${error_message}" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-xs">
|
||||||
|
Used for error details when service is down
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -111,22 +264,50 @@ export const PlaceholdersTabContent: React.FC<PlaceholdersTabContentProps> = ({
|
|||||||
<code className="text-xs">${"{status}"}</code>
|
<code className="text-xs">${"{status}"}</code>
|
||||||
<p className="text-xs text-muted-foreground mt-1">Service status (UP, DOWN)</p>
|
<p className="text-xs text-muted-foreground mt-1">Service status (UP, DOWN)</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-muted/30 p-2 rounded">
|
|
||||||
<code className="text-xs">${"{threshold}"}</code>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">Service threshold value</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted/30 p-2 rounded">
|
<div className="bg-muted/30 p-2 rounded">
|
||||||
<code className="text-xs">${"{url}"}</code>
|
<code className="text-xs">${"{url}"}</code>
|
||||||
<p className="text-xs text-muted-foreground mt-1">Service URL</p>
|
<p className="text-xs text-muted-foreground mt-1">Service URL</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bg-muted/30 p-2 rounded">
|
||||||
|
<code className="text-xs">${"{host}"}</code>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Service host or IP address</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted/30 p-2 rounded">
|
||||||
|
<code className="text-xs">${"{service_type}"}</code>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Service type (HTTP, PING, TCP, DNS)</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted/30 p-2 rounded">
|
||||||
|
<code className="text-xs">${"{port}"}</code>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Service port number</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted/30 p-2 rounded">
|
||||||
|
<code className="text-xs">${"{domain}"}</code>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Domain name (DNS services)</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted/30 p-2 rounded">
|
||||||
|
<code className="text-xs">${"{region_name}"}</code>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Regional agent name</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted/30 p-2 rounded">
|
||||||
|
<code className="text-xs">${"{agent_id}"}</code>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Regional agent ID</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted/30 p-2 rounded">
|
||||||
|
<code className="text-xs">${"{uptime}"}</code>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Service uptime percentage</p>
|
||||||
|
</div>
|
||||||
<div className="bg-muted/30 p-2 rounded">
|
<div className="bg-muted/30 p-2 rounded">
|
||||||
<code className="text-xs">${"{time}"}</code>
|
<code className="text-xs">${"{time}"}</code>
|
||||||
<p className="text-xs text-muted-foreground mt-1">Current date and time</p>
|
<p className="text-xs text-muted-foreground mt-1">Current date and time</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bg-muted/30 p-2 rounded">
|
||||||
|
<code className="text-xs">${"{error_message}"}</code>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Error details when service is down</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
|
||||||
|
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>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium">System Resource Restore Messages</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="restore_cpu_message"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>CPU Restore Message</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="CPU usage on ${server_name} has returned to normal: ${cpu_usage}%"
|
||||||
|
className="min-h-20"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="restore_ram_message"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>RAM Restore Message</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Memory usage on ${server_name} has returned to normal: ${ram_usage}%"
|
||||||
|
className="min-h-20"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="restore_disk_message"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Disk Restore Message</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Disk usage on ${server_name} has returned to normal: ${disk_usage}%"
|
||||||
|
className="min-h-20"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="restore_network_message"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Network Restore Message</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Network usage on ${server_name} has returned to normal: ${network_usage}%"
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="restore_cpu_temp_message"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>CPU Temperature Restore Message</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="CPU temperature on ${server_name} has returned to normal: ${cpu_temp}°C"
|
||||||
|
className="min-h-20"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="restore_disk_io_message"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Disk I/O Restore Message</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Disk I/O on ${server_name} has returned to normal: ${disk_io} MB/s"
|
||||||
|
className="min-h-20"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Control } from "react-hook-form";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
|
interface ServerThresholdFieldsProps {
|
||||||
|
control: Control<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServerThresholdFields: React.FC<ServerThresholdFieldsProps> = ({ control }) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium">Server Resource Thresholds</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="cpu_threshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>CPU Threshold (%)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
placeholder="85"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
CPU usage percentage that triggers an alert
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="ram_threshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>RAM Threshold (%)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
placeholder="80"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Memory usage percentage that triggers an alert
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="disk_threshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Disk Threshold (%)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
placeholder="90"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Disk usage percentage that triggers an alert
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="network_threshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Network Threshold (%)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
placeholder="75"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Network usage percentage that triggers an alert
|
||||||
|
</FormDescription>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
|
|
||||||
export * from './useTemplateForm';
|
export * from './useTemplateForm';
|
||||||
@@ -1,72 +1,180 @@
|
|||||||
|
|
||||||
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 { 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', 'server_threshold'] 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"),
|
||||||
|
restore_ram_message: z.string().optional(),
|
||||||
|
restore_cpu_message: z.string().optional(),
|
||||||
|
restore_disk_message: z.string().optional(),
|
||||||
|
restore_network_message: z.string().optional(),
|
||||||
|
restore_cpu_temp_message: z.string().optional(),
|
||||||
|
restore_disk_io_message: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Server threshold schema
|
||||||
|
const serverThresholdSchema = z.object({
|
||||||
|
...baseSchema,
|
||||||
|
templateType: z.literal('server_threshold'),
|
||||||
|
cpu_threshold: z.number().min(0).max(100, "CPU threshold must be between 0-100"),
|
||||||
|
ram_threshold: z.number().min(0).max(100, "RAM threshold must be between 0-100"),
|
||||||
|
disk_threshold: z.number().min(0).max(100, "Disk threshold must be between 0-100"),
|
||||||
|
network_threshold: z.number().min(0).max(100, "Network threshold must be between 0-100"),
|
||||||
|
notification_id: z.string().optional(),
|
||||||
|
server_template_id: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combined schema
|
||||||
|
export const templateFormSchema = z.discriminatedUnion("templateType", [
|
||||||
|
serverTemplateSchema,
|
||||||
|
serviceTemplateSchema,
|
||||||
|
sslTemplateSchema,
|
||||||
|
serverThresholdSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
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",
|
||||||
|
restore_ram_message: "Memory usage on ${server_name} has returned to normal: ${ram_usage}%",
|
||||||
|
restore_cpu_message: "CPU usage on ${server_name} has returned to normal: ${cpu_usage}%",
|
||||||
|
restore_disk_message: "Disk usage on ${server_name} has returned to normal: ${disk_usage}%",
|
||||||
|
restore_network_message: "Network usage on ${server_name} has returned to normal: ${network_usage}%",
|
||||||
|
restore_cpu_temp_message: "CPU temperature on ${server_name} has returned to normal: ${cpu_temp}°C",
|
||||||
|
restore_disk_io_message: "Disk I/O on ${server_name} has returned to normal: ${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",
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'server_threshold':
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
templateType: 'server_threshold' as const,
|
||||||
|
cpu_threshold: 85,
|
||||||
|
ram_threshold: 80,
|
||||||
|
disk_threshold: 90,
|
||||||
|
network_threshold: 75,
|
||||||
|
notification_id: "",
|
||||||
|
server_template_id: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
|
||||||
|
|
||||||
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);
|
|
||||||
return templateService.getTemplate(templateId);
|
return templateService.getTemplate(templateId, templateType);
|
||||||
},
|
},
|
||||||
enabled: !!templateId && open,
|
enabled: !!templateId && open,
|
||||||
});
|
});
|
||||||
@@ -74,53 +182,71 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
|
|||||||
// Set form values when template data is loaded
|
// Set form values when template data is loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (templateData && open) {
|
if (templateData && open) {
|
||||||
// console.log("Setting form values with template data:", templateData);
|
|
||||||
|
const formData: any = {
|
||||||
form.reset({
|
|
||||||
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 || "",
|
|
||||||
resolved_message: templateData.resolved_message || "",
|
|
||||||
service_name_placeholder: templateData.service_name_placeholder || "${service_name}",
|
|
||||||
response_time_placeholder: templateData.response_time_placeholder || "${response_time}",
|
|
||||||
status_placeholder: templateData.status_placeholder || "${status}",
|
|
||||||
threshold_placeholder: templateData.threshold_placeholder || "${threshold}",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [templateData, open, form]);
|
|
||||||
|
|
||||||
// Handle form errors
|
// Define the expected fields for each template type
|
||||||
useEffect(() => {
|
const expectedFields = {
|
||||||
const subscription = form.formState.errors.root?.message &&
|
server: [
|
||||||
toast({
|
'ram_message', 'cpu_message', 'disk_message', 'network_message',
|
||||||
title: "Error",
|
'up_message', 'down_message', 'notification_id', 'warning_message',
|
||||||
description: form.formState.errors.root.message,
|
'paused_message', 'cpu_temp_message', 'disk_io_message',
|
||||||
variant: "destructive",
|
'restore_ram_message', 'restore_cpu_message', 'restore_disk_message',
|
||||||
|
'restore_network_message', 'restore_cpu_temp_message', 'restore_disk_io_message'
|
||||||
|
],
|
||||||
|
service: [
|
||||||
|
'up_message', 'down_message', 'maintenance_message', 'incident_message',
|
||||||
|
'resolved_message', 'warning_message'
|
||||||
|
],
|
||||||
|
ssl: ['expired', 'exiring_soon', 'warning'],
|
||||||
|
server_threshold: [
|
||||||
|
'cpu_threshold', 'ram_threshold', 'disk_threshold', 'network_threshold',
|
||||||
|
'notification_id', 'server_template_id'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add template-specific fields
|
||||||
|
const fieldsToProcess = expectedFields[templateType] || [];
|
||||||
|
fieldsToProcess.forEach(key => {
|
||||||
|
let value = (templateData as any)[key];
|
||||||
|
|
||||||
|
// Convert string values to numbers for server threshold fields
|
||||||
|
if (templateType === 'server_threshold' &&
|
||||||
|
['cpu_threshold', 'ram_threshold', 'disk_threshold', 'network_threshold'].includes(key)) {
|
||||||
|
value = typeof value === 'string' ? Number(value) : value;
|
||||||
|
// Ensure valid number, fallback to default if invalid
|
||||||
|
if (isNaN(value)) {
|
||||||
|
const defaults = getDefaultValues(templateType) as any;
|
||||||
|
value = defaults[key] || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the value, using empty string as fallback for string fields
|
||||||
|
formData[key] = value !== undefined && value !== null ? value : "";
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
console.log("Final form data being set:", formData);
|
||||||
if (subscription) {
|
form.reset(formData);
|
||||||
// Clean up if needed
|
}
|
||||||
}
|
}, [templateData, open, form, templateType]);
|
||||||
};
|
|
||||||
}, [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) => {
|
||||||
// console.error("Error creating template:", error);
|
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to create template. Please check your inputs and try again.",
|
description: "Failed to create template. Please check your inputs and try again.",
|
||||||
@@ -131,18 +257,18 @@ 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) => {
|
||||||
// console.error("Error updating template:", error);
|
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to update template. Please check your inputs and try again.",
|
description: "Failed to update template. Please check your inputs and try again.",
|
||||||
@@ -155,39 +281,28 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
|
|||||||
|
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
const onSubmit = (formData: TemplateFormData) => {
|
const onSubmit = (formData: TemplateFormData) => {
|
||||||
// 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);
|
|
||||||
updateMutation.mutate({ id: templateId, data: completeData });
|
updateMutation.mutate({ id: templateId, data: completeData });
|
||||||
} else {
|
} else {
|
||||||
// console.log("Creating new template");
|
|
||||||
createMutation.mutate(completeData);
|
createMutation.mutate(completeData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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,
|
||||||
@@ -196,4 +311,4 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
|
|||||||
isSubmitting,
|
isSubmitting,
|
||||||
onSubmit
|
onSubmit
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -8,12 +7,13 @@ 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 { 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 +24,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;
|
||||||
@@ -34,9 +36,9 @@ 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"), // 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(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,11 +61,27 @@ const slackSchema = baseSchema.extend({
|
|||||||
const signalSchema = baseSchema.extend({
|
const signalSchema = baseSchema.extend({
|
||||||
notification_type: z.literal("signal"),
|
notification_type: z.literal("signal"),
|
||||||
signal_number: z.string().min(1, "Signal number is required"),
|
signal_number: z.string().min(1, "Signal number is required"),
|
||||||
|
signal_api_endpoint: z.string().url("Must be a valid API endpoint URL"),
|
||||||
|
});
|
||||||
|
|
||||||
|
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 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"),
|
||||||
|
smtp_password: z.string().min(1, "SMTP password is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const webhookSchema = baseSchema.extend({
|
||||||
|
notification_type: z.literal("webhook"),
|
||||||
|
webhook_url: z.string().url("Must be a valid URL"),
|
||||||
|
webhook_payload_template: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const formSchema = z.discriminatedUnion("notification_type", [
|
const formSchema = z.discriminatedUnion("notification_type", [
|
||||||
@@ -71,11 +89,104 @@ const formSchema = z.discriminatedUnion("notification_type", [
|
|||||||
discordSchema,
|
discordSchema,
|
||||||
slackSchema,
|
slackSchema,
|
||||||
signalSchema,
|
signalSchema,
|
||||||
emailSchema
|
googleChatSchema,
|
||||||
|
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",
|
||||||
|
icon: "/upload/notification/telegram.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 = {
|
||||||
|
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": "\${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 +204,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,10 +234,14 @@ export const NotificationChannelDialog = ({
|
|||||||
onClose(false);
|
onClose(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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
|
// Handle all notification types including webhook through alert_configurations
|
||||||
const configData = {
|
const configData = {
|
||||||
...values,
|
...values,
|
||||||
service_id: values.service_id || "global",
|
service_id: values.service_id || "global",
|
||||||
@@ -137,7 +252,14 @@ export const NotificationChannelDialog = ({
|
|||||||
} else {
|
} else {
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -145,7 +267,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 +281,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,63 +297,37 @@ 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 items-center space-x-3">
|
||||||
</FormControl>
|
<img
|
||||||
<FormLabel className="font-normal">
|
src={option.icon}
|
||||||
Telegram
|
alt={`${option.label} icon`}
|
||||||
</FormLabel>
|
className="w-5 h-5 object-contain"
|
||||||
</FormItem>
|
/>
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
<div className="flex flex-col">
|
||||||
<FormControl>
|
<span className="font-medium">{option.label}</span>
|
||||||
<RadioGroupItem value="discord" />
|
<span className="text-xs text-muted-foreground">{option.description}</span>
|
||||||
</FormControl>
|
</div>
|
||||||
<FormLabel className="font-normal">
|
</div>
|
||||||
Discord
|
</SelectItem>
|
||||||
</FormLabel>
|
))}
|
||||||
</FormItem>
|
</SelectContent>
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
</Select>
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Show different fields based on notification type */}
|
|
||||||
{notificationType === "telegram" && (
|
{notificationType === "telegram" && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -243,6 +339,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 +355,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 +373,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 +392,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>
|
||||||
)}
|
)}
|
||||||
@@ -296,20 +404,230 @@ export const NotificationChannelDialog = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{notificationType === "signal" && (
|
{notificationType === "signal" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="signal_number"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Signal Number</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="+1234567890" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Signal phone number to send notifications to
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="signal_api_endpoint"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Signal API Endpoint</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="https://your-signal-api.com/v2/send" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The Rest API endpoint for your Signal service
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{notificationType === "google_chat" && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="signal_number"
|
name="google_chat_webhook_url"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Signal Number</FormLabel>
|
<FormLabel>Google Chat Webhook URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="+1234567890" {...field} />
|
<Input placeholder="https://chat.googleapis.com/v1/spaces/..." {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Google Chat webhook URL from your Google Chat space
|
||||||
|
</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>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="smtp_password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>SMTP Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Enter your SMTP password" {...field} type="password" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Password for authenticating with the SMTP server
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="webhook_payload_template"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Payload Template (Optional)</FormLabel>
|
||||||
|
|
||||||
|
<FormDescription>
|
||||||
|
JSON template for the webhook payload. Leave empty to use default template.
|
||||||
|
</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="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}
|
||||||
@@ -319,7 +637,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 +656,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>
|
||||||
@@ -346,4 +664,4 @@ export const NotificationChannelDialog = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
|
|
||||||
await alertConfigService.updateAlertConfiguration(config.id, {
|
if (config.isWebhook) {
|
||||||
enabled: !config.enabled
|
// Handle webhook toggle
|
||||||
});
|
try {
|
||||||
|
const newEnabled = config.enabled ? "off" : "on";
|
||||||
// The parent component will refresh the list
|
await pb.collection('webhook').update(config.id, {
|
||||||
onEdit(config);
|
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, {
|
||||||
|
enabled: !config.enabled
|
||||||
|
});
|
||||||
|
|
||||||
|
// The parent component will refresh the list
|
||||||
|
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>
|
||||||
@@ -119,4 +170,4 @@ export const NotificationChannelList = ({
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -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) => {
|
||||||
const success = await alertConfigService.deleteAlertConfiguration(id);
|
// Check if it's a webhook first
|
||||||
if (success) {
|
const isWebhook = webhookConfigs.find(w => w.id === id);
|
||||||
fetchAlertConfigurations();
|
|
||||||
|
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);
|
||||||
|
if (success) {
|
||||||
|
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,7 +149,9 @@ 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>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value={currentTab} className="mt-0">
|
<TabsContent value={currentTab} className="mt-0">
|
||||||
@@ -114,4 +179,4 @@ const NotificationSettings = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotificationSettings;
|
export default NotificationSettings;
|
||||||
@@ -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">
|
||||||
<FormControl>
|
<Select
|
||||||
<SelectTrigger>
|
onValueChange={handleAddNotificationChannel}
|
||||||
<SelectValue placeholder={t('chooseChannel')} />
|
value=""
|
||||||
</SelectTrigger>
|
>
|
||||||
</FormControl>
|
<FormControl>
|
||||||
<SelectContent>
|
<SelectTrigger>
|
||||||
<SelectItem value="none">{t('none')}</SelectItem>
|
<SelectValue placeholder="Select channels to add..." />
|
||||||
{alertConfigs.length > 0 ? (
|
</SelectTrigger>
|
||||||
alertConfigs.map((config) => (
|
</FormControl>
|
||||||
<SelectItem key={config.id} value={config.id || ""}>
|
<SelectContent>
|
||||||
{config.notify_name} ({config.notification_type})
|
{alertConfigs.length > 0 ? (
|
||||||
</SelectItem>
|
alertConfigs
|
||||||
))
|
.filter(config => config.id && !selectedChannels.includes(config.id))
|
||||||
) : isLoading ? (
|
.map((config) => (
|
||||||
<SelectItem value="loading" disabled>{t('loadingChannels')}</SelectItem>
|
<SelectItem key={config.id} value={config.id || "unknown"}>
|
||||||
) : (
|
{config.notify_name} ({config.notification_type})
|
||||||
<SelectItem value="none-disabled" disabled>{t('noChannelsFound')}</SelectItem>
|
</SelectItem>
|
||||||
)}
|
))
|
||||||
</SelectContent>
|
) : isLoading ? (
|
||||||
</Select>
|
<SelectItem value="loading-placeholder" disabled>{t('loadingChannels')}</SelectItem>
|
||||||
|
) : (
|
||||||
|
<SelectItem value="no-channels-placeholder" disabled>{t('noChannelsFound')}</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</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')}
|
||||||
@@ -200,6 +254,41 @@ export const AddSSLCertificateForm = ({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|||||||
@@ -7,17 +7,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,66 +36,138 @@ 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);
|
||||||
|
|
||||||
|
// Parse existing notification channels from certificate data
|
||||||
|
const parseNotificationChannels = (cert: SSLCertificate): string[] => {
|
||||||
|
const channels: string[] = [];
|
||||||
|
|
||||||
|
// Check notification_id field first (multi-channel support)
|
||||||
|
if (cert.notification_id && cert.notification_id.trim()) {
|
||||||
|
channels.push(...cert.notification_id.split(',').map(id => id.trim()).filter(Boolean));
|
||||||
|
}
|
||||||
|
// Fallback to notification_channel field
|
||||||
|
else if (cert.notification_channel && cert.notification_channel.trim()) {
|
||||||
|
channels.push(...cert.notification_channel.split(',').map(id => id.trim()).filter(Boolean));
|
||||||
|
}
|
||||||
|
return channels;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the alert template from certificate data
|
||||||
|
const getAlertTemplate = (cert: SSLCertificate): string => {
|
||||||
|
let templateId = "";
|
||||||
|
|
||||||
|
// Check template_id field first (new field for PocketBase)
|
||||||
|
if (cert.template_id && cert.template_id.trim()) {
|
||||||
|
templateId = cert.template_id;
|
||||||
|
}
|
||||||
|
// Fallback to alert_template field
|
||||||
|
else if (cert.alert_template && cert.alert_template.trim()) {
|
||||||
|
templateId = cert.alert_template;
|
||||||
|
}
|
||||||
|
|
||||||
|
return templateId || "none";
|
||||||
|
};
|
||||||
|
|
||||||
const form = useForm<FormValues>({
|
const form = useForm<FormValues>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
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: parseNotificationChannels(certificate),
|
||||||
|
alert_template: getAlertTemplate(certificate),
|
||||||
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 handleSubmit = (data: FormValues) => {
|
// Update form values when certificate data changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (certificate) {
|
||||||
|
const channels = parseNotificationChannels(certificate);
|
||||||
|
const template = getAlertTemplate(certificate);
|
||||||
|
|
||||||
|
form.reset({
|
||||||
|
domain: certificate.domain,
|
||||||
|
warning_threshold: certificate.warning_threshold,
|
||||||
|
expiry_threshold: certificate.expiry_threshold,
|
||||||
|
notification_channels: channels,
|
||||||
|
alert_template: template,
|
||||||
|
check_interval: certificate.check_interval || 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}, [certificate, form]);
|
||||||
|
|
||||||
|
const handleChannelAdd = (channelId: string) => {
|
||||||
|
const currentChannels = form.getValues("notification_channels") || [];
|
||||||
|
if (!currentChannels.includes(channelId)) {
|
||||||
|
const newChannels = [...currentChannels, channelId];
|
||||||
|
form.setValue("notification_channels", newChannels);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChannelRemove = (channelId: string) => {
|
||||||
|
const currentChannels = form.getValues("notification_channels") || [];
|
||||||
|
const newChannels = currentChannels.filter(id => id !== channelId);
|
||||||
|
form.setValue("notification_channels", newChannels);
|
||||||
|
};
|
||||||
|
|
||||||
|
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) => {
|
||||||
// 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,
|
||||||
// Ensure values are correctly typed as numbers
|
|
||||||
|
notification_channel: data.notification_channels.length > 0 ? data.notification_channels.join(',') : '',
|
||||||
|
notification_id: data.notification_channels.length > 0 ? data.notification_channels.join(',') : '',
|
||||||
|
template_id: data.alert_template && data.alert_template !== 'none' ? data.alert_template : '',
|
||||||
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 +181,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 +263,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 && (
|
||||||
<FormControl>
|
<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>
|
||||||
|
<Select
|
||||||
|
onValueChange={handleChannelAdd}
|
||||||
|
disabled={isLoading}
|
||||||
|
value=""
|
||||||
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t('chooseChannel')} />
|
<SelectValue placeholder={t('chooseChannel')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
<SelectContent>
|
||||||
<SelectContent>
|
{alertConfigs
|
||||||
{alertConfigs.length > 0 ? (
|
.filter(config => config.id && !notificationChannels?.includes(config.id))
|
||||||
alertConfigs.map((config) => (
|
.map((config) => (
|
||||||
<SelectItem key={config.id} value={config.id || ""}>
|
<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>
|
||||||
) : (
|
)}
|
||||||
<>
|
</SelectContent>
|
||||||
<SelectItem value="email">Email</SelectItem>
|
</Select>
|
||||||
<SelectItem value="telegram">Telegram</SelectItem>
|
</FormControl>
|
||||||
<SelectItem value="slack">Slack</SelectItem>
|
|
||||||
<SelectItem value="webhook">Webhook</SelectItem>
|
|
||||||
<SelectItem value="none">None</SelectItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<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 +318,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"
|
||||||
|
|||||||
@@ -29,6 +29,21 @@ export const SSLCertificateDetailDialog = ({
|
|||||||
|
|
||||||
if (!certificate) return null;
|
if (!certificate) return null;
|
||||||
|
|
||||||
|
// Parse Subject Alternative Names for better display
|
||||||
|
const formatSANs = (sans: string): string[] => {
|
||||||
|
if (!sans) return [];
|
||||||
|
|
||||||
|
// Split by common delimiters and clean up
|
||||||
|
const sansList = sans
|
||||||
|
.split(/[,;\n]/)
|
||||||
|
.map(san => san.trim())
|
||||||
|
.filter(san => san.length > 0);
|
||||||
|
|
||||||
|
return sansList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sansList = certificate.cert_sans ? formatSANs(certificate.cert_sans) : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
@@ -135,7 +150,17 @@ export const SSLCertificateDetailDialog = ({
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<p className="text-sm font-medium text-muted-foreground mb-2">{t('subjectAlternativeNames')}</p>
|
<p className="text-sm font-medium text-muted-foreground mb-2">{t('subjectAlternativeNames')}</p>
|
||||||
<div className="bg-muted px-3 py-2 rounded">
|
<div className="bg-muted px-3 py-2 rounded">
|
||||||
<p className="text-sm whitespace-pre-wrap">{certificate.cert_sans}</p>
|
{sansList.length > 0 ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{sansList.map((san, index) => (
|
||||||
|
<div key={index} className="text-sm font-mono break-all">
|
||||||
|
{san}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm">N/A</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -7,28 +7,39 @@ 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;
|
||||||
|
signal_api_endpoint?: string;
|
||||||
notify_name: string;
|
notify_name: string;
|
||||||
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;
|
||||||
|
// Email specific fields
|
||||||
|
email_address?: string;
|
||||||
|
email_sender_name?: string;
|
||||||
|
smtp_server?: string;
|
||||||
|
smtp_port?: string;
|
||||||
|
smtp_password?: string;
|
||||||
|
webhook_id?: string;
|
||||||
|
channel_id?: string;
|
||||||
|
// Webhook fields for alert_configurations
|
||||||
|
webhook_url?: string;
|
||||||
|
webhook_payload_template?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
||||||
@@ -39,20 +50,61 @@ 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 || "";
|
||||||
|
cleanConfig.signal_api_endpoint = config.signal_api_endpoint || "";
|
||||||
|
} 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 || "";
|
||||||
|
cleanConfig.smtp_password = config.smtp_password || "";
|
||||||
|
|
||||||
|
} else if (config.notification_type === "webhook") {
|
||||||
|
|
||||||
|
cleanConfig.webhook_url = config.webhook_url || "";
|
||||||
|
cleanConfig.webhook_payload_template = config.webhook_payload_template || "";
|
||||||
|
|
||||||
|
}
|
||||||
|
const result = await pb.collection('alert_configurations').create(cleanConfig);
|
||||||
|
|
||||||
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);
|
|
||||||
|
// Try to get more details from the error
|
||||||
|
if (error && typeof error === 'object') {
|
||||||
|
}
|
||||||
|
|
||||||
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 +112,27 @@ 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);
|
// Build the update config with proper field mapping
|
||||||
// console.info("Alert configuration updated:", result);
|
const updateConfig: any = {};
|
||||||
|
|
||||||
|
// Copy all provided fields
|
||||||
|
Object.keys(config).forEach(key => {
|
||||||
|
if (config[key as keyof AlertConfiguration] !== undefined) {
|
||||||
|
updateConfig[key] = config[key as keyof AlertConfiguration];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await pb.collection('alert_configurations').update(id, updateConfig);
|
||||||
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);
|
|
||||||
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 +140,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",
|
||||||
@@ -100,4 +156,4 @@ export const alertConfigService = {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,43 +1,39 @@
|
|||||||
|
|
||||||
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}`);
|
|
||||||
|
|
||||||
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 || generateDefaultUptimeMessage(service, status, responseTime);
|
||||||
} else if (status === "down") {
|
} else if (status === "down") {
|
||||||
templateText = template.down_message || `Service ${service.name} is DOWN`;
|
templateText = (template as any).down_message || generateDefaultUptimeMessage(service, status, responseTime);
|
||||||
} 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 || generateDefaultUptimeMessage(service, status, responseTime);
|
||||||
} 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 || generateDefaultUptimeMessage(service, status, responseTime);
|
||||||
} 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 || generateDefaultUptimeMessage(service, status, responseTime);
|
||||||
} else {
|
} else {
|
||||||
templateText = `Service ${service.name} status changed to: ${status}`;
|
templateText = generateDefaultUptimeMessage(service, status, responseTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip replacement if template is empty
|
// Skip replacement if template is empty
|
||||||
if (!templateText) {
|
if (!templateText) {
|
||||||
console.log("Empty template for status:", status);
|
return generateDefaultUptimeMessage(service, status, responseTime);
|
||||||
return generateDefaultMessage(service.name, status, responseTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Using template text:", templateText);
|
|
||||||
|
|
||||||
// Replace placeholders with actual values
|
// Replace placeholders with actual values
|
||||||
let message = templateText
|
let message = templateText
|
||||||
.replace(/\${service_name}/g, service.name || 'Unknown Service')
|
.replace(/\${service_name}/g, service.name || 'Unknown Service')
|
||||||
@@ -50,22 +46,113 @@ export function processTemplate(
|
|||||||
message = message.replace(/\${response_time}/g, 'N/A');
|
message = message.replace(/\${response_time}/g, 'N/A');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace any other placeholders
|
// Replace service-specific placeholders
|
||||||
message = message
|
message = message
|
||||||
.replace(/\${threshold}/g, service.threshold || 'N/A')
|
.replace(/\${url}/g, service.url || service.URL || 'N/A')
|
||||||
.replace(/\${url}/g, service.url || 'N/A')
|
.replace(/\${host}/g, service.host || service.Host || 'N/A')
|
||||||
|
.replace(/\${service_type}/g, service.service_type?.toUpperCase() || service.ServiceType?.toUpperCase() || service.type?.toUpperCase() || 'N/A')
|
||||||
|
.replace(/\${port}/g, service.port ? service.port.toString() : (service.Port ? service.Port.toString() : 'N/A'))
|
||||||
|
.replace(/\${domain}/g, service.domain || service.Domain || 'N/A')
|
||||||
|
.replace(/\${region_name}/g, service.region_name || service.RegionName || 'Default')
|
||||||
|
.replace(/\${agent_id}/g, service.agent_id ? service.agent_id.toString() : (service.AgentID ? service.AgentID.toString() : '1'))
|
||||||
|
.replace(/\${uptime}/g, service.uptime ? `${service.uptime}%` : (service.Uptime ? `${service.Uptime}%` : 'N/A'))
|
||||||
|
.replace(/\${error_message}/g, service.error_message || service.ErrorMessage || service.error || '')
|
||||||
.replace(/\${time}/g, new Date().toLocaleString());
|
.replace(/\${time}/g, new Date().toLocaleString());
|
||||||
|
|
||||||
console.log("Processed template message:", message);
|
|
||||||
return message;
|
return message;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing template:", error);
|
return generateDefaultUptimeMessage(service, status, responseTime);
|
||||||
return generateDefaultMessage(service.name, status, responseTime);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a default message when no template is available
|
* Generate a default uptime message with proper formatting and emojis
|
||||||
|
*/
|
||||||
|
export function generateDefaultUptimeMessage(
|
||||||
|
service: any,
|
||||||
|
status: string,
|
||||||
|
responseTime?: number
|
||||||
|
): string {
|
||||||
|
const serviceName = service.name || service.Name || 'Unknown Service';
|
||||||
|
const statusUpper = status.toUpperCase();
|
||||||
|
|
||||||
|
// Status emoji mapping
|
||||||
|
let statusEmoji = "🔵";
|
||||||
|
if (status === "up") {
|
||||||
|
statusEmoji = "🟢";
|
||||||
|
} else if (status === "down") {
|
||||||
|
statusEmoji = "🔴";
|
||||||
|
} else if (status === "warning") {
|
||||||
|
statusEmoji = "🟡";
|
||||||
|
} else if (status === "maintenance" || status === "paused") {
|
||||||
|
statusEmoji = "🟠";
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = `${statusEmoji}Service ${serviceName} is ${statusUpper}.`;
|
||||||
|
|
||||||
|
// Add service details
|
||||||
|
const host = service.host || service.Host;
|
||||||
|
const url = service.url || service.URL;
|
||||||
|
const serviceType = service.service_type || service.ServiceType || service.type;
|
||||||
|
const port = service.port || service.Port;
|
||||||
|
const domain = service.domain || service.Domain;
|
||||||
|
const regionName = service.region_name || service.RegionName;
|
||||||
|
const agentId = service.agent_id || service.AgentID;
|
||||||
|
const uptime = service.uptime || service.Uptime;
|
||||||
|
|
||||||
|
// Build formatted details
|
||||||
|
const details = [];
|
||||||
|
|
||||||
|
if (url && url !== 'N/A') {
|
||||||
|
details.push(` - Host URL: ${url}`);
|
||||||
|
} else if (host && host !== 'N/A') {
|
||||||
|
details.push(` - Host: ${host}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceType && serviceType !== 'N/A') {
|
||||||
|
details.push(` - Type: ${serviceType.toUpperCase()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port && port !== 'N/A') {
|
||||||
|
details.push(` - Port: ${port}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain && domain !== 'N/A') {
|
||||||
|
details.push(` - Domain: ${domain}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response time handling
|
||||||
|
if (responseTime !== undefined && responseTime > 0) {
|
||||||
|
details.push(` - Response time: ${responseTime}ms`);
|
||||||
|
} else {
|
||||||
|
details.push(` - Response time: N/A`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regionName && regionName !== 'N/A') {
|
||||||
|
details.push(` - Region: ${regionName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agentId && agentId !== 'N/A') {
|
||||||
|
details.push(` - Agent: ${agentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uptime !== undefined && uptime !== 'N/A') {
|
||||||
|
details.push(` - Uptime: ${uptime}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timestamp
|
||||||
|
details.push(` - Time: ${new Date().toLocaleString()}`);
|
||||||
|
|
||||||
|
// Combine message with details
|
||||||
|
if (details.length > 0) {
|
||||||
|
message += '\n' + details.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a default message when no template is available (legacy support)
|
||||||
*/
|
*/
|
||||||
export function generateDefaultMessage(
|
export function generateDefaultMessage(
|
||||||
serviceName: string,
|
serviceName: string,
|
||||||
@@ -83,4 +170,4 @@ export function generateDefaultMessage(
|
|||||||
message += `. Time: ${new Date().toLocaleString()}`;
|
message += `. Time: ${new Date().toLocaleString()}`;
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
@@ -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,114 @@
|
|||||||
|
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;
|
||||||
|
restore_ram_message: string;
|
||||||
|
restore_cpu_message: string;
|
||||||
|
restore_disk_message: string;
|
||||||
|
restore_network_message: string;
|
||||||
|
restore_disk_io_message: string;
|
||||||
|
restore_cpu_temp_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;
|
||||||
|
restore_ram_message: string;
|
||||||
|
restore_cpu_message: string;
|
||||||
|
restore_disk_message: string;
|
||||||
|
restore_network_message: string;
|
||||||
|
restore_disk_io_message: string;
|
||||||
|
restore_cpu_temp_message: string;
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const serverNotificationTemplateService = {
|
||||||
|
async getTemplates(): Promise<ServerNotificationTemplate[]> {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const response = await pb.collection('server_notification_templates').getList(1, 50, {
|
||||||
|
sort: '-created',
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.items as unknown as ServerNotificationTemplate[];
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTemplate(id: string): Promise<ServerNotificationTemplate> {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const response = await pb.collection('server_notification_templates').getOne(id);
|
||||||
|
|
||||||
|
return response as unknown as ServerNotificationTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTemplate(data: CreateUpdateServerNotificationTemplateData): Promise<ServerNotificationTemplate> {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const response = await pb.collection('server_notification_templates').create(data);
|
||||||
|
|
||||||
|
return response as unknown as ServerNotificationTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTemplate(id: string, data: Partial<CreateUpdateServerNotificationTemplateData>): Promise<ServerNotificationTemplate> {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const response = await pb.collection('server_notification_templates').update(id, data);
|
||||||
|
|
||||||
|
return response as unknown as ServerNotificationTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteTemplate(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
|
||||||
|
await pb.collection('server_notification_templates').delete(id);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -9,7 +9,6 @@ export type { Service, UptimeData, CreateServiceParams };
|
|||||||
export const serviceService = {
|
export const serviceService = {
|
||||||
async getServices(): Promise<Service[]> {
|
async getServices(): Promise<Service[]> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// First get the total count of records
|
// First get the total count of records
|
||||||
const countResponse = await pb.collection('services').getList(1, 1, {
|
const countResponse = await pb.collection('services').getList(1, 1, {
|
||||||
sort: 'name',
|
sort: 'name',
|
||||||
@@ -37,7 +36,7 @@ export const serviceService = {
|
|||||||
retries: item.max_retries || item.retries || 3,
|
retries: item.max_retries || item.retries || 3,
|
||||||
notificationChannel: item.notification_id,
|
notificationChannel: item.notification_id,
|
||||||
notification_channel: item.notification_channel, // Add this field for multiple channels support
|
notification_channel: item.notification_channel, // Add this field for multiple channels support
|
||||||
notification_status: item.notification_status || "disabled",
|
notification_status: item.notification_status || false,
|
||||||
alertTemplate: item.template_id,
|
alertTemplate: item.template_id,
|
||||||
muteAlerts: item.alerts === "muted", // Convert string to boolean for compatibility
|
muteAlerts: item.alerts === "muted", // Convert string to boolean for compatibility
|
||||||
alerts: item.alerts || "unmuted", // Store actual database field
|
alerts: item.alerts || "unmuted", // Store actual database field
|
||||||
@@ -49,7 +48,6 @@ export const serviceService = {
|
|||||||
regional_monitoring_enabled: item.regional_status === "enabled", // Backward compatibility
|
regional_monitoring_enabled: item.regional_status === "enabled", // Backward compatibility
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("Error fetching services:", error);
|
|
||||||
throw new Error('Failed to load services data.');
|
throw new Error('Failed to load services data.');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -60,7 +58,6 @@ export const serviceService = {
|
|||||||
const serviceType = params.type.toLowerCase();
|
const serviceType = params.type.toLowerCase();
|
||||||
|
|
||||||
// Debug log to check what we're sending
|
// Debug log to check what we're sending
|
||||||
// console.log("Creating service with params:", params);
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
name: params.name,
|
name: params.name,
|
||||||
@@ -71,16 +68,16 @@ export const serviceService = {
|
|||||||
last_checked: new Date().toLocaleString(),
|
last_checked: new Date().toLocaleString(),
|
||||||
heartbeat_interval: params.interval,
|
heartbeat_interval: params.interval,
|
||||||
max_retries: params.retries,
|
max_retries: params.retries,
|
||||||
notification_status: params.notificationStatus || "disabled",
|
// Store notification_status as boolean
|
||||||
// Store multiple notification channels as JSON string
|
notification_status: params.notificationStatus === true,
|
||||||
|
// Always store channels and template if provided (don't clear based on status)
|
||||||
notification_channel: params.notificationChannels && params.notificationChannels.length > 0
|
notification_channel: params.notificationChannels && params.notificationChannels.length > 0
|
||||||
? JSON.stringify(params.notificationChannels)
|
? JSON.stringify(params.notificationChannels)
|
||||||
: null,
|
: null,
|
||||||
// Store multiple notification IDs as comma-separated string in notification_id field
|
|
||||||
notification_id: params.notificationChannels && params.notificationChannels.length > 0
|
notification_id: params.notificationChannels && params.notificationChannels.length > 0
|
||||||
? params.notificationChannels.join(',')
|
? params.notificationChannels.join(',')
|
||||||
: null,
|
: null,
|
||||||
template_id: params.alertTemplate,
|
template_id: params.alertTemplate || null,
|
||||||
// Regional monitoring fields - use regional_status
|
// Regional monitoring fields - use regional_status
|
||||||
regional_status: params.regionalStatus || "disabled",
|
regional_status: params.regionalStatus || "disabled",
|
||||||
region_name: params.regionName || "",
|
region_name: params.regionName || "",
|
||||||
@@ -96,9 +93,7 @@ export const serviceService = {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log("Creating service with data:", data);
|
|
||||||
const record = await pb.collection('services').create(data);
|
const record = await pb.collection('services').create(data);
|
||||||
// console.log("Service created, returned record:", record);
|
|
||||||
|
|
||||||
// Return the newly created service
|
// Return the newly created service
|
||||||
const newService = {
|
const newService = {
|
||||||
@@ -117,7 +112,7 @@ export const serviceService = {
|
|||||||
retries: record.max_retries || 3,
|
retries: record.max_retries || 3,
|
||||||
notificationChannel: record.notification_id,
|
notificationChannel: record.notification_id,
|
||||||
notification_channel: record.notification_channel,
|
notification_channel: record.notification_channel,
|
||||||
notification_status: record.notification_status || "disabled",
|
notification_status: record.notification_status || false,
|
||||||
alertTemplate: record.template_id,
|
alertTemplate: record.template_id,
|
||||||
regional_status: record.regional_status || "disabled",
|
regional_status: record.regional_status || "disabled",
|
||||||
regional_monitoring_enabled: record.regional_status === "enabled",
|
regional_monitoring_enabled: record.regional_status === "enabled",
|
||||||
@@ -130,7 +125,6 @@ export const serviceService = {
|
|||||||
|
|
||||||
return newService;
|
return newService;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("Error creating service:", error);
|
|
||||||
throw new Error('Failed to create service.');
|
throw new Error('Failed to create service.');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -141,23 +135,22 @@ export const serviceService = {
|
|||||||
const serviceType = params.type.toLowerCase();
|
const serviceType = params.type.toLowerCase();
|
||||||
|
|
||||||
// Debug log to check what we're updating
|
// Debug log to check what we're updating
|
||||||
// console.log("Updating service with params:", params);
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
name: params.name,
|
name: params.name,
|
||||||
service_type: serviceType,
|
service_type: serviceType,
|
||||||
heartbeat_interval: params.interval,
|
heartbeat_interval: params.interval,
|
||||||
max_retries: params.retries,
|
max_retries: params.retries,
|
||||||
notification_status: params.notificationStatus || "disabled",
|
// Store notification_status as boolean - preserve existing channels/template
|
||||||
// Store multiple notification channels as JSON string
|
notification_status: params.notificationStatus === true,
|
||||||
notification_channel: params.notificationChannels && params.notificationChannels.length > 0
|
// Only update channels and template if they are provided (don't clear them)
|
||||||
? JSON.stringify(params.notificationChannels)
|
...(params.notificationChannels && params.notificationChannels.length > 0 && {
|
||||||
: null,
|
notification_channel: JSON.stringify(params.notificationChannels),
|
||||||
// Store multiple notification IDs as comma-separated string in notification_id field
|
notification_id: params.notificationChannels.join(',')
|
||||||
notification_id: params.notificationChannels && params.notificationChannels.length > 0
|
}),
|
||||||
? params.notificationChannels.join(',')
|
...(params.alertTemplate && {
|
||||||
: null,
|
template_id: params.alertTemplate
|
||||||
template_id: params.alertTemplate || null,
|
}),
|
||||||
// Regional monitoring fields - use regional_status
|
// Regional monitoring fields - use regional_status
|
||||||
regional_status: params.regionalStatus || "disabled",
|
regional_status: params.regionalStatus || "disabled",
|
||||||
region_name: params.regionName || "",
|
region_name: params.regionName || "",
|
||||||
@@ -173,7 +166,6 @@ export const serviceService = {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log("Updating service with data:", data);
|
|
||||||
|
|
||||||
// Use timeout to ensure the request doesn't hang
|
// Use timeout to ensure the request doesn't hang
|
||||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
@@ -182,7 +174,6 @@ export const serviceService = {
|
|||||||
|
|
||||||
const updatePromise = pb.collection('services').update(id, data);
|
const updatePromise = pb.collection('services').update(id, data);
|
||||||
const record = await Promise.race([updatePromise, timeoutPromise]) as any;
|
const record = await Promise.race([updatePromise, timeoutPromise]) as any;
|
||||||
// console.log("Service updated, returned record:", record);
|
|
||||||
|
|
||||||
// Return the updated service
|
// Return the updated service
|
||||||
const updatedService = {
|
const updatedService = {
|
||||||
@@ -201,7 +192,7 @@ export const serviceService = {
|
|||||||
retries: record.max_retries || 3,
|
retries: record.max_retries || 3,
|
||||||
notificationChannel: record.notification_id,
|
notificationChannel: record.notification_id,
|
||||||
notification_channel: record.notification_channel,
|
notification_channel: record.notification_channel,
|
||||||
notification_status: record.notification_status || "disabled",
|
notification_status: record.notification_status || false,
|
||||||
alertTemplate: record.template_id,
|
alertTemplate: record.template_id,
|
||||||
regional_status: record.regional_status || "disabled",
|
regional_status: record.regional_status || "disabled",
|
||||||
regional_monitoring_enabled: record.regional_status === "enabled",
|
regional_monitoring_enabled: record.regional_status === "enabled",
|
||||||
@@ -211,7 +202,6 @@ export const serviceService = {
|
|||||||
|
|
||||||
return updatedService;
|
return updatedService;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//console.error("Error updating service:", error);
|
|
||||||
throw new Error(`Failed to update service: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
throw new Error(`Failed to update service: ${error instanceof Error ? error.message : 'Unknown 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,17 @@ 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 || "",
|
||||||
|
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
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
@@ -38,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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -67,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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -84,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;
|
||||||
}
|
}
|
||||||
@@ -101,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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -112,11 +112,9 @@ 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...`);
|
|
||||||
|
|
||||||
let success = 0;
|
let success = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
|
|
||||||
@@ -125,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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,100 +1,143 @@
|
|||||||
|
|
||||||
|
|
||||||
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";
|
||||||
|
import {
|
||||||
|
serverThresholdService,
|
||||||
|
ServerThreshold,
|
||||||
|
CreateUpdateServerThresholdData
|
||||||
|
} from "./serverThresholdService";
|
||||||
|
|
||||||
export interface NotificationTemplate {
|
export type TemplateType = 'server' | 'service' | 'ssl' | 'server_threshold';
|
||||||
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 | ServerThreshold;
|
||||||
name: string;
|
export type AnyTemplateData = CreateUpdateServerNotificationTemplateData | CreateUpdateServiceNotificationTemplateData | CreateUpdateSslNotificationTemplateData | CreateUpdateServerThresholdData;
|
||||||
up_message: string;
|
|
||||||
down_message: string;
|
// Export individual template types
|
||||||
service_name_placeholder: string;
|
export type { ServerNotificationTemplate, ServiceNotificationTemplate, SslNotificationTemplate, ServerThreshold };
|
||||||
response_time_placeholder: string;
|
export type { CreateUpdateServerNotificationTemplateData, CreateUpdateServiceNotificationTemplateData, CreateUpdateSslNotificationTemplateData, CreateUpdateServerThresholdData };
|
||||||
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) {
|
case 'server_threshold':
|
||||||
// console.error("Error fetching server templates:", error);
|
return serverThresholdService.getServerThresholds();
|
||||||
throw error;
|
default:
|
||||||
|
throw new Error(`Unknown template type: ${type}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
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;
|
case 'server_threshold':
|
||||||
|
return serverThresholdService.getServerThreshold(id);
|
||||||
|
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;
|
case 'server_threshold':
|
||||||
|
return serverThresholdService.createServerThreshold(data as CreateUpdateServerThresholdData);
|
||||||
|
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;
|
case 'server_threshold':
|
||||||
|
return serverThresholdService.updateServerThreshold(id, data as Partial<CreateUpdateServerThresholdData>);
|
||||||
|
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;
|
case 'server_threshold':
|
||||||
|
return serverThresholdService.deleteServerThreshold(id);
|
||||||
|
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}',
|
||||||
|
'${host}', '${service_type}', '${port}', '${domain}',
|
||||||
|
'${region_name}', '${agent_id}', '${uptime}', '${time}'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
ssl: {
|
||||||
|
label: 'SSL Certificate',
|
||||||
|
description: 'Templates for SSL certificate monitoring alerts',
|
||||||
|
placeholders: [
|
||||||
|
'${domain}', '${certificate_name}', '${expiry_date}', '${days_left}',
|
||||||
|
'${issuer}', '${serial_number}', '${time}'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
server_threshold: {
|
||||||
|
label: 'Server Threshold',
|
||||||
|
description: 'Templates for server resource threshold configurations',
|
||||||
|
placeholders: [
|
||||||
|
'${name}', '${cpu_threshold}', '${ram_threshold}', '${disk_threshold}', '${network_threshold}'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ export const commonTranslations: CommonTranslations = {
|
|||||||
english: "Englisch",
|
english: "Englisch",
|
||||||
khmer: "Khmer",
|
khmer: "Khmer",
|
||||||
german: "Deutsch",
|
german: "Deutsch",
|
||||||
|
simplifiedChinese: "Simplified Chinese",
|
||||||
goodMorning: "Guten Morgen",
|
goodMorning: "Guten Morgen",
|
||||||
goodAfternoon: "Guten Nachmittag",
|
goodAfternoon: "Guten Nachmittag",
|
||||||
goodEvening: "Guten Abend",
|
goodEvening: "Guten Abend",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
deleteSSLCertificate: "Delete SSL Certificate",
|
deleteSSLCertificate: "Delete SSL Certificate",
|
||||||
sslCertificateDetails: "SSL-Zertifikatdetails",
|
sslCertificateDetails: "SSL-Zertifikatdetails",
|
||||||
detailedInfo: "Detaillierte Informationen zum SSL-Zertifikat für",
|
detailedInfo: "Detaillierte Informationen zum SSL-Zertifikat für",
|
||||||
|
viewDetailedInformation: "View detailed information for",
|
||||||
|
|
||||||
// Status related
|
// Status related
|
||||||
valid: "Gültig",
|
valid: "Gültig",
|
||||||
@@ -48,11 +49,16 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
validFrom: "Gültig von",
|
validFrom: "Gültig von",
|
||||||
validUntil: "Gültig bis",
|
validUntil: "Gültig bis",
|
||||||
validityDays: "Gültigkeitsdauer Tage",
|
validityDays: "Gültigkeitsdauer Tage",
|
||||||
|
validityPeriod: "Validity Period",
|
||||||
organization: "Organisation",
|
organization: "Organisation",
|
||||||
commonName: "Allgemeiner Name",
|
commonName: "Allgemeiner Name",
|
||||||
serialNumber: "Seriennummer",
|
serialNumber: "Seriennummer",
|
||||||
algorithm: "Algorithmus",
|
algorithm: "Algorithmus",
|
||||||
subjectAltNames: "Thema Alternative Bezeichnungen",
|
subjectAltNames: "Thema Alternative Bezeichnungen",
|
||||||
|
subjectAlternativeNames: "Subject Alternative Names",
|
||||||
|
resolvedIP: "Resolved IP",
|
||||||
|
issuedTo: "Issued To",
|
||||||
|
days: "days",
|
||||||
|
|
||||||
// Buttons and actions
|
// Buttons and actions
|
||||||
addDomain: "Domain hinzufügen",
|
addDomain: "Domain hinzufügen",
|
||||||
@@ -72,8 +78,11 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
validity: "Gültigkeit",
|
validity: "Gültigkeit",
|
||||||
issuerInfo: "Informationen zum Aussteller",
|
issuerInfo: "Informationen zum Aussteller",
|
||||||
technicalDetails: "Technische Details",
|
technicalDetails: "Technische Details",
|
||||||
|
technicalInformation: "Technical Information",
|
||||||
monitoringConfig: "Monitoring-Konfiguration",
|
monitoringConfig: "Monitoring-Konfiguration",
|
||||||
|
monitoringConfiguration: "Monitoring Configuration",
|
||||||
recordInfo: "Aufzeichnungsinformationen",
|
recordInfo: "Aufzeichnungsinformationen",
|
||||||
|
certificateDetails: "Certificate Details",
|
||||||
|
|
||||||
// Notifications and messages
|
// Notifications and messages
|
||||||
sslCertificateAdded: "SSL-Zertifikat erfolgreich hinzugefügt",
|
sslCertificateAdded: "SSL-Zertifikat erfolgreich hinzugefügt",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const commonTranslations: CommonTranslations = {
|
|||||||
english: "English",
|
english: "English",
|
||||||
khmer: "Khmer",
|
khmer: "Khmer",
|
||||||
german: "Deutsch",
|
german: "Deutsch",
|
||||||
|
simplifiedChinese: "Simplified Chinese",
|
||||||
goodMorning: "Good morning",
|
goodMorning: "Good morning",
|
||||||
goodAfternoon: "Good afternoon",
|
goodAfternoon: "Good afternoon",
|
||||||
goodEvening: "Good evening",
|
goodEvening: "Good evening",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
deleteSSLCertificate: "Delete SSL Certificate",
|
deleteSSLCertificate: "Delete SSL Certificate",
|
||||||
sslCertificateDetails: "SSL Certificate Details",
|
sslCertificateDetails: "SSL Certificate Details",
|
||||||
detailedInfo: "Detailed information for",
|
detailedInfo: "Detailed information for",
|
||||||
|
viewDetailedInformation: "View detailed information for",
|
||||||
|
|
||||||
// Status related
|
// Status related
|
||||||
valid: "Valid",
|
valid: "Valid",
|
||||||
@@ -48,11 +49,16 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
validFrom: "Valid From",
|
validFrom: "Valid From",
|
||||||
validUntil: "Valid Until",
|
validUntil: "Valid Until",
|
||||||
validityDays: "Validity Days",
|
validityDays: "Validity Days",
|
||||||
|
validityPeriod: "Validity Period",
|
||||||
organization: "Organization",
|
organization: "Organization",
|
||||||
commonName: "Common Name",
|
commonName: "Common Name",
|
||||||
serialNumber: "Serial Number",
|
serialNumber: "Serial Number",
|
||||||
algorithm: "Algorithm",
|
algorithm: "Algorithm",
|
||||||
subjectAltNames: "Subject Alternative Names",
|
subjectAltNames: "Subject Alternative Names",
|
||||||
|
subjectAlternativeNames: "Subject Alternative Names",
|
||||||
|
resolvedIP: "Resolved IP",
|
||||||
|
issuedTo: "Issued To",
|
||||||
|
days: "days",
|
||||||
|
|
||||||
// Buttons and actions
|
// Buttons and actions
|
||||||
addDomain: "Add Domain",
|
addDomain: "Add Domain",
|
||||||
@@ -72,8 +78,11 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
validity: "Validity",
|
validity: "Validity",
|
||||||
issuerInfo: "Issuer Information",
|
issuerInfo: "Issuer Information",
|
||||||
technicalDetails: "Technical Details",
|
technicalDetails: "Technical Details",
|
||||||
|
technicalInformation: "Technical Information",
|
||||||
monitoringConfig: "Monitoring Configuration",
|
monitoringConfig: "Monitoring Configuration",
|
||||||
|
monitoringConfiguration: "Monitoring Configuration",
|
||||||
recordInfo: "Record Information",
|
recordInfo: "Record Information",
|
||||||
|
certificateDetails: "Certificate Details",
|
||||||
|
|
||||||
// Notifications and messages
|
// Notifications and messages
|
||||||
sslCertificateAdded: "SSL Certificate added successfully",
|
sslCertificateAdded: "SSL Certificate added successfully",
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import enTranslations from './en';
|
|||||||
import kmTranslations from './km';
|
import kmTranslations from './km';
|
||||||
import deTranslations from './de';
|
import deTranslations from './de';
|
||||||
import jaTranslations from './ja';
|
import jaTranslations from './ja';
|
||||||
|
import zhcnTranslations from './zhcn';
|
||||||
|
|
||||||
export type Language = "en" | "km" | "de" | "ja";
|
export type Language = "en" | "km" | "de" | "ja" | "zhcn";
|
||||||
|
|
||||||
export const translations = {
|
export const translations = {
|
||||||
en: enTranslations,
|
en: enTranslations,
|
||||||
km: kmTranslations,
|
km: kmTranslations,
|
||||||
de: deTranslations,
|
de: deTranslations,
|
||||||
ja: jaTranslations,
|
ja: jaTranslations,
|
||||||
|
zhcn: zhcnTranslations,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Type for accessing translations by module and key
|
// Type for accessing translations by module and key
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const commonTranslations: CommonTranslations = {
|
|||||||
english: "English",
|
english: "English",
|
||||||
khmer: "Khmer",
|
khmer: "Khmer",
|
||||||
german: "Deutsch",
|
german: "Deutsch",
|
||||||
|
simplifiedChinese: "简体中文",
|
||||||
japanese: "日本語",
|
japanese: "日本語",
|
||||||
goodMorning: "おはようございます",
|
goodMorning: "おはようございます",
|
||||||
goodAfternoon: "こんにちは",
|
goodAfternoon: "こんにちは",
|
||||||
@@ -26,4 +27,4 @@ export const commonTranslations: CommonTranslations = {
|
|||||||
description: "説明",
|
description: "説明",
|
||||||
success: "成功",
|
success: "成功",
|
||||||
error: "エラー",
|
error: "エラー",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
deleteSSLCertificate: "SSL証明書を削除",
|
deleteSSLCertificate: "SSL証明書を削除",
|
||||||
sslCertificateDetails: "SSL証明書詳細",
|
sslCertificateDetails: "SSL証明書詳細",
|
||||||
detailedInfo: "詳細情報:",
|
detailedInfo: "詳細情報:",
|
||||||
|
viewDetailedInformation: "View detailed information for",
|
||||||
|
|
||||||
// ステータス関連
|
// ステータス関連
|
||||||
valid: "有効",
|
valid: "有効",
|
||||||
@@ -47,11 +48,16 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
validFrom: "有効開始日",
|
validFrom: "有効開始日",
|
||||||
validUntil: "有効終了日",
|
validUntil: "有効終了日",
|
||||||
validityDays: "有効期間",
|
validityDays: "有効期間",
|
||||||
|
validityPeriod: "Validity Period",
|
||||||
organization: "組織",
|
organization: "組織",
|
||||||
commonName: "コモンネーム",
|
commonName: "コモンネーム",
|
||||||
serialNumber: "シリアル番号",
|
serialNumber: "シリアル番号",
|
||||||
algorithm: "アルゴリズム",
|
algorithm: "アルゴリズム",
|
||||||
subjectAltNames: "サブジェクト代替名",
|
subjectAltNames: "サブジェクト代替名",
|
||||||
|
subjectAlternativeNames: "Subject Alternative Names",
|
||||||
|
resolvedIP: "Resolved IP",
|
||||||
|
issuedTo: "Issued To",
|
||||||
|
days: "days",
|
||||||
|
|
||||||
// ボタンとアクション
|
// ボタンとアクション
|
||||||
addDomain: "ドメインを追加",
|
addDomain: "ドメインを追加",
|
||||||
@@ -71,8 +77,11 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
validity: "有効性",
|
validity: "有効性",
|
||||||
issuerInfo: "発行者情報",
|
issuerInfo: "発行者情報",
|
||||||
technicalDetails: "技術的詳細",
|
technicalDetails: "技術的詳細",
|
||||||
|
technicalInformation: "Technical Information",
|
||||||
monitoringConfig: "監視設定",
|
monitoringConfig: "監視設定",
|
||||||
|
monitoringConfiguration: "Monitoring Configuration",
|
||||||
recordInfo: "レコード情報",
|
recordInfo: "レコード情報",
|
||||||
|
certificateDetails: "Certificate Details",
|
||||||
|
|
||||||
// 通知とメッセージ
|
// 通知とメッセージ
|
||||||
sslCertificateAdded: "SSL証明書が正常に追加されました",
|
sslCertificateAdded: "SSL証明書が正常に追加されました",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const commonTranslations: CommonTranslations = {
|
|||||||
english: "អង់គ្លេស",
|
english: "អង់គ្លេស",
|
||||||
khmer: "ខ្មែរ",
|
khmer: "ខ្មែរ",
|
||||||
german: "Deutsch",
|
german: "Deutsch",
|
||||||
|
simplifiedChinese: "简体中文",
|
||||||
goodMorning: "អរុណសួស្តី",
|
goodMorning: "អរុណសួស្តី",
|
||||||
goodAfternoon: "ទិវាសួស្តី",
|
goodAfternoon: "ទិវាសួស្តី",
|
||||||
goodEvening: "សាយណ្ហសួស្តី",
|
goodEvening: "សាយណ្ហសួស្តី",
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
editSSLCertificate: "កែសម្រួលវិញ្ញាបនបត្រ SSL",
|
editSSLCertificate: "កែសម្រួលវិញ្ញាបនបត្រ SSL",
|
||||||
deleteSSLCertificate: "លុបវិញ្ញាបនបត្រ SSL",
|
deleteSSLCertificate: "លុបវិញ្ញាបនបត្រ SSL",
|
||||||
sslCertificateDetails: "ព័ត៌មានលម្អិតនៃវិញ្ញាបនបត្រ SSL",
|
sslCertificateDetails: "ព័ត៌មានលម្អិតនៃវិញ្ញាបនបត្រ SSL",
|
||||||
detailedInfo: "ព័ត៌មានលម្អិតសម្រាប់",
|
detailedInfo: "ព័ត៌មានលម្អិត",
|
||||||
|
viewDetailedInformation: "មើលព័ត៌មានលម្អិត",
|
||||||
|
|
||||||
// Status related
|
// Status related
|
||||||
valid: "មានសុពលភាព",
|
valid: "មានសុពលភាព",
|
||||||
@@ -48,11 +49,16 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
validFrom: "មានសុពលភាពចាប់ពី",
|
validFrom: "មានសុពលភាពចាប់ពី",
|
||||||
validUntil: "មានសុពលភាពរហូតដល់",
|
validUntil: "មានសុពលភាពរហូតដល់",
|
||||||
validityDays: "ថ្ងៃសុពលភាព",
|
validityDays: "ថ្ងៃសុពលភាព",
|
||||||
|
validityPeriod: "រយៈពេលសុពលភាព",
|
||||||
organization: "អង្គការ",
|
organization: "អង្គការ",
|
||||||
commonName: "ឈ្មោះទូទៅ",
|
commonName: "ឈ្មោះទូទៅ",
|
||||||
serialNumber: "លេខសៀរៀល",
|
serialNumber: "លេខសៀរៀល",
|
||||||
algorithm: "អាល់ហ្គោរីត",
|
algorithm: "អាល់ហ្គោរីត",
|
||||||
subjectAltNames: "ឈ្មោះផ្សេងៗដែលអាចប្រើបាន",
|
subjectAltNames: "ឈ្មោះផ្សេងៗដែលអាចប្រើបាន",
|
||||||
|
subjectAlternativeNames: "ឈ្មោះជំនួសផ្សេងៗ",
|
||||||
|
resolvedIP: "IP របស់ដូមែន",
|
||||||
|
issuedTo: "ចេញសម្រាប់",
|
||||||
|
days: "ថ្ងៃ",
|
||||||
|
|
||||||
// Buttons and actions
|
// Buttons and actions
|
||||||
addDomain: "បន្ថែមដូមេន",
|
addDomain: "បន្ថែមដូមេន",
|
||||||
@@ -72,8 +78,11 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
validity: "សុពលភាព",
|
validity: "សុពលភាព",
|
||||||
issuerInfo: "ព័ត៌មានអ្នកចេញផ្សាយ",
|
issuerInfo: "ព័ត៌មានអ្នកចេញផ្សាយ",
|
||||||
technicalDetails: "ព័ត៌មានបច្ចេកទេស",
|
technicalDetails: "ព័ត៌មានបច្ចេកទេស",
|
||||||
|
technicalInformation: "ព័ត៌មានបច្ចេកទេស",
|
||||||
monitoringConfig: "ការកំណត់រចនាសម្ព័ន្ធការតាមដាន",
|
monitoringConfig: "ការកំណត់រចនាសម្ព័ន្ធការតាមដាន",
|
||||||
|
monitoringConfiguration: "ការកំណត់រចនាសម្ព័ន្ធការតាមដាន",
|
||||||
recordInfo: "ព័ត៌មានកំណត់ត្រា",
|
recordInfo: "ព័ត៌មានកំណត់ត្រា",
|
||||||
|
certificateDetails: "ព័ត៌មានលម្អិតវិញ្ញាបនបត្រ",
|
||||||
|
|
||||||
// Notifications and messages
|
// Notifications and messages
|
||||||
sslCertificateAdded: "វិញ្ញាបនបត្រ SSL បានបន្ថែមដោយជោគជ័យ",
|
sslCertificateAdded: "វិញ្ញាបនបត្រ SSL បានបន្ថែមដោយជោគជ័យ",
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export interface CommonTranslations {
|
|||||||
language: string;
|
language: string;
|
||||||
english: string;
|
english: string;
|
||||||
khmer: string;
|
khmer: string;
|
||||||
|
german: string;
|
||||||
|
japanese: string;
|
||||||
|
simplifiedChinese: string;
|
||||||
goodMorning: string;
|
goodMorning: string;
|
||||||
goodAfternoon: string;
|
goodAfternoon: string;
|
||||||
goodEvening: string;
|
goodEvening: string;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface SSLTranslations {
|
|||||||
deleteSSLCertificate: string;
|
deleteSSLCertificate: string;
|
||||||
sslCertificateDetails: string;
|
sslCertificateDetails: string;
|
||||||
detailedInfo: string;
|
detailedInfo: string;
|
||||||
|
viewDetailedInformation: string;
|
||||||
|
|
||||||
// Status related
|
// Status related
|
||||||
valid: string;
|
valid: string;
|
||||||
@@ -46,11 +47,16 @@ export interface SSLTranslations {
|
|||||||
validFrom: string;
|
validFrom: string;
|
||||||
validUntil: string;
|
validUntil: string;
|
||||||
validityDays: string;
|
validityDays: string;
|
||||||
|
validityPeriod: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
algorithm: string;
|
algorithm: string;
|
||||||
subjectAltNames: string;
|
subjectAltNames: string;
|
||||||
|
subjectAlternativeNames: string;
|
||||||
|
resolvedIP: string;
|
||||||
|
issuedTo: string;
|
||||||
|
days: string;
|
||||||
|
|
||||||
// Buttons and actions
|
// Buttons and actions
|
||||||
addDomain: string;
|
addDomain: string;
|
||||||
@@ -70,8 +76,11 @@ export interface SSLTranslations {
|
|||||||
validity: string;
|
validity: string;
|
||||||
issuerInfo: string;
|
issuerInfo: string;
|
||||||
technicalDetails: string;
|
technicalDetails: string;
|
||||||
|
technicalInformation: string;
|
||||||
monitoringConfig: string;
|
monitoringConfig: string;
|
||||||
|
monitoringConfiguration: string;
|
||||||
recordInfo: string;
|
recordInfo: string;
|
||||||
|
certificateDetails: string;
|
||||||
|
|
||||||
// Notifications and messages
|
// Notifications and messages
|
||||||
sslCertificateAdded: string;
|
sslCertificateAdded: string;
|
||||||
@@ -102,4 +111,4 @@ export interface SSLTranslations {
|
|||||||
lastUpdated: string;
|
lastUpdated: string;
|
||||||
lastNotification: string;
|
lastNotification: string;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
import { AboutTranslations } from '../types/about';
|
||||||
|
|
||||||
|
export const aboutTranslations: AboutTranslations = {
|
||||||
|
aboutCheckcle: "关于 Checkcle",
|
||||||
|
systemDescription: "Checkcle 是一个开源监控平台,可提供有关服务器和服务健康状况的实时洞察、事件管理以及透明化运营。以 MIT 许可证发布。",
|
||||||
|
systemVersion: "系统版本",
|
||||||
|
license: "许可证",
|
||||||
|
mitLicense: "MIT 许可证",
|
||||||
|
links: "链接",
|
||||||
|
viewOnGithub: "在 GitHub 上查看",
|
||||||
|
viewDocumentation: "查看文档",
|
||||||
|
followOnX: "在 X 上关注",
|
||||||
|
joinDiscord: "加入 Discord",
|
||||||
|
quickActions: "快速操作",
|
||||||
|
quickActionsDescription: "快速访问常用的监控操作和功能。选择下面的操作开始。",
|
||||||
|
quickTips: "快速提示",
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
import { CommonTranslations } from '../types/common';
|
||||||
|
|
||||||
|
export const commonTranslations: CommonTranslations = {
|
||||||
|
welcome: "欢迎",
|
||||||
|
logout: "注销",
|
||||||
|
language: "语言",
|
||||||
|
english: "English",
|
||||||
|
khmer: "Khmer",
|
||||||
|
german: "Deutsch",
|
||||||
|
simplifiedChinese: "简体中文",
|
||||||
|
goodMorning: "早上好",
|
||||||
|
goodAfternoon: "下午好",
|
||||||
|
goodEvening: "晚上好",
|
||||||
|
profile: "个人资料",
|
||||||
|
settings: "设置",
|
||||||
|
documentation: "文档",
|
||||||
|
notifications: "通知",
|
||||||
|
close: "关闭",
|
||||||
|
cancel: "取消",
|
||||||
|
view: "查看",
|
||||||
|
edit: "编辑",
|
||||||
|
delete: "删除",
|
||||||
|
status: "状态",
|
||||||
|
time: "时间",
|
||||||
|
title: "标题",
|
||||||
|
description: "描述",
|
||||||
|
success: "成功",
|
||||||
|
error: "错误",
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
import { IncidentTranslations } from '../types/incident';
|
||||||
|
|
||||||
|
export const incidentTranslations: IncidentTranslations = {
|
||||||
|
incidentManagement: '事件管理',
|
||||||
|
incidentsManagementDesc: '跟踪和管理服务事件及其解决方案',
|
||||||
|
unresolvedIncidents: '未解决',
|
||||||
|
resolvedIncidents: '已解决',
|
||||||
|
activeIncidents: '活动事件',
|
||||||
|
criticalIssues: '关键问题',
|
||||||
|
avgResolutionTime: '平均解决时间',
|
||||||
|
noIncidents: '没有活动事件',
|
||||||
|
createIncident: '创建事件',
|
||||||
|
investigating: '调查中',
|
||||||
|
identified: '已识别',
|
||||||
|
monitoring: '监控',
|
||||||
|
resolved: '已解决',
|
||||||
|
scheduleIncidentManagement: '计划与事件管理',
|
||||||
|
incidentName: '事件名称',
|
||||||
|
incidentStatus: '事件状态',
|
||||||
|
highPriority: '高优先级',
|
||||||
|
configurationSettings: '配置设置',
|
||||||
|
incidentCreatedSuccess: '事件创建成功',
|
||||||
|
basicInfo: '基本信息',
|
||||||
|
serviceId: '服务 ID',
|
||||||
|
assignedTo: '分配给',
|
||||||
|
unassigned: '未分配',
|
||||||
|
timeline: '时间线',
|
||||||
|
incidentTime: '事件时间',
|
||||||
|
resolutionTime: '解决时间',
|
||||||
|
systems: '系统',
|
||||||
|
noSystems: '没有受影响的系统',
|
||||||
|
impactAnalysis: '影响分析',
|
||||||
|
rootCause: '根本原因',
|
||||||
|
resolutionSteps: '解决步骤',
|
||||||
|
lessonsLearned: '经验',
|
||||||
|
resolutionDetails: '解决详情',
|
||||||
|
assignment: '分配',
|
||||||
|
download: '下载',
|
||||||
|
downloadPdf: '下载 PDF',
|
||||||
|
print: '打印',
|
||||||
|
confidentialNote: '此文档为机密文件,仅供内部使用。',
|
||||||
|
generatedOn: '生成时间',
|
||||||
|
enterResolutionSteps: '输入解决此事件的步骤',
|
||||||
|
enterLessonsLearned: '输入从此事件中学到的经验',
|
||||||
|
editIncident: '编辑事件',
|
||||||
|
editIncidentDesc: '更新此事件的详细信息',
|
||||||
|
updating: '更新中...',
|
||||||
|
update: '更新',
|
||||||
|
create: '创建',
|
||||||
|
creating: '创建中...',
|
||||||
|
configuration: '配置',
|
||||||
|
failedToUpdateStatus: '更新状态失败',
|
||||||
|
inProgress: '进行中',
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
import { Translations } from '../types';
|
||||||
|
import { commonTranslations } from './common';
|
||||||
|
import { menuTranslations } from './menu';
|
||||||
|
import { loginTranslations } from './login';
|
||||||
|
import { aboutTranslations } from './about';
|
||||||
|
import { servicesTranslations } from './services';
|
||||||
|
import { maintenanceTranslations } from './maintenance';
|
||||||
|
import { incidentTranslations } from './incident';
|
||||||
|
import { sslTranslations } from './ssl';
|
||||||
|
import { settingsTranslations } from './settings';
|
||||||
|
|
||||||
|
const zhcnTranslations: Translations = {
|
||||||
|
common: commonTranslations,
|
||||||
|
menu: menuTranslations,
|
||||||
|
login: loginTranslations,
|
||||||
|
about: aboutTranslations,
|
||||||
|
services: servicesTranslations,
|
||||||
|
maintenance: maintenanceTranslations,
|
||||||
|
incident: incidentTranslations,
|
||||||
|
ssl: sslTranslations,
|
||||||
|
settings: settingsTranslations
|
||||||
|
};
|
||||||
|
|
||||||
|
export default zhcnTranslations;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
import { LoginTranslations } from '../types/login';
|
||||||
|
|
||||||
|
export const loginTranslations: LoginTranslations = {
|
||||||
|
signInToYourAccount: "登录您的账号",
|
||||||
|
dontHaveAccount: "没有账号?",
|
||||||
|
createOne: "创建一个",
|
||||||
|
signInWithGoogle: "使用 Google 登录",
|
||||||
|
orContinueWith: "或",
|
||||||
|
email: "邮箱",
|
||||||
|
password: "密码",
|
||||||
|
forgot: "忘记密码?",
|
||||||
|
signIn: "登录",
|
||||||
|
signingIn: "登录中...",
|
||||||
|
loginSuccessful: "登录成功",
|
||||||
|
loginSuccessMessage: "您已成功登录。",
|
||||||
|
loginFailed: "登录失败",
|
||||||
|
authenticationFailed: "身份验证失败",
|
||||||
|
bySigningIn: "通过登录,您同意我们的",
|
||||||
|
termsAndConditions: "条款与条件",
|
||||||
|
and: "和",
|
||||||
|
privacyPolicy: "隐私政策",
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
|
||||||
|
import { MaintenanceTranslations } from '../types/maintenance';
|
||||||
|
|
||||||
|
export const maintenanceTranslations: MaintenanceTranslations = {
|
||||||
|
scheduledMaintenance: '计划维护',
|
||||||
|
scheduledMaintenanceDesc: '查看和管理您系统和服务的计划维护窗口',
|
||||||
|
upcomingMaintenance: '即将',
|
||||||
|
ongoingMaintenance: '进行中',
|
||||||
|
completedMaintenance: '已完成',
|
||||||
|
createMaintenanceWindow: '创建维护',
|
||||||
|
totalScheduledHours: '总计划小时',
|
||||||
|
maintenanceName: '维护名称',
|
||||||
|
maintenanceStatus: '状态',
|
||||||
|
scheduledStart: '计划开始时间',
|
||||||
|
scheduledEnd: '计划结束时间',
|
||||||
|
affectedServices: '受影响的服务',
|
||||||
|
impact: '影响',
|
||||||
|
minor: '次要',
|
||||||
|
major: '主要',
|
||||||
|
critical: '关键',
|
||||||
|
none: '无',
|
||||||
|
actions: '操作',
|
||||||
|
scheduled: '计划',
|
||||||
|
inprogress: '进行中',
|
||||||
|
completed: '已完成',
|
||||||
|
cancelled: '已取消',
|
||||||
|
markAsInProgress: '标记为进行中',
|
||||||
|
markAsCompleted: '标记为已完成',
|
||||||
|
markAsCancelled: '标记为已取消',
|
||||||
|
confirmDelete: '确认删除',
|
||||||
|
deleteMaintenanceConfirmation: '您确定要删除此维护窗口吗?',
|
||||||
|
thisActionCannotBeUndone: '此操作无法撤销。',
|
||||||
|
maintenanceDeleted: '维护已删除',
|
||||||
|
maintenanceDeletedDesc: '维护窗口已成功删除。',
|
||||||
|
errorDeletingMaintenance: '删除维护窗口时出错。',
|
||||||
|
statusUpdated: '状态更新',
|
||||||
|
maintenanceStatusUpdated: '维护状态已成功更新。',
|
||||||
|
errorUpdatingMaintenanceStatus: '更新维护状态时出错。',
|
||||||
|
createMaintenance: '创建维护',
|
||||||
|
createMaintenanceDesc: '为您的服务计划新的维护窗口',
|
||||||
|
enterTitle: '输入维护标题',
|
||||||
|
enterDescription: '输入详细的维护描述',
|
||||||
|
startTime: '开始时间',
|
||||||
|
endTime: '结束时间',
|
||||||
|
selectDate: '选择日期',
|
||||||
|
enterAffectedServices: '输入受影响的服务',
|
||||||
|
separateServicesWithComma: '用逗号分隔多个服务',
|
||||||
|
priority: '优先级',
|
||||||
|
selectPriority: '选择优先级',
|
||||||
|
selectStatus: '选择状态',
|
||||||
|
selectImpact: '选择影响',
|
||||||
|
notifySubscribers: '通知订阅者',
|
||||||
|
notifySubscribersDesc: '当此维护开始时,向所有订阅者发送通知',
|
||||||
|
maintenanceCreated: '创建维护',
|
||||||
|
maintenanceCreatedDesc: '维护窗口已成功计划。',
|
||||||
|
errorCreatingMaintenance: '创建维护窗口时出错。',
|
||||||
|
errorFetchingMaintenanceData: '获取维护数据时出错。',
|
||||||
|
low: '低',
|
||||||
|
medium: '中',
|
||||||
|
high: '高',
|
||||||
|
created: '创建',
|
||||||
|
lastUpdated: '最后更新',
|
||||||
|
subscribersWillBeNotified: '订阅者将在维护开始时收到通知',
|
||||||
|
noNotifications: '不会发送通知',
|
||||||
|
noScheduledMaintenance: '没有计划的维护',
|
||||||
|
noMaintenanceWindows: '没有此时间段的维护窗口。通过点击“创建维护”按钮创建一个。',
|
||||||
|
maintenanceCreatedSuccess: '维护窗口创建成功',
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
import { MenuTranslations } from '../types/menu';
|
||||||
|
|
||||||
|
export const menuTranslations: MenuTranslations = {
|
||||||
|
uptimeMonitoring: "Uptime 监控",
|
||||||
|
instanceMonitoring: "实例监控",
|
||||||
|
sslDomain: "SSL & 域名",
|
||||||
|
scheduleIncident: "计划与事件",
|
||||||
|
operationalPage: "运营页面",
|
||||||
|
reports: "报告",
|
||||||
|
regionalMonitoring: "区域监控",
|
||||||
|
settingPanel: "设置面板",
|
||||||
|
generalSettings: "一般设置",
|
||||||
|
userManagement: "用户管理",
|
||||||
|
notificationSettings: "通知设置",
|
||||||
|
alertsTemplates: "警报模板",
|
||||||
|
rolesManagement: "角色管理",
|
||||||
|
dataRetention: "数据保留",
|
||||||
|
backupSettings: "备份设置",
|
||||||
|
aboutSystem: "关于系统",
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
import { ServicesTranslations } from '../types/services';
|
||||||
|
|
||||||
|
export const servicesTranslations: ServicesTranslations = {
|
||||||
|
serviceName: "服务名称",
|
||||||
|
serviceType: "服务类型",
|
||||||
|
serviceStatus: "服务状态",
|
||||||
|
responseTime: "响应时间",
|
||||||
|
uptime: "Uptime",
|
||||||
|
lastChecked: "最后检查时间",
|
||||||
|
noServices: "没有符合您筛选条件的服务。",
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
import { SettingsTranslations } from '../types/settings';
|
||||||
|
|
||||||
|
export const settingsTranslations: SettingsTranslations = {
|
||||||
|
// Tabs
|
||||||
|
systemSettings: "系统设置",
|
||||||
|
mailSettings: "邮件设置",
|
||||||
|
|
||||||
|
// System Settings
|
||||||
|
appName: "应用名称",
|
||||||
|
appURL: "应用 URL",
|
||||||
|
senderName: "发送者名称",
|
||||||
|
senderEmail: "发送者邮箱地址",
|
||||||
|
hideControls: "隐藏控件",
|
||||||
|
|
||||||
|
// Mail Settings
|
||||||
|
smtpSettings: "SMTP 配置",
|
||||||
|
smtpEnabled: "启用 SMTP",
|
||||||
|
smtpHost: "SMTP 主机",
|
||||||
|
smtpPort: "SMTP 端口",
|
||||||
|
smtpUsername: "SMTP 用户名",
|
||||||
|
smtpPassword: "SMTP 密码",
|
||||||
|
smtpAuthMethod: "认证方法",
|
||||||
|
enableTLS: "启用 TLS",
|
||||||
|
localName: "本地名称",
|
||||||
|
|
||||||
|
// Test Email
|
||||||
|
testEmail: "测试邮箱",
|
||||||
|
sendTestEmail: "发送测试邮箱",
|
||||||
|
emailTemplate: "邮箱模板",
|
||||||
|
verification: "验证",
|
||||||
|
passwordReset: "密码重置",
|
||||||
|
confirmEmailChange: "确认邮箱变更",
|
||||||
|
otp: "OTP",
|
||||||
|
loginAlert: "登录警报",
|
||||||
|
authCollection: "认证集合",
|
||||||
|
selectCollection: "选择集合",
|
||||||
|
toEmailAddress: "收件人邮箱地址",
|
||||||
|
enterEmailAddress: "输入收件人邮箱地址",
|
||||||
|
sending: "发送中...",
|
||||||
|
|
||||||
|
// Actions and status
|
||||||
|
save: "保存变更",
|
||||||
|
saving: "保存中...",
|
||||||
|
settingsUpdated: "设置已成功更新",
|
||||||
|
errorSavingSettings: "保存设置时出错",
|
||||||
|
errorFetchingSettings: "加载设置时出错",
|
||||||
|
testConnection: "测试连接",
|
||||||
|
testingConnection: "测试连接中...",
|
||||||
|
connectionSuccess: "连接成功",
|
||||||
|
connectionFailed: "连接失败"
|
||||||
|
};
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
|
||||||
|
import { SSLTranslations } from '../types/ssl';
|
||||||
|
|
||||||
|
export const sslTranslations: SSLTranslations = {
|
||||||
|
// Page and section titles
|
||||||
|
sslDomainManagement: "SSL & 域名管理",
|
||||||
|
monitorSSLCertificates: "监控和管理您的域名的 SSL 证书",
|
||||||
|
addSSLCertificate: "添加 SSL 证书",
|
||||||
|
editSSLCertificate: "编辑 SSL 证书",
|
||||||
|
deleteSSLCertificate: "删除 SSL 证书",
|
||||||
|
sslCertificateDetails: "SSL 证书详情",
|
||||||
|
detailedInfo: "关于",
|
||||||
|
viewDetailedInformation: "查看详细信息",
|
||||||
|
|
||||||
|
// Status related
|
||||||
|
valid: "有效",
|
||||||
|
expiringSoon: "即将过期",
|
||||||
|
expired: "已过期",
|
||||||
|
pending: "待处理",
|
||||||
|
|
||||||
|
// Statistics and cards
|
||||||
|
validCertificates: "有效证书",
|
||||||
|
expiringSoonCertificates: "即将过期",
|
||||||
|
expiredCertificates: "已过期",
|
||||||
|
|
||||||
|
// Form fields
|
||||||
|
domain: "域",
|
||||||
|
domainName: "域名",
|
||||||
|
domainCannotChange: "域名创建后不能更改",
|
||||||
|
warningThreshold: "警告阈值",
|
||||||
|
warningThresholdDays: "警告阈值(天)",
|
||||||
|
expiryThreshold: "过期阈值",
|
||||||
|
expiryThresholdDays: "过期阈值(天)",
|
||||||
|
notificationChannel: "通知渠道",
|
||||||
|
chooseChannel: "选择通知渠道",
|
||||||
|
whereToSend: "通知渠道",
|
||||||
|
daysBeforeExpiration: "过期前警告天数",
|
||||||
|
daysBeforeCritical: "过期前关键警告天数",
|
||||||
|
getNotifiedExpiration: "获取证书即将过期的通知",
|
||||||
|
getNotifiedCritical: "获取证书即将过期的关键通知",
|
||||||
|
|
||||||
|
// Table headers and fields
|
||||||
|
issuer: "发行机构",
|
||||||
|
expirationDate: "过期日期",
|
||||||
|
daysLeft: "剩余天数",
|
||||||
|
status: "状态",
|
||||||
|
lastNotified: "最后通知时间",
|
||||||
|
actions: "操作",
|
||||||
|
validFrom: "有效从",
|
||||||
|
validUntil: "有效至",
|
||||||
|
validityDays: "有效期天数",
|
||||||
|
validityPeriod: "有效期",
|
||||||
|
organization: "组织",
|
||||||
|
commonName: "通用名称",
|
||||||
|
serialNumber: "序列号",
|
||||||
|
algorithm: "算法",
|
||||||
|
subjectAltNames: "主题备用名称",
|
||||||
|
subjectAlternativeNames: "主题备用名称",
|
||||||
|
resolvedIP: "解析 IP",
|
||||||
|
issuedTo: "颁发给",
|
||||||
|
days: "天",
|
||||||
|
|
||||||
|
// Buttons and actions
|
||||||
|
addDomain: "添加域名",
|
||||||
|
refreshAll: "刷新所有",
|
||||||
|
cancel: "取消",
|
||||||
|
addCertificate: "添加证书",
|
||||||
|
check: "检查",
|
||||||
|
view: "查看",
|
||||||
|
edit: "编辑",
|
||||||
|
delete: "删除",
|
||||||
|
close: "关闭",
|
||||||
|
saveChanges: "保存更改",
|
||||||
|
updating: "更新中",
|
||||||
|
|
||||||
|
// Sections in detail view
|
||||||
|
basicInformation: "基本信息",
|
||||||
|
validity: "有效期",
|
||||||
|
issuerInfo: "发行机构信息",
|
||||||
|
technicalDetails: "技术详情",
|
||||||
|
technicalInformation: "技术信息",
|
||||||
|
monitoringConfig: "监控配置",
|
||||||
|
monitoringConfiguration: "监控配置",
|
||||||
|
recordInfo: "记录信息",
|
||||||
|
certificateDetails: "证书详情",
|
||||||
|
|
||||||
|
// Notifications and messages
|
||||||
|
sslCertificateAdded: "SSL 证书添加成功",
|
||||||
|
sslCertificateUpdated: "SSL 证书更新成功",
|
||||||
|
sslCertificateDeleted: "SSL 证书删除成功",
|
||||||
|
sslCertificateRefreshed: "SSL 证书为 {domain} 刷新成功",
|
||||||
|
allCertificatesRefreshed: "所有 {count} 证书刷新成功",
|
||||||
|
someCertificatesFailed: "{success} 证书刷新成功,{failed} 失败",
|
||||||
|
failedToAddCertificate: "添加 SSL 证书失败",
|
||||||
|
failedToLoadCertificates: "加载 SSL 证书失败",
|
||||||
|
failedToUpdateCertificate: "更新 SSL 证书失败",
|
||||||
|
failedToDeleteCertificate: "删除 SSL 证书失败",
|
||||||
|
failedToCheckCertificate: "检查 SSL 证书失败",
|
||||||
|
noCertificatesToRefresh: "没有证书可刷新",
|
||||||
|
startingRefreshAll: "开始刷新 {count} 证书",
|
||||||
|
checkingSSLCertificate: "检查 SSL 证书...",
|
||||||
|
deleteConfirmation: "确定要删除 {domain} 的证书吗?",
|
||||||
|
deleteWarning: "此操作无法撤销。这将永久删除证书。",
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
unknown: "未知",
|
||||||
|
never: "从不",
|
||||||
|
none: "无",
|
||||||
|
loadingChannels: "加载通知渠道...",
|
||||||
|
noChannelsFound: "未找到通知渠道",
|
||||||
|
noSSLCertificates: "未找到 SSL 证书",
|
||||||
|
created: "创建时间",
|
||||||
|
lastUpdated: "最后更新时间",
|
||||||
|
lastNotification: "最后通知时间",
|
||||||
|
collectionId: "集合 ID"
|
||||||
|
};
|
||||||
@@ -21,6 +21,8 @@ export interface Server {
|
|||||||
template_id: string;
|
template_id: string;
|
||||||
threshold_id: string;
|
threshold_id: string;
|
||||||
notification_id: string;
|
notification_id: string;
|
||||||
|
notification_status: boolean;
|
||||||
|
max_retries: number;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
connection: string;
|
connection: string;
|
||||||
agent_status: string;
|
agent_status: string;
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
## 🌐 Select Language
|
## 🌐 言語選択
|
||||||
|
|
||||||
<table align="center">
|
<table align="center">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="CONTRIBUTING_ja.md">
|
<a href="CONTRIBUTING_ja.md">
|
||||||
<img src="https://flagcdn.com/24x18/jp.png" alt="Japanese" />
|
<img src="https://flagcdn.com/24x18/jp.png" alt="Japanese" />
|
||||||
<br/><strong>Japanese</strong>
|
<br/><strong>日本語</strong>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -25,11 +25,11 @@
|
|||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Thank you to all our contributors, users, and supporters for making this project thrive.
|
このプロジェクトを発展させてくださったすべての貢献者、ユーザー、サポーターの皆様に感謝いたします。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
🚀 <strong>Stay tuned for more updates, features, and improvements.</strong>
|
🚀 <strong>今後のアップデート、機能、改善にご期待ください。</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# 🛠️ CheckCleへの貢献
|
# 🛠️ CheckCleへの貢献
|
||||||
@@ -94,7 +94,37 @@ npm install && npm run dev
|
|||||||
|
|
||||||
#サーバーバックエンド
|
#サーバーバックエンド
|
||||||
cd server
|
cd server
|
||||||
./pocketbase serve --dir pb_data
|
./pocketbase serve --dir pb_data
|
||||||
|
|
||||||
|
localhostを使用していない場合は、次のコマンドで実行してください (./pocketbase serve --http=0.0.0.0:8090 --dir pb_data)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. サービスチェック操作の開始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#サーバーバックエンド
|
||||||
|
サービス操作を開始 (PING、HTTP、TCP、DNSのサービスチェック)
|
||||||
|
|
||||||
|
cd server/service-operation
|
||||||
|
|
||||||
|
go run main.go (localhost接続の場合、.envを変更する必要はありません)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 分散地域エージェントの開始
|
||||||
|
```bash
|
||||||
|
#### 1. リポジトリをフォーク
|
||||||
|
[GitHub](https://github.com/operacle/Distributed-Regional-Monitoring)で「Fork」をクリックして、自分のコピーを作成してください。
|
||||||
|
|
||||||
|
#### 2. フォークをクローン
|
||||||
|
git clone --branch main https://github.com/operacle/Distributed-Regional-Monitoring.git
|
||||||
|
cd Distributed-Regional-Monitoring
|
||||||
|
|
||||||
|
#### 3. Goサービスのインストール(Goサービスがインストールされていることを確認してください)
|
||||||
|
|
||||||
|
.env.example -> .envにコピー
|
||||||
|
.envファイルで地域エージェント設定を変更
|
||||||
|
そして実行: go run main.go
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -103,7 +133,7 @@ cd server
|
|||||||
|
|
||||||
1. コードが既存のスタイルと命名規則に従っていることを確認してください。
|
1. コードが既存のスタイルと命名規則に従っていることを確認してください。
|
||||||
2. 明確で簡潔なコミットメッセージを書いてください。
|
2. 明確で簡潔なコミットメッセージを書いてください。
|
||||||
3. ブランチをプッシュし、`main`ブランチにプルリクエスト(PR)を開いてください。
|
3. ブランチをプッシュし、`develop`ブランチにプルリクエスト(PR)を開いてください。
|
||||||
4. 意味のあるPRの説明を提供してください(何を/なぜ/どのように)。
|
4. 意味のあるPRの説明を提供してください(何を/なぜ/どのように)。
|
||||||
5. 関連するissueがある場合はリンクしてください(例:`Closes #12`)。
|
5. 関連するissueがある場合はリンクしてください(例:`Closes #12`)。
|
||||||
6. すべてのチェックが通ることを確認してください(例:リンティング、テスト)。
|
6. すべてのチェックが通ることを確認してください(例:リンティング、テスト)。
|
||||||
|
|||||||
@@ -69,19 +69,19 @@ The roadmap is divided into the following stages:
|
|||||||
- [ ] ✅ 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
|
||||||
- [ ] 🔧 Rate limiting and abuse protection
|
- [ ] ✅ Rate limiting and abuse protection
|
||||||
- [ ] 🎯 Improve the Operational status page
|
- [ ] 🔧 Server Windows Agent
|
||||||
|
- [ ] 🔧 Improve the Operational status page
|
||||||
|
- [ ] 🔧 More Uptime Service Type (HTTP keyword, HTTP json)
|
||||||
|
- [ ] 🔧 Server support with cpu temperature
|
||||||
|
- [ ] 🔧 Server upport with multiple disks
|
||||||
|
- [ ] 🔧 Server support with Multiple Network Interfaces
|
||||||
- [ ] 🎯 Improve the Schedule and Incident for automation
|
- [ ] 🎯 Improve the Schedule and Incident for automation
|
||||||
- [ ] 🎯 Server Windows Agent
|
- [ ] 🎯 Uptime Monitoring option for choose: HTTP/HTTPS
|
||||||
- [ ] 🎯 More Uptime Service Type (HTTP keyword, HTTP json)
|
|
||||||
- [ ] 🎯 Server support with Multiple Network Interfaces
|
|
||||||
- [ ] 🎯 Server support with cpu temperature
|
|
||||||
- [ ] 🎯 Server upport with multiple disks
|
|
||||||
- [ ] 🎯 Add 2FA support
|
- [ ] 🎯 Add 2FA support
|
||||||
- [ ] 🎯 Rate limiting and abuse protection
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -90,9 +90,10 @@ The roadmap is divided into the following stages:
|
|||||||
These are community-suggested or experimental features under review:
|
These are community-suggested or experimental features under review:
|
||||||
|
|
||||||
- [ ] Grouping uptime services
|
- [ ] Grouping uptime services
|
||||||
- [ ] OAuth2 integration
|
- [ ] OIDC Connect | OAuth2 integration
|
||||||
- [ ] Mobile PWA support
|
- [ ] Mobile PWA support
|
||||||
- [ ] Cloud-native Helm/Kubernetes deployment
|
- [ ] Cloud-native Helm/Kubernetes deployment
|
||||||
|
- [ ] Server outbound and inbound traffic usage
|
||||||
|
|
||||||
You’re welcome to propose features via [GitHub Discussions](https://github.com/operacle/checkcle/discussions) or open an issue with the `feature-request` template.
|
You’re welcome to propose features via [GitHub Discussions](https://github.com/operacle/checkcle/discussions) or open an issue with the `feature-request` template.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# 📍 開発ロードマップ
|
||||||
|
|
||||||
|
このプロジェクトの公式開発ロードマップへようこそ。このドキュメントでは、今後のリリースに向けた計画、優先順位、方向性について説明します。貢献者、ユーザー、ステークホルダーにプロジェクトの進化について情報を提供することを目的としています。
|
||||||
|
|
||||||
|
> 🔄 **注記**: このロードマップは、フィードバック、緊急性、または優先順位の変更に基づいて変更される場合があります。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ ゴールとビジョン
|
||||||
|
|
||||||
|
私たちの使命は、「アップタイム監視とサーバーインフラストラクチャインサイト」のための堅牢でスケーラブル、かつユーザーフレンドリーなソリューションを提供することです。
|
||||||
|
|
||||||
|
**主要な目標:**
|
||||||
|
- 低リソース使用量での高パフォーマンスの提供
|
||||||
|
- スケーラビリティとモジュラリティの確保
|
||||||
|
- 優れたユーザーエクスペリエンス(UX)に焦点を当てる
|
||||||
|
- オープンソースの透明性とコミュニティ主導機能の維持
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧩 主要機能
|
||||||
|
|
||||||
|
#### 提供状況:
|
||||||
|
- ✅ 認証とユーザー管理の設定
|
||||||
|
- ✅ コア監視ダッシュボード
|
||||||
|
- ✅ バックエンドとのエージェント通信
|
||||||
|
- ✅ Docker コンテナ化
|
||||||
|
- ✅ CheckCle ウェブサイト
|
||||||
|
- ✅ CheckCle デモサーバー
|
||||||
|
- ✅ SSL&ドメイン監視
|
||||||
|
- ✅ スケジュールメンテナンス
|
||||||
|
- ✅ インシデント管理
|
||||||
|
- ✅ インフラストラクチャサーバー監視
|
||||||
|
- ✅ 運用ステータス / パブリックステータスページ
|
||||||
|
- ✅ アップタイム監視(HTTP、TCP、PING、DNS)
|
||||||
|
- ✅ 分散地域監視エージェント
|
||||||
|
- ✅ システム設定パネルとメール設定
|
||||||
|
- ✅ データ保持と自動縮小
|
||||||
|
- ✅ ドキュメント付きオープンソースリリース
|
||||||
|
|
||||||
|
---
|
||||||
|
## 🚦 ロードマップステージ
|
||||||
|
|
||||||
|
ロードマップは以下のステージに分かれています:
|
||||||
|
|
||||||
|
| ステージ | 説明 |
|
||||||
|
|-------|-------------|
|
||||||
|
| 🎯 計画済み | スケジュールに組み込まれた承認済み機能または改善 |
|
||||||
|
| 🔧 進行中 | 積極的に開発中 |
|
||||||
|
| ✅ 完了 | 完全に実装およびテスト済み |
|
||||||
|
| ⏳ バックログ | 保留または優先順位付け待ち |
|
||||||
|
| 🧪 実験的 | 新しいコンセプトまたはプロトタイプのテスト |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂 マイルストーン概要
|
||||||
|
|
||||||
|
### 📦 v1.0.0-1.3.0 – 初期リリース _(目標: 2025年Q2)_
|
||||||
|
**ステータス:** ✅ 完了
|
||||||
|
**ゴール:** MVP機能完了、API安定性、コア使用事例対応。
|
||||||
|
|
||||||
|
#### 主要成果物: https://github.com/operacle/checkcle/releases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🚀 v1.4 - 1.5 – 機能強化 _(目標: 2025年Q3)_
|
||||||
|
**ステータス:** 🔧 進行中
|
||||||
|
#### 予定機能:
|
||||||
|
- [ ] ✅ サーバーとサービステーブルの行をクリック可能にして詳細ページへ遷移
|
||||||
|
- [ ] ✅ SSL ダッシュボードテーブルのページネーション実装
|
||||||
|
- [ ] ✅ サーバーエージェント(RPM、Docker コンテナ、汎用バイナリパッケージ)
|
||||||
|
- [ ] 🔧 通知システム(Webhook、Telegram、Discord、Slack、Email、Google Chat)
|
||||||
|
- [ ] 🔧 ステータスと通知に基づくアップタイムサービスとサーバー接続更新の改善
|
||||||
|
- [ ] 🔧 SSL作成後の初期チェック自動実行の改善
|
||||||
|
- [ ] 🔧 レート制限と悪用防止
|
||||||
|
- [ ] 🎯 運用ステータスページの改善
|
||||||
|
- [ ] 🎯 自動化のためのスケジュールとインシデントの改善
|
||||||
|
- [ ] 🎯 サーバー Windows エージェント
|
||||||
|
- [ ] 🎯 より多くのアップタイムサービスタイプ(HTTPキーワード、HTTP JSON)
|
||||||
|
- [ ] 🎯 複数ネットワークインターフェース対応のサーバーサポート
|
||||||
|
- [ ] 🎯 CPU温度対応のサーバーサポート
|
||||||
|
- [ ] 🎯 複数ディスク対応のサーバーサポート
|
||||||
|
- [ ] 🎯 2FA サポート追加
|
||||||
|
- [ ] 🎯 レート制限と悪用防止
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 アイデア / コミュニティウィッシュリスト
|
||||||
|
|
||||||
|
以下はレビュー中のコミュニティ提案または実験的機能です:
|
||||||
|
|
||||||
|
- [ ] アップタイムサービスのグループ化
|
||||||
|
- [ ] OAuth2統合
|
||||||
|
- [ ] モバイル PWA サポート
|
||||||
|
- [ ] クラウドネイティブ Helm/Kubernetes デプロイメント
|
||||||
|
|
||||||
|
[GitHub Discussions](https://github.com/operacle/checkcle/discussions)で機能提案を歓迎します。または`feature-request`テンプレートでイシューを開いてください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 貢献方法
|
||||||
|
|
||||||
|
コミュニティからの貢献を歓迎しています!
|
||||||
|
参加方法:
|
||||||
|
|
||||||
|
- [CONTRIBUTING.md](../CONTRIBUTING.md) | https://docs.checkcle.io をレビュー
|
||||||
|
- [Open Issues](https://github.com/operacle/checkcle/issues) を確認
|
||||||
|
- [Discussions](https://github.com/operacle/checkcle/discussions) でロードマップの形成に参加
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 最終更新
|
||||||
|
|
||||||
|
_このロードマップは **2025年7月26日** に最終更新されました。_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
CheckCle より ❤️ を込めて
|
||||||
|
[ウェブサイト](https://checkcle.io) | [GitHub](https://github.com/operacle/checkcle)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
## 🌐 Select Language
|
## 🌐 言語選択
|
||||||
|
|
||||||
<table align="center">
|
<table align="center">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -17,18 +17,24 @@
|
|||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="README_ja.md">
|
<a href="README_ja.md">
|
||||||
<img src="https://flagcdn.com/24x18/jp.png" alt="Japanese" />
|
<img src="https://flagcdn.com/24x18/jp.png" alt="Japanese" />
|
||||||
<br/><strong>Japanese</strong>
|
<br/><strong>日本語</strong>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="README_zhcn.md">
|
||||||
|
<img src="https://flagcdn.com/24x18/cn.png" alt="Chinese" />
|
||||||
|
<br/><strong>Chinese</strong>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Thank you to all our contributors, users, and supporters for making this project thrive.
|
このプロジェクトを発展させてくださったすべての貢献者、ユーザー、サポーターの皆様に感謝いたします。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
🚀 <strong>Stay tuned for more updates, features, and improvements.</strong>
|
🚀 <strong>今後のアップデート、機能、改善にご期待ください。</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|

|
||||||
@@ -39,8 +45,10 @@ CheckCleは、フルスタックシステム、アプリケーション、イン
|
|||||||
|
|
||||||
## 🎯 ライブデモ
|
## 🎯 ライブデモ
|
||||||
👉 **今すぐ試す:** [CheckCle ライブデモ](https://demo.checkcle.io)
|
👉 **今すぐ試す:** [CheckCle ライブデモ](https://demo.checkcle.io)
|
||||||
|
ユーザー: admin@example.com | パスワード: Admin123456
|
||||||
|
|
||||||
## 🌟 主要機能
|
## 🌟 主要機能
|
||||||
|
### 📝 ロードマップ : [DEVELOPMENT_ROADMAP](DEVELOPMENT_ROADMAP.md)
|
||||||
|
|
||||||
### アップタイムサービス & インフラストラクチャサーバー監視
|
### アップタイムサービス & インフラストラクチャサーバー監視
|
||||||
- HTTP、DNS、Pingプロトコルの監視
|
- HTTP、DNS、Pingプロトコルの監視
|
||||||
@@ -144,6 +152,37 @@ services:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## スポンサー
|
||||||
|
🙏 スポンサーの皆様には心より感謝しております。皆様のご貢献により、インフラストラクチャ(ホスティング、ドメイン)の維持、テストの実行、価値ある機能の継続的な開発が可能になっています。
|
||||||
|
|
||||||
|
### 🥈 シルバー・アップタイム・アライ
|
||||||
|
|
||||||
|
<a href="https://github.com/michaelortnerit">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/135371107?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### 🧡 Ping サポーター
|
||||||
|
|
||||||
|
<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%; display: block;" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### 🤝 エコシステム & コミュニティパートナー
|
||||||
|
<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>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 貢献者
|
||||||
|
CheckCleの継続的な改善にご貢献いただき、ありがとうございます。皆様は素晴らしいです 🫶
|
||||||
|
|
||||||
|
[](https://github.com/operacle/checkcle/graphs/contributors)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🤝 貢献の方法
|
## 🤝 貢献の方法
|
||||||
|
|
||||||
CheckCleの改善にご協力いただける方法をご紹介します:
|
CheckCleの改善にご協力いただける方法をご紹介します:
|
||||||
@@ -158,7 +197,7 @@ CheckCleの改善にご協力いただける方法をご紹介します:
|
|||||||
|
|
||||||
## 🌍 つながりを保つ
|
## 🌍 つながりを保つ
|
||||||
- ウェブサイト: [checkcle.io](https://checkcle.io)
|
- ウェブサイト: [checkcle.io](https://checkcle.io)
|
||||||
- ドキュメント: [docs.checkcle.io](https://docs.checkcle.io)
|
- ドキュメント: [docs.checkcle.io](https://docs.checkcle.io) | CheckCle OSS サイトプランをスポンサーしてくださった[GitBook](https://github.com/gitbookio)に特別な感謝を!
|
||||||
- GitHubリポジトリ: ⭐ [CheckCle](https://github.com/operacle/checkcle.git)
|
- GitHubリポジトリ: ⭐ [CheckCle](https://github.com/operacle/checkcle.git)
|
||||||
- コミュニティチャンネル: ディスカッションやissuesで参加してください!
|
- コミュニティチャンネル: ディスカッションやissuesで参加してください!
|
||||||
- Discord: コミュニティに参加 [@discord](https://discord.gg/xs9gbubGwX)
|
- Discord: コミュニティに参加 [@discord](https://discord.gg/xs9gbubGwX)
|
||||||
@@ -169,13 +208,10 @@ CheckCleの改善にご協力いただける方法をご紹介します:
|
|||||||
CheckCleはMITライセンスの下でリリースされています。
|
CheckCleはMITライセンスの下でリリースされています。
|
||||||
|
|
||||||
---
|
---
|
||||||
## 👥 貢献者
|
|
||||||
|
|
||||||
[](https://github.com/operacle/checkcle/graphs/contributors)
|
|
||||||
|
|
||||||
|
|
||||||
## スター履歴
|
## スター履歴
|
||||||
|
|
||||||
[](https://www.star-history.com/#operacle/checkcle&Date)
|
[](https://www.star-history.com/#operacle/checkcle&Date)
|
||||||
|
|
||||||
CheckCleで情報を把握し、オンラインを維持しましょう! 🌐
|
CheckCleで情報を把握し、オンラインを維持しましょう! 🌐
|
||||||
|
|||||||
@@ -20,6 +20,12 @@
|
|||||||
<br/><strong>Japanese</strong>
|
<br/><strong>Japanese</strong>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="README_zhcn.md">
|
||||||
|
<img src="https://flagcdn.com/24x18/cn.png" alt="Chinese" />
|
||||||
|
<br/><strong>Chinese</strong>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
## 🌐 选择语言
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="../README.md">
|
||||||
|
<img src="https://flagcdn.com/24x18/gb.png" alt="English" />
|
||||||
|
<br/><strong>English</strong>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="README_km.md">
|
||||||
|
<img src="https://flagcdn.com/24x18/kh.png" alt="Khmer" />
|
||||||
|
<br/><strong>ខ្មែរ</strong>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="README_ja.md">
|
||||||
|
<img src="https://flagcdn.com/24x18/jp.png" alt="Japanese" />
|
||||||
|
<br/><strong>Japanese</strong>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="README_zhcn.md">
|
||||||
|
<img src="https://flagcdn.com/24x18/cn.png" alt="Chinese" />
|
||||||
|
<br/><strong>Chinese</strong>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
感谢所有的贡献者、用户和支持者,是你们让这个项目蓬勃发展。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
🚀 <strong>敬请关注更多更新、功能和改进。</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 🚀 CheckCle是什么??
|
||||||
|
|
||||||
|
CheckCle 是一款开源解决方案,用于对全栈系统、应用程序和基础设施进行无缝实时监控。它为开发人员、系统管理员和 DevOps 团队提供其环境各层(无论是服务器、应用程序还是服务)的深入洞察和可操作数据。借助 CheckCle,你可以在整个技术堆栈中获得可见性、控制权以及确保最佳性能的能力。
|
||||||
|
|
||||||
|
## 🎯 在线演示
|
||||||
|
👉 **马上试试看:** [CheckCle Live Demo](https://demo.checkcle.io)
|
||||||
|
用户名: admin@example.com | 密码: Admin123456
|
||||||
|
|
||||||
|
## 🌟 核心功能
|
||||||
|
### 📝 开发路线图 : [DEVELOPMENT_ROADMAP](docs/DEVELOPMENT_ROADMAP.md)
|
||||||
|
|
||||||
|
### 服务可用性与基础设施/服务器监控
|
||||||
|
- 监控 HTTP、DNS 和 Ping 协议
|
||||||
|
- 监控基于 TCP 的服务与 API(如 FTP、SMTP、HTTP)
|
||||||
|
- 跟踪详细的可用性(正常运行时间)、响应时间与性能问题
|
||||||
|
- 分布式区域监控
|
||||||
|
- 事件历史(UP / DOWN / WARNING / PAUSE:上线 / 下线 / 警告 / 暂停)
|
||||||
|
- SSL 与域名监控(域名、签发者、到期日期、剩余天数、状态、上次通知)
|
||||||
|
- 基础设施与服务器监控:支持 Linux(🐧Debian、Ubuntu、CentOS、Red Hat 等)与 Windows(Beta);提供 CPU、内存、磁盘使用、网络活动等服务器指标;一行命令安装的 Agent 脚本
|
||||||
|
- 维护计划与事件管理
|
||||||
|
- 运行状态 / 公共状态页
|
||||||
|
- 通过电子邮件、Telegram、Discord 和 Slack 通知
|
||||||
|
- 报告与分析
|
||||||
|
- 设置面板(用户管理、数据保留、多语言、主题〔深色/浅色模式〕、通知渠道与告警模板)
|
||||||
|
|
||||||
|
## #️⃣ 入门指南
|
||||||
|
|
||||||
|
### 当前架构支持
|
||||||
|
* ✅ x86_64 PC、笔记本电脑、服务器 (amd64)
|
||||||
|
* ✅ 现代树莓派3/4/5(搭载64位操作系统)、Apple Silicon Macs (arm64)
|
||||||
|
|
||||||
|
### 使用以下方法之一安装 CheckCle :
|
||||||
|
|
||||||
|
1. 使用Docker Compose配置进行安装(推荐)
|
||||||
|
```bash
|
||||||
|
|
||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
checkcle:
|
||||||
|
image: operacle/checkcle:latest
|
||||||
|
container_name: checkcle
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8090:8090" # Web 应用端口
|
||||||
|
volumes:
|
||||||
|
- /opt/pb_data:/mnt/pb_data # 将主机目录映射到容器路径
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 4096
|
||||||
|
hard: 8192
|
||||||
|
|
||||||
|
```
|
||||||
|
2. 使用 docker run 进行安装。只需复制下面的 docker run 命令
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name checkcle \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 8090:8090 \
|
||||||
|
-v /opt/pb_data:/mnt/pb_data \
|
||||||
|
--ulimit nofile=4096:8192 \
|
||||||
|
operacle/checkcle:latest
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 网页管理
|
||||||
|
|
||||||
|
默认网址: http://0.0.0.0:8090
|
||||||
|
用户名: admin@example.com
|
||||||
|
密码: Admin123456
|
||||||
|
|
||||||
|
4. 遵循以下快速入门指南 https://docs.checkcle.io
|
||||||
|
|
||||||
|
###
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## 🌟 面向社区的 CheckCle
|
||||||
|
|
||||||
|
- 以热忱打造:由一位开源爱好者为社区创建
|
||||||
|
- 免费且开源:完全免费,无任何隐藏费用
|
||||||
|
- 协作与连接:结识同样热爱开源的志同道合者
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 赞助商
|
||||||
|
🙏 我们非常感谢我们的赞助商。你们的贡献使我们能够维护基础设施(托管、域名)、运行测试,并继续开发有价值的功能。
|
||||||
|
|
||||||
|
### 🥈 白银在线保障合作伙伴
|
||||||
|
|
||||||
|
<a href="https://github.com/michaelortnerit">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/135371107?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### 🧡 支持者
|
||||||
|
|
||||||
|
<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%; display: block;" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### 🤝 生态系统与社区合作伙伴
|
||||||
|
<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>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## 👥 贡献者
|
||||||
|
感谢你为让CheckCle变得更好所做出的贡献与持续努力,你太棒了 🫶
|
||||||
|
|
||||||
|
[](https://github.com/operacle/checkcle/graphs/contributors)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 贡献方式
|
||||||
|
|
||||||
|
以下是你可以帮助改进 CheckCle 的一些方式:
|
||||||
|
|
||||||
|
- 🐞 **报告问题** – 发现故障/异常?请在提交 [GitHub Issue](https://github.com/operacle/checkcle/issues) 告诉我们。
|
||||||
|
- 🌟 **建议功能** – 有个点子?发起 [Discussion](https://github.com/operacle/checkcle/discussions) 讨论,或提交一个功能请求 [Feature Request Issue](https://github.com/operacle/checkcle/issues)。
|
||||||
|
- 🛠 **提交 Pull Request** – 改进代码、修复缺陷、添加功能,或完善文档。
|
||||||
|
- 📝 **改善文档** – 就算是修正一个错别字也很有帮助!
|
||||||
|
- 🌍 **传播项目** – 给 [CheckCle](https://github.com/operacle/checkcle.git) 仓库点个 Star ⭐,在社交平台分享,并邀请更多人参与贡献!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌍 联系我们
|
||||||
|
- 网站: [checkcle.io](https://checkcle.io)
|
||||||
|
- 文档: [docs.checkcle.io](https://docs.checkcle.io) | 非常感谢 [GitBook](https://github.com/gitbookio) 赞助 CheckCle 的开源站点计划(OSS Site Plan)!
|
||||||
|
- 在 Discord 上聊天: 加入我们的 [@discord](https://discord.gg/xs9gbubGwX) 频道
|
||||||
|
- 在 X 上关注我们: [@checkcle_oss](https://x.com/checkcle_oss)
|
||||||
|
|
||||||
|
## 📜 许可证
|
||||||
|
|
||||||
|
CheckCle 遵循 MIT 许可证发布。
|
||||||
|
|
||||||
|
---
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
echo "Running on architecture: $(uname -m)"
|
|
||||||
|
|
||||||
# Copy PocketBase data if empty
|
|
||||||
if [ ! -f /mnt/pb_data/data.db ] && [ -d /app/pb_data ] && [ "$(ls -A /app/pb_data)" ]; then
|
|
||||||
cp -a /app/pb_data/. /mnt/pb_data/
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Start PocketBase in the background with no admin UI and filtered logs
|
|
||||||
echo "Starting CheckCle Application..."
|
|
||||||
(
|
|
||||||
/app/pocketbase serve \
|
|
||||||
--http=0.0.0.0:8090 \
|
|
||||||
--dir /mnt/pb_data \
|
|
||||||
--no-admin \
|
|
||||||
2>&1 | grep -vE 'REST API|Dashboard'
|
|
||||||
) &
|
|
||||||
|
|
||||||
# Wait for PocketBase to become available
|
|
||||||
echo "Waiting for Backend Server to become available..."
|
|
||||||
until curl -s http://localhost:8090/api/health >/dev/null; do
|
|
||||||
echo "Waiting on http://localhost:8090..."
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Backend Server is up!"
|
|
||||||
|
|
||||||
# Start Go service
|
|
||||||
echo "Starting Go Operational service..."
|
|
||||||
/app/service-operation &
|
|
||||||
|
|
||||||
echo "Default Access: admin@example.com/Admin123456"
|
|
||||||
|
|
||||||
# Keep container alive
|
|
||||||
wait
|
|
||||||
@@ -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!"
|
||||||
@@ -4,7 +4,7 @@ PORT=8091
|
|||||||
|
|
||||||
# Operation defaults
|
# Operation defaults
|
||||||
DEFAULT_COUNT=4
|
DEFAULT_COUNT=4
|
||||||
DEFAULT_TIMEOUT=10s
|
DEFAULT_TIMEOUT=15s
|
||||||
MAX_COUNT=20
|
MAX_COUNT=20
|
||||||
MAX_TIMEOUT=30s
|
MAX_TIMEOUT=30s
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func Load() *Config {
|
|||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
Port: getEnv("PORT", "8091"),
|
Port: getEnv("PORT", "8091"),
|
||||||
DefaultCount: getEnvInt("DEFAULT_COUNT", 4),
|
DefaultCount: getEnvInt("DEFAULT_COUNT", 4),
|
||||||
DefaultTimeout: getEnvDuration("DEFAULT_TIMEOUT", 10*time.Second),
|
DefaultTimeout: getEnvDuration("DEFAULT_TIMEOUT", 15*time.Second),
|
||||||
MaxCount: getEnvInt("MAX_COUNT", 20),
|
MaxCount: getEnvInt("MAX_COUNT", 20),
|
||||||
MaxTimeout: getEnvDuration("MAX_TIMEOUT", 30*time.Second),
|
MaxTimeout: getEnvDuration("MAX_TIMEOUT", 30*time.Second),
|
||||||
EnableLogging: getEnvBool("ENABLE_LOGGING", true),
|
EnableLogging: getEnvBool("ENABLE_LOGGING", true),
|
||||||
|
|||||||
@@ -12,38 +12,79 @@ import (
|
|||||||
"service-operation/handlers"
|
"service-operation/handlers"
|
||||||
"service-operation/monitoring"
|
"service-operation/monitoring"
|
||||||
"service-operation/pocketbase"
|
"service-operation/pocketbase"
|
||||||
|
servermonitoring "service-operation/server-monitoring"
|
||||||
|
sslmonitoring "service-operation/ssl-monitoring"
|
||||||
|
uptimemonitoring "service-operation/uptime-monitoring"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
//log.Println("🚀 === STARTING SERVICE OPERATION SERVER ===")
|
||||||
|
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
|
//log.Printf("📋 Configuration loaded:")
|
||||||
|
//log.Printf(" - Port: %s", cfg.Port)
|
||||||
|
//log.Printf(" - Backend PB Enabled: %t", cfg.PocketBaseEnabled)
|
||||||
|
if cfg.PocketBaseEnabled {
|
||||||
|
//log.Printf(" - PocketBase URL: %s", cfg.PocketBaseURL)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize PocketBase client (no credentials required)
|
// Initialize PocketBase client (no credentials required)
|
||||||
var pbClient *pocketbase.PocketBaseClient
|
var pbClient *pocketbase.PocketBaseClient
|
||||||
var monitoringService *monitoring.MonitoringService
|
var monitoringService *monitoring.MonitoringService
|
||||||
var sslMonitoringService *monitoring.SSLMonitoringService
|
var sslMonitoringService *monitoring.SSLMonitoringService
|
||||||
|
var sslNotificationService *sslmonitoring.SSLMonitor
|
||||||
|
var serverMonitoringService *servermonitoring.ServerMonitoringService
|
||||||
|
var uptimeMonitoringService *uptimemonitoring.UptimeMonitor
|
||||||
|
|
||||||
if cfg.PocketBaseEnabled {
|
if cfg.PocketBaseEnabled {
|
||||||
|
//log.Println("🔧 Initializing PocketBase client...")
|
||||||
var err error
|
var err error
|
||||||
pbClient, err = pocketbase.NewPocketBaseClient(cfg.PocketBaseURL)
|
pbClient, err = pocketbase.NewPocketBaseClient(cfg.PocketBaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: Failed to initialize PocketBase client: %v", err)
|
//log.Printf("⚠️ WARNING: Failed to initialize PocketBase client: %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
//log.Println("✅ PocketBase client initialized successfully")
|
||||||
|
|
||||||
|
//log.Println("🔍 Testing PocketBase connection...")
|
||||||
if err := pbClient.TestConnection(); err != nil {
|
if err := pbClient.TestConnection(); err != nil {
|
||||||
log.Printf("Warning: Backend connection test failed: %v", err)
|
//log.Printf("⚠️ WARNING: PocketBase connection test failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
// Initialize and start monitoring service with regional support
|
//log.Println("✅ PocketBase connection test successful")
|
||||||
|
|
||||||
|
// Initialize and start service monitoring with regional support
|
||||||
|
//log.Println("🔧 Initializing service monitoring...")
|
||||||
monitoringService = monitoring.NewMonitoringService(pbClient)
|
monitoringService = monitoring.NewMonitoringService(pbClient)
|
||||||
go monitoringService.Start()
|
go monitoringService.Start()
|
||||||
log.Println("Uptime Monitoring service started with Default System agent support")
|
//log.Println("✅ Service monitoring started with regional agent support")
|
||||||
|
|
||||||
// Initialize and start SSL monitoring service (unchanged - no regional support)
|
// Initialize and start SSL monitoring service (original)
|
||||||
|
//log.Println("🔧 Initializing SSL monitoring (original)...")
|
||||||
sslMonitoringService = monitoring.NewSSLMonitoringService(pbClient)
|
sslMonitoringService = monitoring.NewSSLMonitoringService(pbClient)
|
||||||
go sslMonitoringService.Start()
|
go sslMonitoringService.Start()
|
||||||
log.Println("SSL monitoring service started (independent of Default System Agent)")
|
//log.Println("✅ SSL monitoring started (independent of regional agents)")
|
||||||
|
|
||||||
|
// Initialize and start SSL notification service (new)
|
||||||
|
//log.Println("🔧 Initializing SSL notification monitoring...")
|
||||||
|
sslNotificationService = sslmonitoring.NewSSLMonitor(pbClient)
|
||||||
|
go sslNotificationService.Start()
|
||||||
|
//log.Println("✅ SSL notification monitoring started with Telegram support")
|
||||||
|
|
||||||
|
// Initialize and start server monitoring service
|
||||||
|
//log.Println("🔧 Initializing server monitoring...")
|
||||||
|
serverMonitoringService = servermonitoring.NewServerMonitoringService(pbClient)
|
||||||
|
serverMonitoringService.Start()
|
||||||
|
//log.Println("✅ Server monitoring started with notification support")
|
||||||
|
|
||||||
|
// Initialize and start uptime monitoring service
|
||||||
|
//log.Println("🔧 Initializing uptime monitoring...")
|
||||||
|
uptimeMonitoringService = uptimemonitoring.NewUptimeMonitor(pbClient)
|
||||||
|
go uptimeMonitoringService.Start()
|
||||||
|
//log.Println("✅ Uptime monitoring started with notification support")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//log.Println("🔧 Initializing HTTP handlers...")
|
||||||
handler := handlers.NewOperationHandler(cfg, pbClient)
|
handler := handlers.NewOperationHandler(cfg, pbClient)
|
||||||
|
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
@@ -61,24 +102,28 @@ func main() {
|
|||||||
// Health check
|
// Health check
|
||||||
router.HandleFunc("/health", handler.HandleHealth).Methods("GET")
|
router.HandleFunc("/health", handler.HandleHealth).Methods("GET")
|
||||||
|
|
||||||
log.Printf("Service Operation starting on port %s", cfg.Port)
|
log.Printf("=== 🌐 CHECKCLE SERVICE OPERATION SERVER READY ===")
|
||||||
|
log.Printf("🚀 Starting on port %s", cfg.Port)
|
||||||
if pbClient != nil {
|
if pbClient != nil {
|
||||||
log.Printf("Backend Server integration enabled at %s", pbClient.GetBaseURL())
|
log.Printf("✓Backend integration enabled at %s ", pbClient.GetBaseURL())
|
||||||
}
|
}
|
||||||
if monitoringService != nil {
|
if monitoringService != nil {
|
||||||
log.Printf("Automatic service monitoring enabled with regional agent support")
|
log.Printf("✓Service monitoring enabled with regional agent support")
|
||||||
}
|
}
|
||||||
if sslMonitoringService != nil {
|
if sslMonitoringService != nil {
|
||||||
log.Printf("SSL certificate monitoring enabled (independent)")
|
//log.Printf("🔒 SSL certificate monitoring enabled (independent)")
|
||||||
}
|
}
|
||||||
log.Printf("Endpoints:")
|
if sslNotificationService != nil {
|
||||||
log.Printf(" POST /operation - Full operation test (ping, dns, tcp, http, ssl)")
|
log.Printf("✓SSL notification monitoring enabled with Telegram support")
|
||||||
log.Printf(" GET /operation/quick?type=<type>&host=<host> - Quick operation test")
|
}
|
||||||
log.Printf(" POST /ping - Legacy ping endpoint")
|
if serverMonitoringService != nil {
|
||||||
log.Printf(" GET /ping/quick?host=<host> - Legacy quick ping test")
|
log.Printf("✓Server monitoring enabled with notification support")
|
||||||
log.Printf(" GET /health - Health check")
|
}
|
||||||
log.Printf("Default SystemSupported operations: ping, dns, tcp, http, ssl")
|
if uptimeMonitoringService != nil {
|
||||||
log.Printf("Default System: Tracks 'Default' region connection status")
|
log.Printf("✓Uptime monitoring enabled with notification support")
|
||||||
|
}
|
||||||
|
log.Printf("✓Supported operations: ping, dns, tcp, http, ssl")
|
||||||
|
|
||||||
|
|
||||||
// Setup graceful shutdown
|
// Setup graceful shutdown
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
@@ -86,18 +131,37 @@ func main() {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-c
|
<-c
|
||||||
log.Println("Shutting down monitoring services...")
|
log.Println("🛑 === GRACEFUL SHUTDOWN INITIATED ===")
|
||||||
|
log.Println("🛑 Shutting down monitoring services...")
|
||||||
|
|
||||||
if monitoringService != nil {
|
if monitoringService != nil {
|
||||||
|
log.Println("🛑 Stopping service monitoring...")
|
||||||
monitoringService.Stop()
|
monitoringService.Stop()
|
||||||
}
|
}
|
||||||
if sslMonitoringService != nil {
|
if sslMonitoringService != nil {
|
||||||
|
log.Println("🛑 Stopping SSL monitoring...")
|
||||||
sslMonitoringService.Stop()
|
sslMonitoringService.Stop()
|
||||||
}
|
}
|
||||||
log.Println("Service stopped")
|
if sslNotificationService != nil {
|
||||||
|
log.Println("🛑 Stopping SSL notification monitoring...")
|
||||||
|
sslNotificationService.Stop()
|
||||||
|
}
|
||||||
|
if serverMonitoringService != nil {
|
||||||
|
log.Println("🛑 Stopping server monitoring...")
|
||||||
|
serverMonitoringService.Stop()
|
||||||
|
}
|
||||||
|
if uptimeMonitoringService != nil {
|
||||||
|
log.Println("🛑 Stopping uptime monitoring...")
|
||||||
|
uptimeMonitoringService.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("✅ All services stopped gracefully")
|
||||||
|
log.Println("🛑 === SERVICE OPERATION SERVER STOPPED ===")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
//log.Println("🌐 HTTP server starting...")
|
||||||
if err := http.ListenAndServe(":"+cfg.Port, router); err != nil {
|
if err := http.ListenAndServe(":"+cfg.Port, router); err != nil {
|
||||||
log.Fatal("Failed to start server:", err)
|
log.Fatal("❌ FATAL: Failed to start HTTP server:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package monitoring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"service-operation/notification"
|
||||||
|
"service-operation/pocketbase"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotificationMonitor handles notification logic for the monitoring service
|
||||||
|
type NotificationMonitor struct {
|
||||||
|
notifier *notification.ServiceNotifier
|
||||||
|
lastStatuses map[string]string // Track last known status for each service
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNotificationMonitor creates a new notification monitor
|
||||||
|
func NewNotificationMonitor(pbClient *pocketbase.PocketBaseClient) *NotificationMonitor {
|
||||||
|
return &NotificationMonitor{
|
||||||
|
notifier: notification.NewServiceNotifier(pbClient),
|
||||||
|
lastStatuses: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAndNotify checks if notification should be sent and sends it
|
||||||
|
func (nm *NotificationMonitor) CheckAndNotify(service pocketbase.Service, currentStatus string) {
|
||||||
|
// Skip if no notification configured
|
||||||
|
if service.NotificationID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get last known status
|
||||||
|
lastStatus, exists := nm.lastStatuses[service.ID]
|
||||||
|
|
||||||
|
// Send notification if:
|
||||||
|
// 1. This is the first check (no previous status)
|
||||||
|
// 2. Status has changed
|
||||||
|
// 3. Service is currently down (always notify for down status)
|
||||||
|
shouldNotify := !exists || lastStatus != currentStatus || currentStatus == "down"
|
||||||
|
|
||||||
|
if shouldNotify {
|
||||||
|
log.Printf("Sending notification for service %s: %s -> %s", service.Name, lastStatus, currentStatus)
|
||||||
|
|
||||||
|
// Send notification using custom method
|
||||||
|
if err := nm.notifier.NotifyCustom(
|
||||||
|
service.NotificationID,
|
||||||
|
service.TemplateID,
|
||||||
|
service.Name,
|
||||||
|
currentStatus,
|
||||||
|
nm.formatStatusMessage(service, currentStatus),
|
||||||
|
); err != nil {
|
||||||
|
log.Printf("Failed to send notification: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last known status
|
||||||
|
nm.lastStatuses[service.ID] = currentStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatStatusMessage creates a formatted message for the status change
|
||||||
|
func (nm *NotificationMonitor) formatStatusMessage(service pocketbase.Service, status string) string {
|
||||||
|
switch status {
|
||||||
|
case "up":
|
||||||
|
return "Service is now operational"
|
||||||
|
case "down":
|
||||||
|
return "Service is currently unavailable"
|
||||||
|
case "warning":
|
||||||
|
return "Service is experiencing issues"
|
||||||
|
default:
|
||||||
|
return "Service status has changed"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ func (rm *RegionalMonitor) Start() {
|
|||||||
rm.updateConnectionStatus("online")
|
rm.updateConnectionStatus("online")
|
||||||
rm.isOnline = true
|
rm.isOnline = true
|
||||||
|
|
||||||
log.Printf("Regional monitor started for region: %s (Agent ID: %s)",
|
log.Printf("✓Default Regional monitor started for region: %s (Agent ID: %s)",
|
||||||
service.RegionName, service.AgentID)
|
service.RegionName, service.AgentID)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func (ms *MonitoringService) Start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ms.isRunning = true
|
ms.isRunning = true
|
||||||
log.Println("Starting monitoring service...")
|
//log.Println("Starting monitoring service...")
|
||||||
|
|
||||||
// Start regional monitoring
|
// Start regional monitoring
|
||||||
ms.regionalMonitor.Start()
|
ms.regionalMonitor.Start()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
package monitoring
|
package monitoring
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service-operation/operations"
|
"service-operation/operations"
|
||||||
@@ -30,14 +30,14 @@ func (s *SSLMonitoringService) Start() {
|
|||||||
ticker := time.NewTicker(1 * time.Minute) // Check every minute for scheduling
|
ticker := time.NewTicker(1 * time.Minute) // Check every minute for scheduling
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
log.Println("SSL monitoring service started with interval and check_at scheduling")
|
// log.Println("SSL monitoring service started with interval and check_at scheduling")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
s.checkSSLCertificates()
|
s.checkSSLCertificates()
|
||||||
case <-s.stopChan:
|
case <-s.stopChan:
|
||||||
log.Println("SSL monitoring service stopped")
|
// log.Println("SSL monitoring service stopped")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,21 +48,24 @@ func (s *SSLMonitoringService) Stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSLMonitoringService) checkSSLCertificates() {
|
func (s *SSLMonitoringService) checkSSLCertificates() {
|
||||||
//log.Println("Fetching SSL certificates from PocketBase...")
|
// log.Println("Fetching SSL certificates from PocketBase...")
|
||||||
|
|
||||||
certificates, err := s.pbClient.GetSSLCertificates()
|
certificates, err := s.pbClient.GetSSLCertificates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to fetch SSL certificates: %v", err)
|
// log.Printf("Failed to fetch SSL certificates: %v", err)
|
||||||
|
_ = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//log.Printf("Found %d SSL certificates to check", len(certificates))
|
// log.Printf("Found %d SSL certificates to check", len(certificates))
|
||||||
|
|
||||||
for _, cert := range certificates {
|
for _, cert := range certificates {
|
||||||
if s.shouldCheckCertificate(cert) {
|
if s.shouldCheckCertificate(cert) {
|
||||||
go s.checkSingleCertificateWithRetry(cert)
|
go s.checkSingleCertificateWithRetry(cert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = certificates
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSLMonitoringService) shouldCheckCertificate(cert types.SSLCertificate) bool {
|
func (s *SSLMonitoringService) shouldCheckCertificate(cert types.SSLCertificate) bool {
|
||||||
@@ -72,29 +75,33 @@ func (s *SSLMonitoringService) shouldCheckCertificate(cert types.SSLCertificate)
|
|||||||
if cert.CheckAt != "" {
|
if cert.CheckAt != "" {
|
||||||
if checkAt, err := s.parseFlexibleTime(cert.CheckAt); err == nil {
|
if checkAt, err := s.parseFlexibleTime(cert.CheckAt); err == nil {
|
||||||
if now.After(checkAt) || now.Equal(checkAt) {
|
if now.After(checkAt) || now.Equal(checkAt) {
|
||||||
log.Printf("Certificate %s is due for manual check (check_at: %s)",
|
// log.Printf("Certificate %s is due for manual check (check_at: %s)",
|
||||||
cert.Domain, checkAt.Format("2006-01-02 15:04:05"))
|
// cert.Domain, checkAt.Format("2006-01-02 15:04:05"))
|
||||||
|
_ = checkAt
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
//log.Printf("Certificate %s scheduled for later check (check_at: %s)",
|
// log.Printf("Certificate %s scheduled for later check (check_at: %s)",
|
||||||
// cert.Domain, checkAt.Format("2006-01-02 15:04:05"))
|
// cert.Domain, checkAt.Format("2006-01-02 15:04:05"))
|
||||||
//return false
|
_ = checkAt
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Error parsing check_at for %s: %v", cert.Domain, err)
|
// log.Printf("Error parsing check_at for %s: %v", cert.Domain, err)
|
||||||
|
_ = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 2: Check based on check_interval (in days) from last update
|
// Priority 2: Check based on check_interval (in days) from last update
|
||||||
if cert.Updated == "" {
|
if cert.Updated == "" {
|
||||||
log.Printf("Certificate %s has never been checked, scheduling check", cert.Domain)
|
// log.Printf("Certificate %s has never been checked, scheduling check", cert.Domain)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse last check time from updated field
|
// Parse last check time from updated field
|
||||||
lastCheck, err := s.parseFlexibleTime(cert.Updated)
|
lastCheck, err := s.parseFlexibleTime(cert.Updated)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error parsing last check time for %s, scheduling check: %v", cert.Domain, err)
|
// log.Printf("Error parsing last check time for %s, scheduling check: %v", cert.Domain, err)
|
||||||
|
_ = err
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,11 +119,15 @@ func (s *SSLMonitoringService) shouldCheckCertificate(cert types.SSLCertificate)
|
|||||||
shouldCheck := now.After(nextCheck)
|
shouldCheck := now.After(nextCheck)
|
||||||
|
|
||||||
if shouldCheck {
|
if shouldCheck {
|
||||||
log.Printf("Certificate %s is due for interval check (last: %s, interval: %d days)",
|
// log.Printf("Certificate %s is due for interval check (last: %s, interval: %d days)",
|
||||||
cert.Domain, lastCheck.Format("2006-01-02 15:04:05"), adjustedIntervalDays)
|
// cert.Domain, lastCheck.Format("2006-01-02 15:04:05"), adjustedIntervalDays)
|
||||||
|
_ = lastCheck
|
||||||
|
_ = adjustedIntervalDays
|
||||||
} else {
|
} else {
|
||||||
//log.Printf("Certificate %s not due yet (next check: %s, interval: %d days)",
|
// log.Printf("Certificate %s not due yet (next check: %s, interval: %d days)",
|
||||||
//cert.Domain, nextCheck.Format("2006-01-02 15:04:05"), adjustedIntervalDays)
|
// cert.Domain, nextCheck.Format("2006-01-02 15:04:05"), adjustedIntervalDays)
|
||||||
|
_ = nextCheck
|
||||||
|
_ = adjustedIntervalDays
|
||||||
}
|
}
|
||||||
|
|
||||||
return shouldCheck
|
return shouldCheck
|
||||||
@@ -171,16 +182,16 @@ func (s *SSLMonitoringService) adjustCheckIntervalDays(cert types.SSLCertificate
|
|||||||
func (s *SSLMonitoringService) checkSingleCertificateWithRetry(cert types.SSLCertificate) {
|
func (s *SSLMonitoringService) checkSingleCertificateWithRetry(cert types.SSLCertificate) {
|
||||||
retryCount := s.retryQueue[cert.ID]
|
retryCount := s.retryQueue[cert.ID]
|
||||||
|
|
||||||
log.Printf("🔍 Checking SSL certificate for domain: %s (attempt %d/%d)",
|
// log.Printf("🔍 Checking SSL certificate for domain: %s (attempt %d/%d)",
|
||||||
cert.Domain, retryCount+1, s.maxRetries+1)
|
// cert.Domain, retryCount+1, s.maxRetries+1)
|
||||||
|
|
||||||
result, err := s.performSSLCheck(cert.Domain)
|
result, err := s.performSSLCheck(cert.Domain)
|
||||||
|
|
||||||
if err != nil && retryCount < s.maxRetries {
|
if err != nil && retryCount < s.maxRetries {
|
||||||
// Increment retry count and schedule retry
|
// Increment retry count and schedule retry
|
||||||
s.retryQueue[cert.ID] = retryCount + 1
|
s.retryQueue[cert.ID] = retryCount + 1
|
||||||
log.Printf("SSL check failed for %s, will retry (%d/%d): %v",
|
// log.Printf("SSL check failed for %s, will retry (%d/%d): %v",
|
||||||
cert.Domain, retryCount+1, s.maxRetries, err)
|
// cert.Domain, retryCount+1, s.maxRetries, err)
|
||||||
|
|
||||||
// Schedule retry with exponential backoff
|
// Schedule retry with exponential backoff
|
||||||
go func() {
|
go func() {
|
||||||
@@ -188,6 +199,9 @@ func (s *SSLMonitoringService) checkSingleCertificateWithRetry(cert types.SSLCer
|
|||||||
time.Sleep(backoffDuration)
|
time.Sleep(backoffDuration)
|
||||||
s.checkSingleCertificateWithRetry(cert)
|
s.checkSingleCertificateWithRetry(cert)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
_ = retryCount
|
||||||
|
_ = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,8 +209,8 @@ func (s *SSLMonitoringService) checkSingleCertificateWithRetry(cert types.SSLCer
|
|||||||
delete(s.retryQueue, cert.ID)
|
delete(s.retryQueue, cert.ID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ SSL check failed for domain %s after %d attempts: %v",
|
// log.Printf("❌ SSL check failed for domain %s after %d attempts: %v",
|
||||||
cert.Domain, s.maxRetries+1, err)
|
// cert.Domain, s.maxRetries+1, err)
|
||||||
s.updateCertificateWithError(cert, err)
|
s.updateCertificateWithError(cert, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -206,28 +220,31 @@ func (s *SSLMonitoringService) checkSingleCertificateWithRetry(cert types.SSLCer
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSLMonitoringService) performSSLCheck(domain string) (*types.OperationResult, error) {
|
func (s *SSLMonitoringService) performSSLCheck(domain string) (*types.OperationResult, error) {
|
||||||
log.Printf("Performing SSL check for domain: %s", domain)
|
// log.Printf("Performing SSL check for domain: %s", domain)
|
||||||
sslOp := operations.NewSSLOperation(30 * time.Second)
|
sslOp := operations.NewSSLOperation(30 * time.Second)
|
||||||
result, err := sslOp.Execute(domain)
|
result, err := sslOp.Execute(domain)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("SSL operation failed for %s: %v", domain, err)
|
// log.Printf("SSL operation failed for %s: %v", domain, err)
|
||||||
|
_ = err
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if result == nil {
|
if result == nil {
|
||||||
log.Printf("SSL operation returned nil result for %s", domain)
|
// log.Printf("SSL operation returned nil result for %s", domain)
|
||||||
return nil, fmt.Errorf("SSL check returned nil result")
|
return nil, fmt.Errorf("SSL check returned nil result")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("SSL check completed for %s: success=%v, days_left=%d",
|
// log.Printf("SSL check completed for %s: success=%v, days_left=%d",
|
||||||
domain, result.Success, result.SSLDaysLeft)
|
// domain, result.Success, result.SSLDaysLeft)
|
||||||
|
|
||||||
|
_ = domain
|
||||||
|
_ = result
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSLMonitoringService) updateCertificateWithError(cert types.SSLCertificate, err error) {
|
func (s *SSLMonitoringService) updateCertificateWithError(cert types.SSLCertificate, err error) {
|
||||||
log.Printf("Updating certificate %s with error status", cert.Domain)
|
// log.Printf("Updating certificate %s with error status", cert.Domain)
|
||||||
|
|
||||||
updateData := map[string]interface{}{
|
updateData := map[string]interface{}{
|
||||||
"status": "error",
|
"status": "error",
|
||||||
@@ -250,18 +267,24 @@ func (s *SSLMonitoringService) updateCertificateWithError(cert types.SSLCertific
|
|||||||
updateData["check_at"] = nextCheck.Format(time.RFC3339)
|
updateData["check_at"] = nextCheck.Format(time.RFC3339)
|
||||||
|
|
||||||
if updateErr := s.pbClient.UpdateSSLCertificate(cert.ID, updateData); updateErr != nil {
|
if updateErr := s.pbClient.UpdateSSLCertificate(cert.ID, updateData); updateErr != nil {
|
||||||
log.Printf("Failed to update SSL certificate %s with error status: %v", cert.ID, updateErr)
|
// log.Printf("Failed to update SSL certificate %s with error status: %v", cert.ID, updateErr)
|
||||||
|
_ = updateErr
|
||||||
} else {
|
} else {
|
||||||
log.Printf("📝 Updated certificate %s with error status (next check in %d days)",
|
// log.Printf("📝 Updated certificate %s with error status (next check in %d days)",
|
||||||
cert.Domain, errorIntervalDays)
|
// cert.Domain, errorIntervalDays)
|
||||||
|
_ = errorIntervalDays
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = err
|
||||||
|
_ = updateData
|
||||||
|
_ = nextCheck
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSLMonitoringService) updateCertificateWithResults(cert types.SSLCertificate, result *types.OperationResult) {
|
func (s *SSLMonitoringService) updateCertificateWithResults(cert types.SSLCertificate, result *types.OperationResult) {
|
||||||
status := getSSLStatus(result)
|
status := getSSLStatus(result)
|
||||||
|
|
||||||
log.Printf("Updating certificate %s with results: status=%s, days_left=%d, issuer=%s",
|
// log.Printf("Updating certificate %s with results: status=%s, days_left=%d, issuer=%s",
|
||||||
cert.Domain, status, result.SSLDaysLeft, result.SSLIssuer)
|
// cert.Domain, status, result.SSLDaysLeft, result.SSLIssuer)
|
||||||
|
|
||||||
updateData := map[string]interface{}{
|
updateData := map[string]interface{}{
|
||||||
"status": status,
|
"status": status,
|
||||||
@@ -290,11 +313,18 @@ func (s *SSLMonitoringService) updateCertificateWithResults(cert types.SSLCertif
|
|||||||
updateData["check_at"] = nextCheck.Format(time.RFC3339)
|
updateData["check_at"] = nextCheck.Format(time.RFC3339)
|
||||||
|
|
||||||
if err := s.pbClient.UpdateSSLCertificate(cert.ID, updateData); err != nil {
|
if err := s.pbClient.UpdateSSLCertificate(cert.ID, updateData); err != nil {
|
||||||
log.Printf("Failed to update SSL certificate %s: %v", cert.ID, err)
|
// log.Printf("Failed to update SSL certificate %s: %v", cert.ID, err)
|
||||||
|
_ = err
|
||||||
} else {
|
} else {
|
||||||
log.Printf("✅ SSL certificate updated for %s: %s (%d days left, issuer: %s, next check in %d days)",
|
// log.Printf("✅ SSL certificate updated for %s: %s (%d days left, issuer: %s, next check in %d days)",
|
||||||
cert.Domain, status, result.SSLDaysLeft, result.SSLIssuer, adjustedIntervalDays)
|
// cert.Domain, status, result.SSLDaysLeft, result.SSLIssuer, adjustedIntervalDays)
|
||||||
|
_ = status
|
||||||
|
_ = result
|
||||||
|
_ = adjustedIntervalDays
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = updateData
|
||||||
|
_ = nextCheck
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSSLStatus(result *types.OperationResult) string {
|
func getSSLStatus(result *types.OperationResult) string {
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DiscordService handles Discord notifications
|
||||||
|
type DiscordService struct{}
|
||||||
|
|
||||||
|
// NewDiscordService creates a new Discord notification service
|
||||||
|
func NewDiscordService() *DiscordService {
|
||||||
|
return &DiscordService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiscordPayload represents the payload for Discord webhook
|
||||||
|
type DiscordPayload struct {
|
||||||
|
Content string `json:"content,omitempty"`
|
||||||
|
Embeds []DiscordEmbed `json:"embeds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiscordEmbed represents a Discord embed
|
||||||
|
type DiscordEmbed struct {
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Color int `json:"color,omitempty"`
|
||||||
|
Timestamp string `json:"timestamp,omitempty"`
|
||||||
|
Fields []DiscordEmbedField `json:"fields,omitempty"`
|
||||||
|
Footer *DiscordEmbedFooter `json:"footer,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiscordEmbedField represents a field in Discord embed
|
||||||
|
type DiscordEmbedField struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Inline bool `json:"inline,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiscordEmbedFooter represents footer in Discord embed
|
||||||
|
type DiscordEmbedFooter struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendNotification sends a notification via Discord webhook
|
||||||
|
func (ds *DiscordService) SendNotification(config *AlertConfiguration, message string) error {
|
||||||
|
if config.DiscordWebhookURL == "" {
|
||||||
|
return fmt.Errorf("discord webhook URL is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create rich embed for better formatting
|
||||||
|
embed := ds.createDiscordEmbed(message)
|
||||||
|
|
||||||
|
payload := DiscordPayload{
|
||||||
|
Embeds: []DiscordEmbed{embed},
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal Discord payload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Post(config.DiscordWebhookURL, "application/json", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("discord webhook request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("discord webhook error, status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createDiscordEmbed creates a formatted Discord embed from the message
|
||||||
|
func (ds *DiscordService) createDiscordEmbed(message string) DiscordEmbed {
|
||||||
|
// Parse the message to extract structured information
|
||||||
|
lines := strings.Split(message, "\n")
|
||||||
|
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return DiscordEmbed{
|
||||||
|
Description: message,
|
||||||
|
Color: ds.getDefaultColor(),
|
||||||
|
Timestamp: time.Now().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract title from first line and determine status
|
||||||
|
title := strings.TrimSpace(lines[0])
|
||||||
|
color := ds.determineColorFromMessage(title)
|
||||||
|
|
||||||
|
// Create fields from the remaining lines
|
||||||
|
var fields []DiscordEmbedField
|
||||||
|
|
||||||
|
for i, line := range lines {
|
||||||
|
if i == 0 {
|
||||||
|
continue // Skip title line
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse field from "• Key: Value" format
|
||||||
|
if strings.HasPrefix(line, "•") || strings.HasPrefix(line, "-") {
|
||||||
|
line = strings.TrimPrefix(line, "•")
|
||||||
|
line = strings.TrimPrefix(line, "-")
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
fieldName := strings.TrimSpace(parts[0])
|
||||||
|
fieldValue := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
if fieldName != "" && fieldValue != "" {
|
||||||
|
fields = append(fields, DiscordEmbedField{
|
||||||
|
Name: fieldName,
|
||||||
|
Value: fieldValue,
|
||||||
|
Inline: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If not in key:value format, add as description field
|
||||||
|
fields = append(fields, DiscordEmbedField{
|
||||||
|
Name: "Details",
|
||||||
|
Value: line,
|
||||||
|
Inline: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no structured fields found, use the entire message as description
|
||||||
|
if len(fields) == 0 {
|
||||||
|
// Remove the first line from description since it's used as title
|
||||||
|
description := ""
|
||||||
|
if len(lines) > 1 {
|
||||||
|
description = strings.Join(lines[1:], "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return DiscordEmbed{
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
Color: color,
|
||||||
|
Timestamp: time.Now().Format(time.RFC3339),
|
||||||
|
Footer: &DiscordEmbedFooter{
|
||||||
|
Text: "CheckCle System Alert",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DiscordEmbed{
|
||||||
|
Title: title,
|
||||||
|
Color: color,
|
||||||
|
Timestamp: time.Now().Format(time.RFC3339),
|
||||||
|
Fields: fields,
|
||||||
|
Footer: &DiscordEmbedFooter{
|
||||||
|
Text: "CheckCle System Alert",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// determineColorFromMessage determines Discord embed color based on message content
|
||||||
|
func (ds *DiscordService) determineColorFromMessage(message string) int {
|
||||||
|
messageLower := strings.ToLower(message)
|
||||||
|
|
||||||
|
// Red for critical/error states
|
||||||
|
if strings.Contains(messageLower, "expired") ||
|
||||||
|
strings.Contains(messageLower, "down") ||
|
||||||
|
strings.Contains(messageLower, "failed") ||
|
||||||
|
strings.Contains(messageLower, "error") ||
|
||||||
|
strings.Contains(messageLower, "critical") ||
|
||||||
|
strings.Contains(messageLower, "🚨") ||
|
||||||
|
strings.Contains(messageLower, "🔴") {
|
||||||
|
return 15158332 // Red (#E74C3C)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orange for warnings
|
||||||
|
if strings.Contains(messageLower, "expiring_soon") ||
|
||||||
|
strings.Contains(messageLower, "expiring soon") ||
|
||||||
|
strings.Contains(messageLower, "warning") ||
|
||||||
|
strings.Contains(messageLower, "maintenance") ||
|
||||||
|
strings.Contains(messageLower, "paused") ||
|
||||||
|
strings.Contains(messageLower, "⚠️") ||
|
||||||
|
strings.Contains(messageLower, "🟡") ||
|
||||||
|
strings.Contains(messageLower, "🟠") {
|
||||||
|
return 15105570 // Orange (#E67E22)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Green for success/up states
|
||||||
|
if strings.Contains(messageLower, "up") ||
|
||||||
|
strings.Contains(messageLower, "resolved") ||
|
||||||
|
strings.Contains(messageLower, "success") ||
|
||||||
|
strings.Contains(messageLower, "restored") ||
|
||||||
|
strings.Contains(messageLower, "valid") ||
|
||||||
|
strings.Contains(messageLower, "🟢") ||
|
||||||
|
strings.Contains(messageLower, "✅") {
|
||||||
|
return 3066993 // Green (#2ECC71)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blue for info/default
|
||||||
|
return 3447003 // Blue (#3498DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultColor returns the default Discord embed color (blue)
|
||||||
|
func (ds *DiscordService) getDefaultColor() int {
|
||||||
|
return 3447003 // Blue (#3498DB)
|
||||||
|
}
|
||||||
@@ -0,0 +1,532 @@
|
|||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/smtp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmailService handles email notifications
|
||||||
|
type EmailService struct{}
|
||||||
|
|
||||||
|
// NewEmailService creates a new email notification service
|
||||||
|
func NewEmailService() *EmailService {
|
||||||
|
// log.Printf("✅ Email notification service initialized")
|
||||||
|
return &EmailService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendNotification sends a notification via email
|
||||||
|
func (es *EmailService) SendNotification(config *AlertConfiguration, message string) error {
|
||||||
|
// log.Printf("📧 === SENDING EMAIL NOTIFICATION ===")
|
||||||
|
// log.Printf("🔔 Email notification request received")
|
||||||
|
// log.Printf("📊 Email Configuration:")
|
||||||
|
// log.Printf(" - Email Address: %s", config.EmailAddress)
|
||||||
|
// log.Printf(" - Sender Name: %s", config.EmailSenderName)
|
||||||
|
// log.Printf(" - SMTP Server: %s", config.SMTPServer)
|
||||||
|
// log.Printf(" - SMTP Port: %s", config.SMTPPort)
|
||||||
|
// log.Printf(" - SMTP Password present: %t", config.SMTPPassword != "")
|
||||||
|
// log.Printf(" - Message Length: %d chars", len(message))
|
||||||
|
|
||||||
|
// Validate email configuration
|
||||||
|
if config.EmailAddress == "" || config.SMTPServer == "" || config.SMTPPort == "" {
|
||||||
|
// log.Printf("❌ EMAIL CONFIGURATION ERROR: Missing required fields")
|
||||||
|
// log.Printf(" - Email Address present: %t", config.EmailAddress != "")
|
||||||
|
// log.Printf(" - SMTP Server present: %t", config.SMTPServer != "")
|
||||||
|
// log.Printf(" - SMTP Port present: %t", config.SMTPPort != "")
|
||||||
|
return fmt.Errorf("email configuration is incomplete")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.SMTPPassword == "" {
|
||||||
|
// log.Printf("⚠️ WARNING: SMTP password not provided - authentication may fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := strconv.Atoi(config.SMTPPort)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ SMTP PORT ERROR: Invalid port '%s': %v", config.SMTPPort, err)
|
||||||
|
return fmt.Errorf("invalid SMTP port: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Printf("✅ Email configuration validation passed")
|
||||||
|
// log.Printf("🔧 Parsed SMTP Port: %d", port)
|
||||||
|
|
||||||
|
// Determine alert severity and get appropriate emoji/color
|
||||||
|
severity, emoji := es.parseMessageSeverity(message)
|
||||||
|
// log.Printf("📋 Message Analysis:")
|
||||||
|
// log.Printf(" - Detected Severity: %s", severity)
|
||||||
|
// log.Printf(" - Emoji: %s", emoji)
|
||||||
|
|
||||||
|
// Create enhanced email content
|
||||||
|
subject := es.createEmailSubject(config.EmailSenderName, severity)
|
||||||
|
htmlBody := es.createHTMLEmailBody(message, severity, emoji)
|
||||||
|
plainBody := es.createPlainEmailBody(message, severity, emoji)
|
||||||
|
|
||||||
|
// log.Printf("📝 Email Content Created:")
|
||||||
|
// log.Printf(" - Subject: %s", subject)
|
||||||
|
// log.Printf(" - HTML Body Length: %d chars", len(htmlBody))
|
||||||
|
// log.Printf(" - Plain Body Length: %d chars", len(plainBody))
|
||||||
|
|
||||||
|
// Prepare email message with both HTML and plain text
|
||||||
|
emailMessage := es.createMIMEMessage(config.EmailSenderName, config.EmailAddress, subject, htmlBody, plainBody)
|
||||||
|
|
||||||
|
// log.Printf("📤 Preparing to send email...")
|
||||||
|
// log.Printf(" - SMTP Server: %s:%d", config.SMTPServer, port)
|
||||||
|
// log.Printf(" - From: %s", config.EmailSenderName)
|
||||||
|
// log.Printf(" - To: %s", config.EmailAddress)
|
||||||
|
|
||||||
|
// Send email with enhanced SMTP handling
|
||||||
|
err = es.sendSMTPEmail(config.SMTPServer, port, config.EmailSenderName, config.EmailAddress, config.SMTPPassword, emailMessage)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ EMAIL SENDING FAILED: %v", err)
|
||||||
|
// log.Printf("❌ Error Details:")
|
||||||
|
// log.Printf(" - SMTP Server: %s:%d", config.SMTPServer, port)
|
||||||
|
// log.Printf(" - Error Type: %T", err)
|
||||||
|
// log.Printf(" - Error Message: %s", err.Error())
|
||||||
|
return fmt.Errorf("failed to send email: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Printf("✅ EMAIL SENT SUCCESSFULLY")
|
||||||
|
// log.Printf("✅ Email Details:")
|
||||||
|
// log.Printf(" - Recipient: %s", config.EmailAddress)
|
||||||
|
// log.Printf(" - Subject: %s", subject)
|
||||||
|
// log.Printf(" - Severity: %s", severity)
|
||||||
|
// log.Printf(" - Timestamp: %s", time.Now().Format("2006-01-02 15:04:05"))
|
||||||
|
// log.Printf("=== EMAIL NOTIFICATION COMPLETE ===")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMessageSeverity analyzes the message to determine severity level
|
||||||
|
func (es *EmailService) parseMessageSeverity(message string) (string, string) {
|
||||||
|
messageUpper := strings.ToUpper(message)
|
||||||
|
|
||||||
|
// Check for critical indicators
|
||||||
|
if strings.Contains(messageUpper, "🚨") || strings.Contains(messageUpper, "CRITICAL") ||
|
||||||
|
strings.Contains(messageUpper, "DOWN") || strings.Contains(messageUpper, "ERROR") ||
|
||||||
|
strings.Contains(messageUpper, "FAILED") || strings.Contains(messageUpper, "🔴") {
|
||||||
|
return "CRITICAL", "🚨"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for warning indicators
|
||||||
|
if strings.Contains(messageUpper, "⚠️") || strings.Contains(messageUpper, "WARNING") ||
|
||||||
|
strings.Contains(messageUpper, "🟡") || strings.Contains(messageUpper, "INCIDENT") {
|
||||||
|
return "WARNING", "⚠️"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for success indicators
|
||||||
|
if strings.Contains(messageUpper, "🟢") || strings.Contains(messageUpper, "UP") ||
|
||||||
|
strings.Contains(messageUpper, "SUCCESS") || strings.Contains(messageUpper, "RESOLVED") ||
|
||||||
|
strings.Contains(messageUpper, "RESTORED") {
|
||||||
|
return "SUCCESS", "✅"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for maintenance indicators
|
||||||
|
if strings.Contains(messageUpper, "🟠") || strings.Contains(messageUpper, "MAINTENANCE") ||
|
||||||
|
strings.Contains(messageUpper, "PAUSED") {
|
||||||
|
return "MAINTENANCE", "🔧"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to info
|
||||||
|
return "INFO", "ℹ️"
|
||||||
|
}
|
||||||
|
|
||||||
|
// createEmailSubject creates an appropriate email subject
|
||||||
|
func (es *EmailService) createEmailSubject(senderName, severity string) string {
|
||||||
|
prefix := "Service Alert"
|
||||||
|
if senderName != "" {
|
||||||
|
prefix = fmt.Sprintf("%s - Service Alert", senderName)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch severity {
|
||||||
|
case "CRITICAL":
|
||||||
|
return fmt.Sprintf("🚨 [CRITICAL] %s", prefix)
|
||||||
|
case "WARNING":
|
||||||
|
return fmt.Sprintf("⚠️ [WARNING] %s", prefix)
|
||||||
|
case "SUCCESS":
|
||||||
|
return fmt.Sprintf("✅ [RESOLVED] %s", prefix)
|
||||||
|
case "MAINTENANCE":
|
||||||
|
return fmt.Sprintf("🔧 [MAINTENANCE] %s", prefix)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("ℹ️ [INFO] %s", prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createHTMLEmailBody creates a rich HTML email body
|
||||||
|
func (es *EmailService) createHTMLEmailBody(message, severity, emoji string) string {
|
||||||
|
// Get color scheme based on severity
|
||||||
|
var bgColor, borderColor, textColor string
|
||||||
|
switch severity {
|
||||||
|
case "CRITICAL":
|
||||||
|
bgColor = "#fee2e2"
|
||||||
|
borderColor = "#dc2626"
|
||||||
|
textColor = "#991b1b"
|
||||||
|
case "WARNING":
|
||||||
|
bgColor = "#fef3c7"
|
||||||
|
borderColor = "#d97706"
|
||||||
|
textColor = "#92400e"
|
||||||
|
case "SUCCESS":
|
||||||
|
bgColor = "#dcfce7"
|
||||||
|
borderColor = "#16a34a"
|
||||||
|
textColor = "#15803d"
|
||||||
|
case "MAINTENANCE":
|
||||||
|
bgColor = "#fed7aa"
|
||||||
|
borderColor = "#ea580c"
|
||||||
|
textColor = "#c2410c"
|
||||||
|
default:
|
||||||
|
bgColor = "#dbeafe"
|
||||||
|
borderColor = "#2563eb"
|
||||||
|
textColor = "#1d4ed8"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format message content for HTML
|
||||||
|
htmlMessage := strings.ReplaceAll(message, "\n", "<br>")
|
||||||
|
|
||||||
|
// Enhance bullet points and formatting
|
||||||
|
htmlMessage = strings.ReplaceAll(htmlMessage, " - ", "<br> • ")
|
||||||
|
htmlMessage = strings.ReplaceAll(htmlMessage, "Service:", "<strong>Service:</strong>")
|
||||||
|
htmlMessage = strings.ReplaceAll(htmlMessage, "Status:", "<strong>Status:</strong>")
|
||||||
|
htmlMessage = strings.ReplaceAll(htmlMessage, "Host:", "<strong>Host:</strong>")
|
||||||
|
htmlMessage = strings.ReplaceAll(htmlMessage, "Type:", "<strong>Type:</strong>")
|
||||||
|
htmlMessage = strings.ReplaceAll(htmlMessage, "Response time:", "<strong>Response time:</strong>")
|
||||||
|
htmlMessage = strings.ReplaceAll(htmlMessage, "Time:", "<strong>Time:</strong>")
|
||||||
|
htmlMessage = strings.ReplaceAll(htmlMessage, "Domain:", "<strong>Domain:</strong>")
|
||||||
|
htmlMessage = strings.ReplaceAll(htmlMessage, "Days Remaining:", "<strong>Days Remaining:</strong>")
|
||||||
|
htmlMessage = strings.ReplaceAll(htmlMessage, "Expiration Date:", "<strong>Expiration Date:</strong>")
|
||||||
|
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Service Alert Notification</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 20px; font-family: Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; background-color: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
||||||
|
<!-- Header -->
|
||||||
|
<div style="background-color: %s; color: %s; padding: 20px; border-radius: 8px 8px 0 0; border-left: 4px solid %s;">
|
||||||
|
<h2 style="margin: 0; font-size: 18px;">
|
||||||
|
%s Service Alert Notification
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<div style="background-color: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 3px solid %s;">
|
||||||
|
<p style="margin: 0; font-size: 14px; line-height: 1.6; color: #333;">
|
||||||
|
%s
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #e5e7eb;">
|
||||||
|
<p style="margin: 0; font-size: 12px; color: #6b7280; text-align: center;">
|
||||||
|
This is an automated notification from your monitoring system.<br>
|
||||||
|
Generated at: %s
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`,
|
||||||
|
bgColor, textColor, borderColor, emoji, borderColor, htmlMessage, time.Now().Format("2006-01-02 15:04:05 MST"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// createPlainEmailBody creates a plain text email body
|
||||||
|
func (es *EmailService) createPlainEmailBody(message, severity, emoji string) string {
|
||||||
|
separator := strings.Repeat("=", 50)
|
||||||
|
|
||||||
|
return fmt.Sprintf(`%s
|
||||||
|
%s SERVICE ALERT NOTIFICATION
|
||||||
|
%s
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
%s
|
||||||
|
This is an automated notification from your monitoring system.
|
||||||
|
Generated at: %s
|
||||||
|
%s`,
|
||||||
|
separator,
|
||||||
|
emoji+" "+severity,
|
||||||
|
separator,
|
||||||
|
message,
|
||||||
|
separator,
|
||||||
|
time.Now().Format("2006-01-02 15:04:05 MST"),
|
||||||
|
separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMIMEMessage creates a MIME message with both HTML and plain text
|
||||||
|
func (es *EmailService) createMIMEMessage(fromName, toEmail, subject, htmlBody, plainBody string) string {
|
||||||
|
boundary := fmt.Sprintf("boundary_%d", time.Now().Unix())
|
||||||
|
|
||||||
|
headers := fmt.Sprintf(`From: %s
|
||||||
|
To: %s
|
||||||
|
Subject: %s
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: multipart/alternative; boundary="%s"
|
||||||
|
|
||||||
|
`, fromName, toEmail, subject, boundary)
|
||||||
|
|
||||||
|
body := fmt.Sprintf(`--%s
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
--%s
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
--%s--
|
||||||
|
`, boundary, plainBody, boundary, htmlBody, boundary)
|
||||||
|
|
||||||
|
return headers + body
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendSMTPEmail sends the email using SMTP with proper authentication
|
||||||
|
func (es *EmailService) sendSMTPEmail(smtpServer string, port int, fromEmail, toEmail, password, message string) error {
|
||||||
|
addr := fmt.Sprintf("%s:%d", smtpServer, port)
|
||||||
|
|
||||||
|
// log.Printf("🔌 Connecting to SMTP server: %s", addr)
|
||||||
|
|
||||||
|
// Extract hostname from SMTP server for proper HELO
|
||||||
|
hostname := smtpServer
|
||||||
|
if strings.Contains(hostname, ".") {
|
||||||
|
// Use the SMTP server hostname for HELO
|
||||||
|
hostname = smtpServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Printf("🔧 Using hostname for HELO: %s", hostname)
|
||||||
|
|
||||||
|
// For port 587 (STARTTLS) - most common for authenticated SMTP
|
||||||
|
if port == 587 {
|
||||||
|
// log.Printf("🔐 Attempting STARTTLS connection with authentication...")
|
||||||
|
return es.sendWithSTARTTLSAuth(addr, hostname, fromEmail, toEmail, password, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For port 465 (SSL/TLS)
|
||||||
|
if port == 465 {
|
||||||
|
// log.Printf("🔒 Attempting SSL connection with authentication...")
|
||||||
|
return es.sendWithSSLAuth(addr, hostname, fromEmail, toEmail, password, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For port 25 (Plain SMTP with optional STARTTLS)
|
||||||
|
if port == 25 {
|
||||||
|
// log.Printf("📧 Attempting plain SMTP with optional STARTTLS...")
|
||||||
|
return es.sendWithSTARTTLSAuth(addr, hostname, fromEmail, toEmail, password, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to STARTTLS for any other port
|
||||||
|
// log.Printf("📧 Using STARTTLS with auth fallback for port %d...", port)
|
||||||
|
return es.sendWithSTARTTLSAuth(addr, hostname, fromEmail, toEmail, password, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendWithSTARTTLSAuth sends email with STARTTLS and authentication
|
||||||
|
func (es *EmailService) sendWithSTARTTLSAuth(addr, hostname, fromEmail, toEmail, password, message string) error {
|
||||||
|
// log.Printf("🔐 Establishing STARTTLS connection to %s", addr)
|
||||||
|
|
||||||
|
// Connect to server
|
||||||
|
client, err := smtp.Dial(addr)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to connect to SMTP server: %v", err)
|
||||||
|
return fmt.Errorf("failed to connect to SMTP server: %v", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Send EHLO with proper hostname
|
||||||
|
// log.Printf("👋 Sending EHLO with hostname: %s", hostname)
|
||||||
|
if err = client.Hello(hostname); err != nil {
|
||||||
|
// log.Printf("❌ EHLO failed: %v", err)
|
||||||
|
return fmt.Errorf("EHLO failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if STARTTLS is supported and use it
|
||||||
|
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||||
|
// log.Printf("🔐 STARTTLS supported, initiating TLS...")
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: strings.Split(addr, ":")[0],
|
||||||
|
InsecureSkipVerify: false,
|
||||||
|
}
|
||||||
|
if err = client.StartTLS(tlsConfig); err != nil {
|
||||||
|
// log.Printf("❌ STARTTLS failed: %v", err)
|
||||||
|
return fmt.Errorf("STARTTLS failed: %v", err)
|
||||||
|
}
|
||||||
|
// log.Printf("✅ TLS connection established")
|
||||||
|
} else {
|
||||||
|
// log.Printf("⚠️ STARTTLS not supported by server, continuing with plain connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for AUTH support and authenticate if available
|
||||||
|
if ok, mechanisms := client.Extension("AUTH"); ok {
|
||||||
|
// Suppress unused variable warning
|
||||||
|
_ = mechanisms
|
||||||
|
// log.Printf("🔑 AUTH extension supported with mechanisms: %s", mechanisms)
|
||||||
|
|
||||||
|
// Use the provided credentials for authentication
|
||||||
|
username := fromEmail
|
||||||
|
|
||||||
|
// log.Printf("🔐 Attempting authentication for user: %s", username)
|
||||||
|
// log.Printf("🔑 Password provided: %t", password != "")
|
||||||
|
|
||||||
|
if password != "" {
|
||||||
|
// Create auth mechanism - try PLAIN first as it's most common
|
||||||
|
auth := smtp.PlainAuth("", username, password, strings.Split(addr, ":")[0])
|
||||||
|
|
||||||
|
if err := client.Auth(auth); err != nil {
|
||||||
|
// log.Printf("❌ Authentication failed: %v", err)
|
||||||
|
return fmt.Errorf("SMTP authentication failed: %v", err)
|
||||||
|
} else {
|
||||||
|
// log.Printf("✅ Authentication successful")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// log.Printf("⚠️ No password provided, skipping authentication")
|
||||||
|
return fmt.Errorf("SMTP password is required for authentication")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// log.Printf("ℹ️ AUTH extension not available, proceeding without authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sender
|
||||||
|
// log.Printf("📤 Setting sender: %s", fromEmail)
|
||||||
|
if err = client.Mail(fromEmail); err != nil {
|
||||||
|
// log.Printf("❌ Failed to set sender: %v", err)
|
||||||
|
return fmt.Errorf("failed to set sender: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set recipient
|
||||||
|
// log.Printf("📥 Setting recipient: %s", toEmail)
|
||||||
|
if err = client.Rcpt(toEmail); err != nil {
|
||||||
|
// log.Printf("❌ Failed to set recipient: %v", err)
|
||||||
|
return fmt.Errorf("failed to set recipient: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
// log.Printf("📝 Sending message data...")
|
||||||
|
w, err := client.Data()
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to initiate data transfer: %v", err)
|
||||||
|
return fmt.Errorf("failed to initiate data transfer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write([]byte(message))
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to write message data: %v", err)
|
||||||
|
return fmt.Errorf("failed to write message data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to close data writer: %v", err)
|
||||||
|
return fmt.Errorf("failed to close data writer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit gracefully
|
||||||
|
err = client.Quit()
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("⚠️ Warning during QUIT: %v", err)
|
||||||
|
// Don't return error for QUIT issues as email might have been sent
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Printf("✅ Email sent successfully via STARTTLS")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendWithSSLAuth sends email with SSL/TLS and authentication (for port 465)
|
||||||
|
func (es *EmailService) sendWithSSLAuth(addr, hostname, fromEmail, toEmail, password, message string) error {
|
||||||
|
// log.Printf("🔒 Establishing SSL/TLS connection to %s", addr)
|
||||||
|
|
||||||
|
// Create TLS configuration
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: false,
|
||||||
|
ServerName: strings.Split(addr, ":")[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect with TLS
|
||||||
|
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to establish TLS connection: %v", err)
|
||||||
|
return fmt.Errorf("failed to establish TLS connection: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Create SMTP client
|
||||||
|
client, err := smtp.NewClient(conn, strings.Split(addr, ":")[0])
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to create SMTP client: %v", err)
|
||||||
|
return fmt.Errorf("failed to create SMTP client: %v", err)
|
||||||
|
}
|
||||||
|
defer client.Quit()
|
||||||
|
|
||||||
|
// Send EHLO with proper hostname
|
||||||
|
// log.Printf("👋 Sending EHLO with hostname: %s", hostname)
|
||||||
|
if err = client.Hello(hostname); err != nil {
|
||||||
|
// log.Printf("❌ EHLO failed: %v", err)
|
||||||
|
return fmt.Errorf("EHLO failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate if AUTH is supported and password is provided
|
||||||
|
if ok, mechanisms := client.Extension("AUTH"); ok {
|
||||||
|
// Suppress unused variable warning
|
||||||
|
_ = mechanisms
|
||||||
|
// log.Printf("🔑 AUTH extension supported with mechanisms: %s", mechanisms)
|
||||||
|
|
||||||
|
username := fromEmail
|
||||||
|
|
||||||
|
// log.Printf("🔐 Attempting authentication for user: %s", username)
|
||||||
|
// log.Printf("🔑 Password provided: %t", password != "")
|
||||||
|
|
||||||
|
if password != "" {
|
||||||
|
auth := smtp.PlainAuth("", username, password, strings.Split(addr, ":")[0])
|
||||||
|
|
||||||
|
if err := client.Auth(auth); err != nil {
|
||||||
|
// log.Printf("❌ Authentication failed: %v", err)
|
||||||
|
return fmt.Errorf("SMTP authentication failed: %v", err)
|
||||||
|
} else {
|
||||||
|
// log.Printf("✅ Authentication successful")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// log.Printf("⚠️ No password provided, skipping authentication")
|
||||||
|
return fmt.Errorf("SMTP password is required for authentication")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sender and recipient
|
||||||
|
// log.Printf("📤 Setting sender: %s", fromEmail)
|
||||||
|
if err := client.Mail(fromEmail); err != nil {
|
||||||
|
// log.Printf("❌ Failed to set sender: %v", err)
|
||||||
|
return fmt.Errorf("failed to set sender: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Printf("📥 Setting recipient: %s", toEmail)
|
||||||
|
if err := client.Rcpt(toEmail); err != nil {
|
||||||
|
// log.Printf("❌ Failed to set recipient: %v", err)
|
||||||
|
return fmt.Errorf("failed to set recipient: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
// log.Printf("📝 Sending message data...")
|
||||||
|
w, err := client.Data()
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to initiate data transfer: %v", err)
|
||||||
|
return fmt.Errorf("failed to initiate data transfer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write([]byte(message))
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to write message data: %v", err)
|
||||||
|
return fmt.Errorf("failed to write message data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to close data writer: %v", err)
|
||||||
|
return fmt.Errorf("failed to close data writer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Printf("✅ Email sent successfully via SSL/TLS")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GoogleChatService handles Google Chat notifications
|
||||||
|
type GoogleChatService struct{}
|
||||||
|
|
||||||
|
// NewGoogleChatService creates a new Google Chat notification service
|
||||||
|
func NewGoogleChatService() *GoogleChatService {
|
||||||
|
return &GoogleChatService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoogleChatPayload represents the payload for Google Chat webhook
|
||||||
|
type GoogleChatPayload struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendNotification sends a notification via Google Chat webhook
|
||||||
|
func (gcs *GoogleChatService) SendNotification(config *AlertConfiguration, message string) error {
|
||||||
|
// fmt.Printf("💬 [GOOGLE_CHAT] Attempting to send notification...\n")
|
||||||
|
// fmt.Printf("💬 [GOOGLE_CHAT] Config - Webhook URL present: %v\n", config.GoogleChatWebhookURL != "")
|
||||||
|
// fmt.Printf("💬 [GOOGLE_CHAT] Message: %s\n", message)
|
||||||
|
|
||||||
|
if config.GoogleChatWebhookURL == "" {
|
||||||
|
return fmt.Errorf("google chat webhook URL is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := GoogleChatPayload{
|
||||||
|
Text: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
// fmt.Printf("❌ [GOOGLE_CHAT] JSON marshal error: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("💬 [GOOGLE_CHAT] Sending POST request to webhook...\n")
|
||||||
|
resp, err := http.Post(config.GoogleChatWebhookURL, "application/json", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
// fmt.Printf("❌ [GOOGLE_CHAT] HTTP error: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// fmt.Printf("💬 [GOOGLE_CHAT] Response status: %d\n", resp.StatusCode)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
// fmt.Printf("❌ [GOOGLE_CHAT] Webhook error, status: %d\n", resp.StatusCode)
|
||||||
|
return fmt.Errorf("google chat webhook error, status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("✅ [GOOGLE_CHAT] Message sent successfully!\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendServerNotification sends a server-specific notification via Google Chat
|
||||||
|
func (gcs *GoogleChatService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error {
|
||||||
|
message := gcs.generateServerMessage(payload, template, resourceType)
|
||||||
|
return gcs.SendNotification(config, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendServiceNotification sends a service-specific notification via Google Chat
|
||||||
|
func (gcs *GoogleChatService) SendServiceNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServiceNotificationTemplate) error {
|
||||||
|
message := gcs.generateServiceMessage(payload, template)
|
||||||
|
return gcs.SendNotification(config, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateServerMessage creates a message for server notifications using server template
|
||||||
|
func (gcs *GoogleChatService) generateServerMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string {
|
||||||
|
var templateMessage string
|
||||||
|
|
||||||
|
// Select appropriate template message based on status and resource type
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "down":
|
||||||
|
templateMessage = template.DownMessage
|
||||||
|
case "up":
|
||||||
|
templateMessage = template.UpMessage
|
||||||
|
case "warning":
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
case "paused":
|
||||||
|
templateMessage = template.PausedMessage
|
||||||
|
default:
|
||||||
|
// Handle resource-specific messages
|
||||||
|
switch resourceType {
|
||||||
|
case "cpu":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreCPUMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.CPUMessage
|
||||||
|
}
|
||||||
|
case "ram", "memory":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreRAMMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.RAMMessage
|
||||||
|
}
|
||||||
|
case "disk":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreDiskMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.DiskMessage
|
||||||
|
}
|
||||||
|
case "network":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreNetworkMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.NetworkMessage
|
||||||
|
}
|
||||||
|
case "cpu_temp", "cpu_temperature":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreCPUTempMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.CPUTempMessage
|
||||||
|
}
|
||||||
|
case "disk_io":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreDiskIOMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.DiskIOMessage
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no template message found, use a default
|
||||||
|
if templateMessage == "" {
|
||||||
|
templateMessage = gcs.generateDefaultServerMessage(payload, resourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gcs.replacePlaceholders(templateMessage, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateServiceMessage creates a message for service notifications using service template
|
||||||
|
func (gcs *GoogleChatService) generateServiceMessage(payload *NotificationPayload, template *ServiceNotificationTemplate) string {
|
||||||
|
var templateMessage string
|
||||||
|
|
||||||
|
// Select appropriate template message based on status
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
templateMessage = template.UpMessage
|
||||||
|
case "down":
|
||||||
|
templateMessage = template.DownMessage
|
||||||
|
case "maintenance":
|
||||||
|
templateMessage = template.MaintenanceMessage
|
||||||
|
case "incident":
|
||||||
|
templateMessage = template.IncidentMessage
|
||||||
|
case "resolved":
|
||||||
|
templateMessage = template.ResolvedMessage
|
||||||
|
case "warning":
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
default:
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no template message found, use a default
|
||||||
|
if templateMessage == "" {
|
||||||
|
templateMessage = gcs.generateDefaultUptimeMessage(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gcs.replacePlaceholders(templateMessage, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// replacePlaceholders replaces all placeholders in the message with actual values
|
||||||
|
func (gcs *GoogleChatService) replacePlaceholders(message string, payload *NotificationPayload) string {
|
||||||
|
// Replace basic placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
|
||||||
|
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||||
|
message = strings.ReplaceAll(message, "${host}", gcs.safeString(payload.Host))
|
||||||
|
message = strings.ReplaceAll(message, "${hostname}", gcs.safeString(payload.Hostname))
|
||||||
|
|
||||||
|
// Replace URL with fallback to host
|
||||||
|
url := gcs.safeString(payload.URL)
|
||||||
|
if url == "N/A" && payload.Host != "" {
|
||||||
|
url = payload.Host
|
||||||
|
}
|
||||||
|
message = strings.ReplaceAll(message, "${url}", url)
|
||||||
|
|
||||||
|
// Replace domain
|
||||||
|
message = strings.ReplaceAll(message, "${domain}", gcs.safeString(payload.Domain))
|
||||||
|
|
||||||
|
// Replace service type
|
||||||
|
if payload.ServiceType != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${service_type}", strings.ToUpper(payload.ServiceType))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${service_type}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace region and agent info
|
||||||
|
message = strings.ReplaceAll(message, "${region_name}", gcs.safeString(payload.RegionName))
|
||||||
|
message = strings.ReplaceAll(message, "${agent_id}", gcs.safeString(payload.AgentID))
|
||||||
|
|
||||||
|
// Handle numeric fields safely
|
||||||
|
if payload.Port > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${port}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.ResponseTime > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${response_time}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Uptime > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${uptime}", fmt.Sprintf("%d%%", payload.Uptime))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${uptime}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace server monitoring fields
|
||||||
|
message = strings.ReplaceAll(message, "${cpu_usage}", gcs.safeString(payload.CPUUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${ram_usage}", gcs.safeString(payload.RAMUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${disk_usage}", gcs.safeString(payload.DiskUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${network_usage}", gcs.safeString(payload.NetworkUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${cpu_temp}", gcs.safeString(payload.CPUTemp))
|
||||||
|
message = strings.ReplaceAll(message, "${disk_io}", gcs.safeString(payload.DiskIO))
|
||||||
|
message = strings.ReplaceAll(message, "${threshold}", gcs.safeString(payload.Threshold))
|
||||||
|
|
||||||
|
// Replace error message - important for uptime services
|
||||||
|
message = strings.ReplaceAll(message, "${error_message}", gcs.safeString(payload.ErrorMessage))
|
||||||
|
message = strings.ReplaceAll(message, "${error}", gcs.safeString(payload.ErrorMessage))
|
||||||
|
|
||||||
|
// Replace time placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeString returns the string value or "N/A" if empty
|
||||||
|
func (gcs *GoogleChatService) safeString(value string) string {
|
||||||
|
if value == "" {
|
||||||
|
return "N/A"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDefaultUptimeMessage creates a default uptime message with proper formatting
|
||||||
|
func (gcs *GoogleChatService) generateDefaultUptimeMessage(payload *NotificationPayload) string {
|
||||||
|
// Status emoji mapping for Google Chat
|
||||||
|
statusEmoji := "🔵"
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
statusEmoji = "🟢"
|
||||||
|
case "down":
|
||||||
|
statusEmoji = "🔴"
|
||||||
|
case "warning":
|
||||||
|
statusEmoji = "🟡"
|
||||||
|
case "maintenance", "paused":
|
||||||
|
statusEmoji = "🟠"
|
||||||
|
}
|
||||||
|
|
||||||
|
message := fmt.Sprintf("%s Service %s is %s.", statusEmoji, payload.ServiceName, strings.ToUpper(payload.Status))
|
||||||
|
|
||||||
|
// Build formatted details
|
||||||
|
details := []string{}
|
||||||
|
|
||||||
|
// Add URL or host
|
||||||
|
if payload.URL != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Host URL: %s", payload.URL))
|
||||||
|
} else if payload.Host != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Host: %s", payload.Host))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add service type
|
||||||
|
if payload.ServiceType != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Type: %s", strings.ToUpper(payload.ServiceType)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add port if available
|
||||||
|
if payload.Port > 0 {
|
||||||
|
details = append(details, fmt.Sprintf(" - Port: %d", payload.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add domain if available
|
||||||
|
if payload.Domain != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Domain: %s", payload.Domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add response time
|
||||||
|
if payload.ResponseTime > 0 {
|
||||||
|
details = append(details, fmt.Sprintf(" - Response time: %dms", payload.ResponseTime))
|
||||||
|
} else {
|
||||||
|
details = append(details, " - Response time: N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add region info
|
||||||
|
if payload.RegionName != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Region: %s", payload.RegionName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add agent info
|
||||||
|
if payload.AgentID != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Agent: %s", payload.AgentID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add uptime if available
|
||||||
|
if payload.Uptime > 0 {
|
||||||
|
details = append(details, fmt.Sprintf(" - Uptime: %d%%", payload.Uptime))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timestamp
|
||||||
|
details = append(details, fmt.Sprintf(" - Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05")))
|
||||||
|
|
||||||
|
// Combine message with details
|
||||||
|
if len(details) > 0 {
|
||||||
|
message += "\n" + strings.Join(details, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDefaultServerMessage creates a default server message
|
||||||
|
func (gcs *GoogleChatService) generateDefaultServerMessage(payload *NotificationPayload, resourceType string) string {
|
||||||
|
statusEmoji := "🔵"
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
statusEmoji = "🟢"
|
||||||
|
case "down":
|
||||||
|
statusEmoji = "🔴"
|
||||||
|
case "warning":
|
||||||
|
statusEmoji = "🟡"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s🖥️ Server %s (%s) status: %s", statusEmoji, payload.ServiceName, payload.Hostname, strings.ToUpper(payload.Status))
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service-operation/pocketbase"
|
||||||
|
"service-operation/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceNotifier provides an easy interface to send notifications for service events
|
||||||
|
type ServiceNotifier struct {
|
||||||
|
manager *NotificationManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServiceNotifier creates a new service notifier
|
||||||
|
func NewServiceNotifier(pbClient *pocketbase.PocketBaseClient) *ServiceNotifier {
|
||||||
|
return &ServiceNotifier{
|
||||||
|
manager: NewNotificationManager(pbClient),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyServiceStatus sends a notification for service status change
|
||||||
|
func (sn *ServiceNotifier) NotifyServiceStatus(service pocketbase.Service, result *types.OperationResult) {
|
||||||
|
// Check if service has notification configured
|
||||||
|
if service.NotificationID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create notification payload
|
||||||
|
payload := &NotificationPayload{
|
||||||
|
ServiceName: service.Name,
|
||||||
|
Status: service.Status,
|
||||||
|
Host: service.Host,
|
||||||
|
Port: service.Port,
|
||||||
|
ServiceType: service.ServiceType,
|
||||||
|
ResponseTime: result.ResponseTime.Milliseconds(),
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
ErrorMessage: result.Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notification
|
||||||
|
if err := sn.manager.SendServiceNotification(payload, service.NotificationID, service.TemplateID); err != nil {
|
||||||
|
log.Printf("Failed to send notification for service %s: %v", service.Name, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Notification sent successfully for service %s", service.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyCustom sends a custom notification
|
||||||
|
func (sn *ServiceNotifier) NotifyCustom(notificationID, templateID, serviceName, status, message string) error {
|
||||||
|
payload := &NotificationPayload{
|
||||||
|
ServiceName: serviceName,
|
||||||
|
Status: status,
|
||||||
|
Message: message,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return sn.manager.SendServiceNotification(payload, notificationID, templateID)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
// "log"
|
||||||
|
|
||||||
|
"service-operation/pocketbase"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotificationManager handles all notification operations
|
||||||
|
type NotificationManager struct {
|
||||||
|
pbClient *pocketbase.PocketBaseClient
|
||||||
|
services map[string]NotificationService
|
||||||
|
serverManager *ServerNotificationManager
|
||||||
|
uptimeManager *UptimeNotificationManager
|
||||||
|
sslManager *SSLNotificationManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNotificationManager creates a new notification manager
|
||||||
|
func NewNotificationManager(pbClient *pocketbase.PocketBaseClient) *NotificationManager {
|
||||||
|
// Initialize notification services
|
||||||
|
services := make(map[string]NotificationService)
|
||||||
|
|
||||||
|
// log.Printf("🔧 Initializing notification services...")
|
||||||
|
services["telegram"] = NewTelegramService()
|
||||||
|
services["signal"] = NewSignalService()
|
||||||
|
services["discord"] = NewDiscordService()
|
||||||
|
services["slack"] = NewSlackService()
|
||||||
|
services["google_chat"] = NewGoogleChatService()
|
||||||
|
services["email"] = NewEmailService()
|
||||||
|
services["webhook"] = NewWebhookService()
|
||||||
|
|
||||||
|
// log.Printf("✅ Notification services initialized: %v", getKeys(services))
|
||||||
|
|
||||||
|
// Create specialized managers
|
||||||
|
serverManager := NewServerNotificationManager(pbClient, services)
|
||||||
|
uptimeManager := NewUptimeNotificationManager(pbClient, services)
|
||||||
|
sslManager := NewSSLNotificationManager(pbClient, services)
|
||||||
|
|
||||||
|
return &NotificationManager{
|
||||||
|
pbClient: pbClient,
|
||||||
|
services: services,
|
||||||
|
serverManager: serverManager,
|
||||||
|
uptimeManager: uptimeManager,
|
||||||
|
sslManager: sslManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendServiceNotification sends notification for a service based on its configuration
|
||||||
|
func (nm *NotificationManager) SendServiceNotification(payload *NotificationPayload, notificationID, templateID string) error {
|
||||||
|
return nm.serverManager.SendServiceNotification(payload, notificationID, templateID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendResourceNotification sends notification for specific resource alerts (CPU, RAM, Disk, etc.)
|
||||||
|
func (nm *NotificationManager) SendResourceNotification(payload *NotificationPayload, notificationID, templateID, resourceType string) error {
|
||||||
|
return nm.serverManager.SendResourceNotification(payload, notificationID, templateID, resourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendUptimeServiceNotification sends notification for uptime services using service templates
|
||||||
|
func (nm *NotificationManager) SendUptimeServiceNotification(payload *NotificationPayload, notificationID, templateID string) error {
|
||||||
|
return nm.uptimeManager.SendUptimeServiceNotification(payload, notificationID, templateID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSSLNotification sends notification for SSL certificates using SSL templates
|
||||||
|
func (nm *NotificationManager) SendSSLNotification(payload *NotificationPayload, notificationID, templateID string) error {
|
||||||
|
return nm.sslManager.SendSSLNotification(payload, notificationID, templateID)
|
||||||
|
}
|
||||||
@@ -0,0 +1,480 @@
|
|||||||
|
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"service-operation/pocketbase"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerNotificationManager handles server-specific notifications
|
||||||
|
type ServerNotificationManager struct {
|
||||||
|
pbClient *pocketbase.PocketBaseClient
|
||||||
|
services map[string]NotificationService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServerNotificationManager creates a new server notification manager
|
||||||
|
func NewServerNotificationManager(pbClient *pocketbase.PocketBaseClient, services map[string]NotificationService) *ServerNotificationManager {
|
||||||
|
return &ServerNotificationManager{
|
||||||
|
pbClient: pbClient,
|
||||||
|
services: services,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendServiceNotification sends notification for a service based on its configuration
|
||||||
|
func (snm *ServerNotificationManager) SendServiceNotification(payload *NotificationPayload, notificationID, templateID string) error {
|
||||||
|
// log.Printf("📨 SendServiceNotification called with notification_id: %s, template_id: %s", notificationID, templateID)
|
||||||
|
// log.Printf("📨 Payload: %+v", payload)
|
||||||
|
|
||||||
|
if notificationID == "" {
|
||||||
|
// log.Printf("❌ Notification ID is required but was empty")
|
||||||
|
return fmt.Errorf("notification ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse multiple notification IDs
|
||||||
|
notificationIDs := parseNotificationIDs(notificationID)
|
||||||
|
if len(notificationIDs) == 0 {
|
||||||
|
// log.Printf("❌ No valid notification IDs found")
|
||||||
|
return fmt.Errorf("no valid notification IDs found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors []string
|
||||||
|
successCount := 0
|
||||||
|
|
||||||
|
// Send notification to each channel
|
||||||
|
for _, id := range notificationIDs {
|
||||||
|
// log.Printf("📤 Processing notification ID: %s", id)
|
||||||
|
|
||||||
|
// Check if notification is enabled
|
||||||
|
if !isNotificationEnabled(snm.pbClient, id) {
|
||||||
|
// log.Printf("⚠️ Notification %s is disabled, skipping", id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get alert configuration
|
||||||
|
alertConfig, err := getAlertConfiguration(snm.pbClient, id)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to get alert configuration for %s: %v", id, err)
|
||||||
|
errors = append(errors, fmt.Sprintf("failed to get config for %s: %v", id, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get template if provided
|
||||||
|
var template *ServerNotificationTemplate
|
||||||
|
if templateID != "" {
|
||||||
|
template, err = getNotificationTemplate(snm.pbClient, templateID)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("⚠️ Warning: failed to get template %s for notification %s: %v", templateID, id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate message from template or use default
|
||||||
|
message := snm.generateMessage(payload, template)
|
||||||
|
// log.Printf("📝 Generated message for %s: %s", id, message)
|
||||||
|
|
||||||
|
// Send notification using appropriate service
|
||||||
|
service, exists := snm.services[alertConfig.NotificationType]
|
||||||
|
if !exists {
|
||||||
|
// log.Printf("❌ Unsupported notification type for %s: %s", id, alertConfig.NotificationType)
|
||||||
|
errors = append(errors, fmt.Sprintf("unsupported notification type for %s: %s", id, alertConfig.NotificationType))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.SendNotification(alertConfig, message)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to send notification via %s for %s: %v", alertConfig.NotificationType, id, err)
|
||||||
|
errors = append(errors, fmt.Sprintf("failed to send via %s for %s: %v", alertConfig.NotificationType, id, err))
|
||||||
|
} else {
|
||||||
|
// log.Printf("✅ Successfully sent notification via %s for %s", alertConfig.NotificationType, id)
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report results
|
||||||
|
if successCount > 0 {
|
||||||
|
// log.Printf("✅ Successfully sent %d out of %d notifications", successCount, len(notificationIDs))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
// log.Printf("❌ Errors occurred: %v", errors)
|
||||||
|
if successCount == 0 {
|
||||||
|
return fmt.Errorf("all notifications failed: %v", errors)
|
||||||
|
}
|
||||||
|
// If some succeeded, just log errors but don't return error
|
||||||
|
// log.Printf("⚠️ Some notifications failed but %d succeeded", successCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendResourceNotification sends notification for specific resource alerts (CPU, RAM, Disk, etc.)
|
||||||
|
func (snm *ServerNotificationManager) SendResourceNotification(payload *NotificationPayload, notificationID, templateID, resourceType string) error {
|
||||||
|
// log.Printf("📨 SendResourceNotification called for resource: %s", resourceType)
|
||||||
|
// log.Printf("📨 Notification ID: %s, Template ID: %s", notificationID, templateID)
|
||||||
|
// log.Printf("📨 Payload: %+v", payload)
|
||||||
|
|
||||||
|
if notificationID == "" {
|
||||||
|
// log.Printf("❌ Notification ID is required but was empty")
|
||||||
|
return fmt.Errorf("notification ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse multiple notification IDs
|
||||||
|
notificationIDs := parseNotificationIDs(notificationID)
|
||||||
|
if len(notificationIDs) == 0 {
|
||||||
|
// log.Printf("❌ No valid notification IDs found")
|
||||||
|
return fmt.Errorf("no valid notification IDs found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors []string
|
||||||
|
successCount := 0
|
||||||
|
|
||||||
|
// Send notification to each channel
|
||||||
|
for _, id := range notificationIDs {
|
||||||
|
// log.Printf("📤 Processing resource notification ID: %s", id)
|
||||||
|
|
||||||
|
// Check if notification is enabled
|
||||||
|
if !isNotificationEnabled(snm.pbClient, id) {
|
||||||
|
// log.Printf("⚠️ Notification %s is disabled, skipping", id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get alert configuration
|
||||||
|
alertConfig, err := getAlertConfiguration(snm.pbClient, id)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to get alert configuration for %s: %v", id, err)
|
||||||
|
errors = append(errors, fmt.Sprintf("failed to get config for %s: %v", id, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get template if provided
|
||||||
|
var template *ServerNotificationTemplate
|
||||||
|
if templateID != "" {
|
||||||
|
template, err = getNotificationTemplate(snm.pbClient, templateID)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("⚠️ Warning: failed to get template %s for notification %s: %v", templateID, id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate resource-specific message
|
||||||
|
message := snm.generateResourceMessage(payload, template, resourceType)
|
||||||
|
// log.Printf("📝 Generated resource message for %s: %s", id, message)
|
||||||
|
|
||||||
|
// Send notification
|
||||||
|
service, exists := snm.services[alertConfig.NotificationType]
|
||||||
|
if !exists {
|
||||||
|
// log.Printf("❌ Unsupported notification type for %s: %s", id, alertConfig.NotificationType)
|
||||||
|
errors = append(errors, fmt.Sprintf("unsupported notification type for %s: %s", id, alertConfig.NotificationType))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.SendNotification(alertConfig, message)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to send resource notification via %s for %s: %v", alertConfig.NotificationType, id, err)
|
||||||
|
errors = append(errors, fmt.Sprintf("failed to send via %s for %s: %v", alertConfig.NotificationType, id, err))
|
||||||
|
} else {
|
||||||
|
// log.Printf("✅ Successfully sent resource notification via %s for %s", alertConfig.NotificationType, id)
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report results
|
||||||
|
if successCount > 0 {
|
||||||
|
// log.Printf("✅ Successfully sent %d out of %d resource notifications", successCount, len(notificationIDs))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
// log.Printf("❌ Resource notification errors occurred: %v", errors)
|
||||||
|
if successCount == 0 {
|
||||||
|
return fmt.Errorf("all resource notifications failed: %v", errors)
|
||||||
|
}
|
||||||
|
// If some succeeded, just log errors but don't return error
|
||||||
|
// log.Printf("⚠️ Some resource notifications failed but %d succeeded", successCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateResourceMessage creates notification message for specific resource alerts
|
||||||
|
func (snm *ServerNotificationManager) generateResourceMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string {
|
||||||
|
var baseMessage string
|
||||||
|
|
||||||
|
// Select appropriate resource message from template based on status
|
||||||
|
if template != nil {
|
||||||
|
// log.Printf("🔧 Using template for resource type: %s with status: %s", resourceType, payload.Status)
|
||||||
|
|
||||||
|
// Check if this is a recovery/restore notification
|
||||||
|
if payload.Status == "up" {
|
||||||
|
// Use restore messages for recovery notifications
|
||||||
|
switch strings.ToLower(resourceType) {
|
||||||
|
case "cpu":
|
||||||
|
baseMessage = template.RestoreCPUMessage
|
||||||
|
case "ram", "memory":
|
||||||
|
baseMessage = template.RestoreRAMMessage
|
||||||
|
case "disk":
|
||||||
|
baseMessage = template.RestoreDiskMessage
|
||||||
|
case "network":
|
||||||
|
baseMessage = template.RestoreNetworkMessage
|
||||||
|
case "cpu_temp", "cpu_temperature":
|
||||||
|
baseMessage = template.RestoreCPUTempMessage
|
||||||
|
case "disk_io":
|
||||||
|
baseMessage = template.RestoreDiskIOMessage
|
||||||
|
default:
|
||||||
|
baseMessage = template.UpMessage // fallback to general up message
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use regular alert messages for warning/down notifications
|
||||||
|
switch strings.ToLower(resourceType) {
|
||||||
|
case "cpu":
|
||||||
|
baseMessage = template.CPUMessage
|
||||||
|
case "ram", "memory":
|
||||||
|
baseMessage = template.RAMMessage
|
||||||
|
case "disk":
|
||||||
|
baseMessage = template.DiskMessage
|
||||||
|
case "network":
|
||||||
|
baseMessage = template.NetworkMessage
|
||||||
|
case "cpu_temp", "cpu_temperature":
|
||||||
|
baseMessage = template.CPUTempMessage
|
||||||
|
case "disk_io":
|
||||||
|
baseMessage = template.DiskIOMessage
|
||||||
|
default:
|
||||||
|
baseMessage = template.WarningMessage // fallback to warning message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Printf("📝 Template message selected for %s (status: %s): %s", resourceType, payload.Status, baseMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no template or empty message, use default
|
||||||
|
if baseMessage == "" {
|
||||||
|
// log.Printf("🔧 Using default message for resource %s", resourceType)
|
||||||
|
baseMessage = snm.getDefaultResourceMessage(payload, resourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace placeholders with actual values
|
||||||
|
message := snm.replacePlaceholders(baseMessage, payload)
|
||||||
|
|
||||||
|
// log.Printf("📝 Final resource message after placeholder replacement: %s", message)
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultResourceMessage provides default messages for resource alerts
|
||||||
|
func (snm *ServerNotificationManager) getDefaultResourceMessage(payload *NotificationPayload, resourceType string) string {
|
||||||
|
statusEmoji := "⚠️"
|
||||||
|
statusText := "Alert"
|
||||||
|
|
||||||
|
if payload.Status == "up" {
|
||||||
|
statusEmoji = "✅"
|
||||||
|
statusText = "Recovery"
|
||||||
|
} else if payload.Status == "down" {
|
||||||
|
statusEmoji = "❌"
|
||||||
|
statusText = "Critical"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(resourceType) {
|
||||||
|
case "cpu":
|
||||||
|
if payload.Status == "up" {
|
||||||
|
return fmt.Sprintf("%s CPU %s: Server %s CPU usage has returned to normal: %s", statusEmoji, statusText, payload.ServiceName, payload.CPUUsage)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s CPU %s: Server %s CPU usage is %s", statusEmoji, statusText, payload.ServiceName, payload.CPUUsage)
|
||||||
|
case "ram", "memory":
|
||||||
|
if payload.Status == "up" {
|
||||||
|
return fmt.Sprintf("%s RAM %s: Server %s RAM usage has returned to normal: %s", statusEmoji, statusText, payload.ServiceName, payload.RAMUsage)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s RAM %s: Server %s RAM usage is %s", statusEmoji, statusText, payload.ServiceName, payload.RAMUsage)
|
||||||
|
case "disk":
|
||||||
|
if payload.Status == "up" {
|
||||||
|
return fmt.Sprintf("%s Disk %s: Server %s disk usage has returned to normal: %s", statusEmoji, statusText, payload.ServiceName, payload.DiskUsage)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s Disk %s: Server %s disk usage is %s", statusEmoji, statusText, payload.ServiceName, payload.DiskUsage)
|
||||||
|
case "network":
|
||||||
|
if payload.Status == "up" {
|
||||||
|
return fmt.Sprintf("%s Network %s: Server %s network usage has returned to normal: %s", statusEmoji, statusText, payload.ServiceName, payload.NetworkUsage)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s Network %s: Server %s network usage is %s", statusEmoji, statusText, payload.ServiceName, payload.NetworkUsage)
|
||||||
|
case "cpu_temp", "cpu_temperature":
|
||||||
|
if payload.Status == "up" {
|
||||||
|
return fmt.Sprintf("%s CPU Temperature %s: Server %s CPU temperature has returned to normal: %s", statusEmoji, statusText, payload.ServiceName, payload.CPUTemp)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s CPU Temperature %s: Server %s CPU temperature is %s", statusEmoji, statusText, payload.ServiceName, payload.CPUTemp)
|
||||||
|
case "disk_io":
|
||||||
|
if payload.Status == "up" {
|
||||||
|
return fmt.Sprintf("%s Disk I/O %s: Server %s disk I/O has returned to normal: %s", statusEmoji, statusText, payload.ServiceName, payload.DiskIO)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s Disk I/O %s: Server %s disk I/O is %s", statusEmoji, statusText, payload.ServiceName, payload.DiskIO)
|
||||||
|
default:
|
||||||
|
if payload.Status == "up" {
|
||||||
|
return fmt.Sprintf("%s Resource %s: Server %s %s has recovered", statusEmoji, statusText, payload.ServiceName, payload.Message)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s Resource %s: Server %s %s", statusEmoji, statusText, payload.ServiceName, payload.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateMessage creates notification message from template or default
|
||||||
|
func (snm *ServerNotificationManager) generateMessage(payload *NotificationPayload, template *ServerNotificationTemplate) string {
|
||||||
|
var baseMessage string
|
||||||
|
|
||||||
|
// Select appropriate message based on status and template
|
||||||
|
if template != nil {
|
||||||
|
// log.Printf("🔧 Using template for status: %s", strings.ToLower(payload.Status))
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
baseMessage = template.UpMessage
|
||||||
|
case "down":
|
||||||
|
baseMessage = template.DownMessage
|
||||||
|
case "warning":
|
||||||
|
baseMessage = template.WarningMessage
|
||||||
|
case "paused":
|
||||||
|
baseMessage = template.PausedMessage
|
||||||
|
default:
|
||||||
|
baseMessage = template.UpMessage // fallback
|
||||||
|
}
|
||||||
|
// log.Printf("📝 Template message selected: %s", baseMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no template or empty message, use default
|
||||||
|
if baseMessage == "" {
|
||||||
|
// log.Printf("🔧 Using default message (no template or empty template message)")
|
||||||
|
baseMessage = snm.getDefaultMessage(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace placeholders with actual values
|
||||||
|
message := snm.replacePlaceholders(baseMessage, payload)
|
||||||
|
|
||||||
|
// log.Printf("📝 Final message after placeholder replacement: %s", message)
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// replacePlaceholders replaces all placeholders in the message with actual values
|
||||||
|
func (snm *ServerNotificationManager) replacePlaceholders(message string, payload *NotificationPayload) string {
|
||||||
|
// Service/Server name placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
|
||||||
|
message = strings.ReplaceAll(message, "${server_name}", payload.ServiceName) // For server notifications
|
||||||
|
|
||||||
|
// Status placeholder
|
||||||
|
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||||
|
|
||||||
|
// Host/IP placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${host}", payload.Host)
|
||||||
|
message = strings.ReplaceAll(message, "${ip}", payload.Host) // Alternative placeholder
|
||||||
|
message = strings.ReplaceAll(message, "${ip_address}", payload.Host) // New server monitoring placeholder
|
||||||
|
|
||||||
|
// Hostname placeholder
|
||||||
|
message = strings.ReplaceAll(message, "${hostname}", payload.Hostname)
|
||||||
|
|
||||||
|
// Port placeholder
|
||||||
|
if payload.Port > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${port}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response time placeholder
|
||||||
|
if payload.ResponseTime > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${response_time}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
message = strings.ReplaceAll(message, "${date}", payload.Timestamp.Format("2006-01-02"))
|
||||||
|
|
||||||
|
// Service type placeholder
|
||||||
|
message = strings.ReplaceAll(message, "${service_type}", payload.ServiceType)
|
||||||
|
|
||||||
|
// Error message placeholder
|
||||||
|
if payload.ErrorMessage != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${error}", payload.ErrorMessage)
|
||||||
|
message = strings.ReplaceAll(message, "${error_message}", payload.ErrorMessage)
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${error}", "")
|
||||||
|
message = strings.ReplaceAll(message, "${error_message}", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom message placeholder
|
||||||
|
if payload.Message != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${message}", payload.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server monitoring specific placeholders
|
||||||
|
if payload.CPUUsage != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${cpu_usage}", payload.CPUUsage)
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${cpu_usage}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.RAMUsage != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${ram_usage}", payload.RAMUsage)
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${ram_usage}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.DiskUsage != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${disk_usage}", payload.DiskUsage)
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${disk_usage}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.NetworkUsage != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${network_usage}", payload.NetworkUsage)
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${network_usage}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.CPUTemp != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${cpu_temp}", payload.CPUTemp)
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${cpu_temp}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.DiskIO != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${disk_io}", payload.DiskIO)
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${disk_io}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Threshold != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${threshold}", payload.Threshold)
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${threshold}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultMessage provides a default notification message
|
||||||
|
func (snm *ServerNotificationManager) getDefaultMessage(payload *NotificationPayload) string {
|
||||||
|
statusEmoji := "✅"
|
||||||
|
if payload.Status == "down" {
|
||||||
|
statusEmoji = "❌"
|
||||||
|
} else if payload.Status == "warning" {
|
||||||
|
statusEmoji = "⚠️"
|
||||||
|
}
|
||||||
|
|
||||||
|
message := fmt.Sprintf("%s Server Alert\n\n", statusEmoji)
|
||||||
|
message += fmt.Sprintf("Server: %s\n", payload.ServiceName)
|
||||||
|
message += fmt.Sprintf("Status: %s\n", strings.ToUpper(payload.Status))
|
||||||
|
message += fmt.Sprintf("IP Address: %s\n", payload.Host)
|
||||||
|
|
||||||
|
if payload.CPUUsage != "" {
|
||||||
|
message += fmt.Sprintf("CPU Usage: %s\n", payload.CPUUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.RAMUsage != "" {
|
||||||
|
message += fmt.Sprintf("RAM Usage: %s\n", payload.RAMUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.DiskUsage != "" {
|
||||||
|
message += fmt.Sprintf("Disk Usage: %s\n", payload.DiskUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
message += fmt.Sprintf("Time: %s\n", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
|
if payload.ErrorMessage != "" {
|
||||||
|
message += fmt.Sprintf("Error: %s\n", payload.ErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"service-operation/pocketbase"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseNotificationIDs parses comma-separated notification IDs
|
||||||
|
func parseNotificationIDs(notificationID string) []string {
|
||||||
|
if notificationID == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by comma and trim whitespace
|
||||||
|
ids := strings.Split(notificationID, ",")
|
||||||
|
var cleanIDs []string
|
||||||
|
for _, id := range ids {
|
||||||
|
cleanID := strings.TrimSpace(id)
|
||||||
|
if cleanID != "" {
|
||||||
|
cleanIDs = append(cleanIDs, cleanID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("📋 Parsed notification IDs: %v", cleanIDs)
|
||||||
|
return cleanIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
// isNotificationEnabled checks if the notification is enabled
|
||||||
|
func isNotificationEnabled(pbClient *pocketbase.PocketBaseClient, notificationID string) bool {
|
||||||
|
config, err := getAlertConfiguration(pbClient, notificationID)
|
||||||
|
if err != nil {
|
||||||
|
//log.Printf("❌ Error getting alert configuration for enabled check: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled, err := strconv.ParseBool(config.Enabled)
|
||||||
|
if err != nil {
|
||||||
|
//log.Printf("❌ Error parsing enabled field: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("ℹ️ Notification %s enabled status: %t", notificationID, enabled)
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAlertConfiguration fetches alert configuration from PocketBase
|
||||||
|
func getAlertConfiguration(pbClient *pocketbase.PocketBaseClient, notificationID string) (*AlertConfiguration, error) {
|
||||||
|
url := fmt.Sprintf("%s/api/collections/alert_configurations/records/%s", pbClient.GetBaseURL(), notificationID)
|
||||||
|
//log.Printf("🌐 Fetching alert configuration from: %s", url)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
//log.Printf("❌ HTTP error fetching alert configuration: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Printf("❌ Failed to fetch alert configuration, status: %d", resp.StatusCode)
|
||||||
|
return nil, fmt.Errorf("failed to fetch alert configuration, status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config AlertConfiguration
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
|
||||||
|
//log.Printf("❌ Error decoding alert configuration JSON: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("✅ Successfully fetched alert configuration: %+v", config)
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNotificationTemplate fetches server notification template from PocketBase
|
||||||
|
func getNotificationTemplate(pbClient *pocketbase.PocketBaseClient, templateID string) (*ServerNotificationTemplate, error) {
|
||||||
|
url := fmt.Sprintf("%s/api/collections/server_notification_templates/records/%s", pbClient.GetBaseURL(), templateID)
|
||||||
|
//log.Printf("🌐 Fetching notification template from: %s", url)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
//log.Printf("❌ HTTP error fetching notification template: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Printf("❌ Failed to fetch notification template, status: %d", resp.StatusCode)
|
||||||
|
return nil, fmt.Errorf("failed to fetch notification template, status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var template ServerNotificationTemplate
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&template); err != nil {
|
||||||
|
//log.Printf("❌ Error decoding notification template JSON: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("✅ Successfully fetched notification template: %+v", template)
|
||||||
|
return &template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getServiceNotificationTemplate fetches service notification template from PocketBase
|
||||||
|
func getServiceNotificationTemplate(pbClient *pocketbase.PocketBaseClient, templateID string) (*ServiceNotificationTemplate, error) {
|
||||||
|
url := fmt.Sprintf("%s/api/collections/service_notification_templates/records/%s", pbClient.GetBaseURL(), templateID)
|
||||||
|
//log.Printf("🌐 Fetching service notification template from: %s", url)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
//log.Printf("❌ HTTP error fetching service notification template: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
//log.Printf("❌ Failed to fetch service notification template, status: %d", resp.StatusCode)
|
||||||
|
return nil, fmt.Errorf("failed to fetch service notification template, status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var template ServiceNotificationTemplate
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&template); err != nil {
|
||||||
|
//log.Printf("❌ Error decoding service notification template JSON: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("✅ Successfully fetched service notification template: %+v", template)
|
||||||
|
return &template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get map keys
|
||||||
|
func getKeys(m map[string]NotificationService) []string {
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignalService handles Signal notifications
|
||||||
|
type SignalService struct{}
|
||||||
|
|
||||||
|
// NewSignalService creates a new Signal notification service
|
||||||
|
func NewSignalService() *SignalService {
|
||||||
|
return &SignalService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignalPayload represents the payload for Signal REST API
|
||||||
|
type SignalPayload struct {
|
||||||
|
Number string `json:"number"`
|
||||||
|
Recipients []string `json:"recipients"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendNotification sends a notification via Signal REST API
|
||||||
|
func (ss *SignalService) SendNotification(config *AlertConfiguration, message string) error {
|
||||||
|
// fmt.Printf("📱 [SIGNAL] Attempting to send notification...\n")
|
||||||
|
// fmt.Printf("📱 [SIGNAL] Config - Phone Number: %s, API Endpoint: %s\n", config.SignalNumber, config.SignalAPIEndpoint)
|
||||||
|
// fmt.Printf("📱 [SIGNAL] Message: %s\n", message)
|
||||||
|
|
||||||
|
if config.SignalNumber == "" {
|
||||||
|
return fmt.Errorf("signal phone number is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.SignalAPIEndpoint == "" {
|
||||||
|
return fmt.Errorf("signal API endpoint is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the configured endpoint directly (it should already include the full path like /v2/send)
|
||||||
|
apiURL := config.SignalAPIEndpoint
|
||||||
|
// fmt.Printf("📱 [SIGNAL] API URL: %s\n", apiURL)
|
||||||
|
|
||||||
|
// Prepare payload for Signal REST API
|
||||||
|
payload := SignalPayload{
|
||||||
|
Number: config.SignalNumber, // This should be the sender's number (registered with signal-cli)
|
||||||
|
Recipients: []string{config.SignalNumber}, // Send to the same number for now
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
// fmt.Printf("❌ [SIGNAL] JSON marshal error: %v\n", err)
|
||||||
|
return fmt.Errorf("failed to marshal signal payload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("📱 [SIGNAL] Payload: %s\n", string(jsonData))
|
||||||
|
// fmt.Printf("📱 [SIGNAL] Sending POST request...\n")
|
||||||
|
|
||||||
|
// Send POST request to Signal REST API
|
||||||
|
resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
// fmt.Printf("❌ [SIGNAL] HTTP error: %v\n", err)
|
||||||
|
return fmt.Errorf("failed to send signal request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// fmt.Printf("📱 [SIGNAL] Response Status: %d\n", resp.StatusCode)
|
||||||
|
|
||||||
|
// Check if the request was successful
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||||
|
// fmt.Printf("❌ [SIGNAL] HTTP Status error: %d\n", resp.StatusCode)
|
||||||
|
|
||||||
|
// Try to read error response
|
||||||
|
var errorResponse map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&errorResponse); err == nil {
|
||||||
|
// fmt.Printf("❌ [SIGNAL] Error response: %+v\n", errorResponse)
|
||||||
|
return fmt.Errorf("signal API error (status %d): %v", resp.StatusCode, errorResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("signal API returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
// fmt.Printf("❌ [SIGNAL] JSON decode error: %v\n", err)
|
||||||
|
// Don't fail if we can't decode response but got success status
|
||||||
|
// fmt.Printf("✅ [SIGNAL] Message sent successfully (response decode failed but status was OK)!\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("📱 [SIGNAL] Response: %+v\n", result)
|
||||||
|
// fmt.Printf("✅ [SIGNAL] Message sent successfully!\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendServerNotification sends a server-specific notification via Signal
|
||||||
|
func (ss *SignalService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error {
|
||||||
|
message := ss.generateServerMessage(payload, template, resourceType)
|
||||||
|
return ss.SendNotification(config, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendServiceNotification sends a service-specific notification via Signal
|
||||||
|
func (ss *SignalService) SendServiceNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServiceNotificationTemplate) error {
|
||||||
|
message := ss.generateServiceMessage(payload, template)
|
||||||
|
return ss.SendNotification(config, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateServerMessage creates a message for server notifications using server template
|
||||||
|
func (ss *SignalService) generateServerMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string {
|
||||||
|
var templateMessage string
|
||||||
|
|
||||||
|
// Select appropriate template message based on status and resource type
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "down":
|
||||||
|
templateMessage = template.DownMessage
|
||||||
|
case "up":
|
||||||
|
templateMessage = template.UpMessage
|
||||||
|
case "warning":
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
case "paused":
|
||||||
|
templateMessage = template.PausedMessage
|
||||||
|
default:
|
||||||
|
// Handle resource-specific messages
|
||||||
|
switch resourceType {
|
||||||
|
case "cpu":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreCPUMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.CPUMessage
|
||||||
|
}
|
||||||
|
case "ram", "memory":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreRAMMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.RAMMessage
|
||||||
|
}
|
||||||
|
case "disk":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreDiskMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.DiskMessage
|
||||||
|
}
|
||||||
|
case "network":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreNetworkMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.NetworkMessage
|
||||||
|
}
|
||||||
|
case "cpu_temp", "cpu_temperature":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreCPUTempMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.CPUTempMessage
|
||||||
|
}
|
||||||
|
case "disk_io":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreDiskIOMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.DiskIOMessage
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no template message found, use a default
|
||||||
|
if templateMessage == "" {
|
||||||
|
templateMessage = ss.generateDefaultServerMessage(payload, resourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss.replacePlaceholders(templateMessage, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateServiceMessage creates a message for service notifications using service template
|
||||||
|
func (ss *SignalService) generateServiceMessage(payload *NotificationPayload, template *ServiceNotificationTemplate) string {
|
||||||
|
var templateMessage string
|
||||||
|
|
||||||
|
// Select appropriate template message based on status
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
templateMessage = template.UpMessage
|
||||||
|
case "down":
|
||||||
|
templateMessage = template.DownMessage
|
||||||
|
case "maintenance":
|
||||||
|
templateMessage = template.MaintenanceMessage
|
||||||
|
case "incident":
|
||||||
|
templateMessage = template.IncidentMessage
|
||||||
|
case "resolved":
|
||||||
|
templateMessage = template.ResolvedMessage
|
||||||
|
case "warning":
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
default:
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no template message found, use a default
|
||||||
|
if templateMessage == "" {
|
||||||
|
templateMessage = ss.generateDefaultUptimeMessage(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss.replacePlaceholders(templateMessage, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// replacePlaceholders replaces all placeholders in the message with actual values
|
||||||
|
func (ss *SignalService) replacePlaceholders(message string, payload *NotificationPayload) string {
|
||||||
|
// Replace basic placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
|
||||||
|
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||||
|
message = strings.ReplaceAll(message, "${host}", ss.safeString(payload.Host))
|
||||||
|
message = strings.ReplaceAll(message, "${hostname}", ss.safeString(payload.Hostname))
|
||||||
|
|
||||||
|
// Replace URL with fallback to host
|
||||||
|
url := ss.safeString(payload.URL)
|
||||||
|
if url == "N/A" && payload.Host != "" {
|
||||||
|
url = payload.Host
|
||||||
|
}
|
||||||
|
message = strings.ReplaceAll(message, "${url}", url)
|
||||||
|
|
||||||
|
// Replace domain
|
||||||
|
message = strings.ReplaceAll(message, "${domain}", ss.safeString(payload.Domain))
|
||||||
|
|
||||||
|
// Replace service type
|
||||||
|
if payload.ServiceType != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${service_type}", strings.ToUpper(payload.ServiceType))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${service_type}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace region and agent info
|
||||||
|
message = strings.ReplaceAll(message, "${region_name}", ss.safeString(payload.RegionName))
|
||||||
|
message = strings.ReplaceAll(message, "${agent_id}", ss.safeString(payload.AgentID))
|
||||||
|
|
||||||
|
// Handle numeric fields safely
|
||||||
|
if payload.Port > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${port}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.ResponseTime > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${response_time}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Uptime > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${uptime}", fmt.Sprintf("%d%%", payload.Uptime))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${uptime}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace server monitoring fields
|
||||||
|
message = strings.ReplaceAll(message, "${cpu_usage}", ss.safeString(payload.CPUUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${ram_usage}", ss.safeString(payload.RAMUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${disk_usage}", ss.safeString(payload.DiskUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${network_usage}", ss.safeString(payload.NetworkUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${cpu_temp}", ss.safeString(payload.CPUTemp))
|
||||||
|
message = strings.ReplaceAll(message, "${disk_io}", ss.safeString(payload.DiskIO))
|
||||||
|
message = strings.ReplaceAll(message, "${threshold}", ss.safeString(payload.Threshold))
|
||||||
|
|
||||||
|
// Replace error message - important for uptime services
|
||||||
|
message = strings.ReplaceAll(message, "${error_message}", ss.safeString(payload.ErrorMessage))
|
||||||
|
message = strings.ReplaceAll(message, "${error}", ss.safeString(payload.ErrorMessage))
|
||||||
|
|
||||||
|
// Replace time placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeString returns the string value or "N/A" if empty
|
||||||
|
func (ss *SignalService) safeString(value string) string {
|
||||||
|
if value == "" {
|
||||||
|
return "N/A"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDefaultUptimeMessage creates a default uptime message with proper formatting
|
||||||
|
func (ss *SignalService) generateDefaultUptimeMessage(payload *NotificationPayload) string {
|
||||||
|
// Status emoji mapping
|
||||||
|
statusEmoji := "🔵"
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
statusEmoji = "🟢"
|
||||||
|
case "down":
|
||||||
|
statusEmoji = "🔴"
|
||||||
|
case "warning":
|
||||||
|
statusEmoji = "🟡"
|
||||||
|
case "maintenance", "paused":
|
||||||
|
statusEmoji = "🟠"
|
||||||
|
}
|
||||||
|
|
||||||
|
message := fmt.Sprintf("%s Service %s is %s.", statusEmoji, payload.ServiceName, strings.ToUpper(payload.Status))
|
||||||
|
|
||||||
|
// Build formatted details
|
||||||
|
details := []string{}
|
||||||
|
|
||||||
|
// Add URL or host
|
||||||
|
if payload.URL != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Host URL: %s", payload.URL))
|
||||||
|
} else if payload.Host != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Host: %s", payload.Host))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add service type
|
||||||
|
if payload.ServiceType != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Type: %s", strings.ToUpper(payload.ServiceType)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add port if available
|
||||||
|
if payload.Port > 0 {
|
||||||
|
details = append(details, fmt.Sprintf(" - Port: %d", payload.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add domain if available
|
||||||
|
if payload.Domain != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Domain: %s", payload.Domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add response time
|
||||||
|
if payload.ResponseTime > 0 {
|
||||||
|
details = append(details, fmt.Sprintf(" - Response time: %dms", payload.ResponseTime))
|
||||||
|
} else {
|
||||||
|
details = append(details, " - Response time: N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add region info
|
||||||
|
if payload.RegionName != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Region: %s", payload.RegionName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add agent info
|
||||||
|
if payload.AgentID != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Agent: %s", payload.AgentID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add uptime if available
|
||||||
|
if payload.Uptime > 0 {
|
||||||
|
details = append(details, fmt.Sprintf(" - Uptime: %d%%", payload.Uptime))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timestamp
|
||||||
|
details = append(details, fmt.Sprintf(" - Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05")))
|
||||||
|
|
||||||
|
// Combine message with details
|
||||||
|
if len(details) > 0 {
|
||||||
|
message += "\n" + strings.Join(details, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDefaultServerMessage creates a default server message
|
||||||
|
func (ss *SignalService) generateDefaultServerMessage(payload *NotificationPayload, resourceType string) string {
|
||||||
|
statusEmoji := "🔵"
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
statusEmoji = "🟢"
|
||||||
|
case "down":
|
||||||
|
statusEmoji = "🔴"
|
||||||
|
case "warning":
|
||||||
|
statusEmoji = "🟡"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s 🖥️ Server %s (%s) status: %s", statusEmoji, payload.ServiceName, payload.Hostname, strings.ToUpper(payload.Status))
|
||||||
|
}
|
||||||
@@ -0,0 +1,538 @@
|
|||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SlackService handles Slack notifications
|
||||||
|
type SlackService struct{}
|
||||||
|
|
||||||
|
// NewSlackService creates a new Slack notification service
|
||||||
|
func NewSlackService() *SlackService {
|
||||||
|
return &SlackService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlackPayload represents the payload for Slack webhook
|
||||||
|
type SlackPayload struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Channel string `json:"channel,omitempty"`
|
||||||
|
IconEmoji string `json:"icon_emoji,omitempty"`
|
||||||
|
Attachments []SlackAttachment `json:"attachments,omitempty"`
|
||||||
|
Blocks []SlackBlock `json:"blocks,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlackAttachment represents a Slack attachment
|
||||||
|
type SlackAttachment struct {
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
Timestamp int64 `json:"ts,omitempty"`
|
||||||
|
Fields []SlackField `json:"fields,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlackField represents a field in Slack attachment
|
||||||
|
type SlackField struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Short bool `json:"short"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlackBlock represents a Slack block element
|
||||||
|
type SlackBlock struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text *SlackText `json:"text,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlackText represents text in Slack blocks
|
||||||
|
type SlackText struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendNotification sends a notification via Slack webhook
|
||||||
|
func (ss *SlackService) SendNotification(config *AlertConfiguration, message string) error {
|
||||||
|
if config.SlackWebhookURL == "" {
|
||||||
|
return fmt.Errorf("slack webhook URL is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and format the message for better Slack presentation
|
||||||
|
formattedPayload := ss.createSlackPayload(config, message)
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(formattedPayload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Post(config.SlackWebhookURL, "application/json", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("slack webhook error, status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendServerNotification sends a server-specific notification via Slack
|
||||||
|
func (ss *SlackService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error {
|
||||||
|
message := ss.generateServerMessage(payload, template, resourceType)
|
||||||
|
return ss.SendNotification(config, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendServiceNotification sends a service-specific notification via Slack
|
||||||
|
func (ss *SlackService) SendServiceNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServiceNotificationTemplate) error {
|
||||||
|
message := ss.generateServiceMessage(payload, template)
|
||||||
|
return ss.SendNotification(config, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createSlackPayload creates a properly formatted Slack payload with rich formatting
|
||||||
|
func (ss *SlackService) createSlackPayload(config *AlertConfiguration, message string) SlackPayload {
|
||||||
|
// Determine color based on message content
|
||||||
|
color := ss.getColorFromMessage(message)
|
||||||
|
|
||||||
|
// Parse message for structured presentation
|
||||||
|
title, fields := ss.parseMessageForSlack(message)
|
||||||
|
|
||||||
|
payload := SlackPayload{
|
||||||
|
Username: config.NotifyName,
|
||||||
|
IconEmoji: ss.getEmojiFromMessage(message),
|
||||||
|
Text: title,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create attachment for better formatting
|
||||||
|
if len(fields) > 0 {
|
||||||
|
attachment := SlackAttachment{
|
||||||
|
Color: color,
|
||||||
|
Title: title,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Fields: fields,
|
||||||
|
}
|
||||||
|
payload.Attachments = []SlackAttachment{attachment}
|
||||||
|
payload.Text = "" // Clear text when using attachments
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMessageForSlack parses the message to extract title and create structured fields
|
||||||
|
func (ss *SlackService) parseMessageForSlack(message string) (string, []SlackField) {
|
||||||
|
lines := strings.Split(message, "\n")
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
title := strings.TrimSpace(lines[0])
|
||||||
|
var fields []SlackField
|
||||||
|
|
||||||
|
// Process bullet points and key-value pairs
|
||||||
|
for i, line := range lines {
|
||||||
|
if i == 0 {
|
||||||
|
continue // Skip title
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle bullet points (•, -, *)
|
||||||
|
if strings.HasPrefix(line, "•") || strings.HasPrefix(line, "-") || strings.HasPrefix(line, "*") {
|
||||||
|
// Remove bullet and clean up
|
||||||
|
cleaned := strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(line, "•"), "-"), "*"))
|
||||||
|
|
||||||
|
// Try to split on colon for key-value pairs
|
||||||
|
if strings.Contains(cleaned, ":") {
|
||||||
|
parts := strings.SplitN(cleaned, ":", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
fields = append(fields, SlackField{
|
||||||
|
Title: key,
|
||||||
|
Value: value,
|
||||||
|
Short: len(value) < 30, // Short fields for compact display
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add as a single field
|
||||||
|
fields = append(fields, SlackField{
|
||||||
|
Title: "Info",
|
||||||
|
Value: cleaned,
|
||||||
|
Short: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if strings.Contains(line, ":") {
|
||||||
|
// Direct key-value pairs
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
fields = append(fields, SlackField{
|
||||||
|
Title: key,
|
||||||
|
Value: value,
|
||||||
|
Short: len(value) < 30,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return title, fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// getColorFromMessage determines the appropriate color based on message content
|
||||||
|
func (ss *SlackService) getColorFromMessage(message string) string {
|
||||||
|
lowerMsg := strings.ToLower(message)
|
||||||
|
|
||||||
|
// Critical/Error states - Red
|
||||||
|
if strings.Contains(lowerMsg, "down") || strings.Contains(lowerMsg, "failed") ||
|
||||||
|
strings.Contains(lowerMsg, "error") || strings.Contains(lowerMsg, "expired") ||
|
||||||
|
strings.Contains(lowerMsg, "🔴") || strings.Contains(lowerMsg, "🚨") {
|
||||||
|
return "#FF0000" // Red
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning states - Yellow/Orange
|
||||||
|
if strings.Contains(lowerMsg, "warning") || strings.Contains(lowerMsg, "expiring") ||
|
||||||
|
strings.Contains(lowerMsg, "🟡") || strings.Contains(lowerMsg, "⚠️") {
|
||||||
|
return "#FFA500" // Orange
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success states - Green
|
||||||
|
if strings.Contains(lowerMsg, "up") || strings.Contains(lowerMsg, "restored") ||
|
||||||
|
strings.Contains(lowerMsg, "resolved") || strings.Contains(lowerMsg, "🟢") ||
|
||||||
|
strings.Contains(lowerMsg, "✅") {
|
||||||
|
return "#00FF00" // Green
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintenance/Info states - Blue
|
||||||
|
if strings.Contains(lowerMsg, "maintenance") || strings.Contains(lowerMsg, "paused") ||
|
||||||
|
strings.Contains(lowerMsg, "🟠") || strings.Contains(lowerMsg, "🔵") {
|
||||||
|
return "#0080FF" // Blue
|
||||||
|
}
|
||||||
|
|
||||||
|
return "#808080" // Default gray
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEmojiFromMessage extracts or determines appropriate emoji for the message
|
||||||
|
func (ss *SlackService) getEmojiFromMessage(message string) string {
|
||||||
|
lowerMsg := strings.ToLower(message)
|
||||||
|
|
||||||
|
// Check for specific service types first
|
||||||
|
if strings.Contains(lowerMsg, "server") {
|
||||||
|
if strings.Contains(lowerMsg, "down") || strings.Contains(lowerMsg, "failed") {
|
||||||
|
return ":red_circle:"
|
||||||
|
} else if strings.Contains(lowerMsg, "warning") {
|
||||||
|
return ":warning:"
|
||||||
|
} else if strings.Contains(lowerMsg, "up") || strings.Contains(lowerMsg, "restored") {
|
||||||
|
return ":white_check_mark:"
|
||||||
|
}
|
||||||
|
return ":desktop_computer:"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSL Certificate notifications
|
||||||
|
if strings.Contains(lowerMsg, "certificate") || strings.Contains(lowerMsg, "ssl") {
|
||||||
|
if strings.Contains(lowerMsg, "expired") {
|
||||||
|
return ":no_entry:"
|
||||||
|
} else if strings.Contains(lowerMsg, "expiring") {
|
||||||
|
return ":warning:"
|
||||||
|
}
|
||||||
|
return ":lock:"
|
||||||
|
}
|
||||||
|
|
||||||
|
// General status-based emojis
|
||||||
|
if strings.Contains(lowerMsg, "down") || strings.Contains(lowerMsg, "failed") {
|
||||||
|
return ":red_circle:"
|
||||||
|
} else if strings.Contains(lowerMsg, "warning") {
|
||||||
|
return ":warning:"
|
||||||
|
} else if strings.Contains(lowerMsg, "up") || strings.Contains(lowerMsg, "restored") {
|
||||||
|
return ":white_check_mark:"
|
||||||
|
} else if strings.Contains(lowerMsg, "maintenance") {
|
||||||
|
return ":construction:"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ":information_source:" // Default info emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateServerMessage creates a message for server notifications using server template
|
||||||
|
func (ss *SlackService) generateServerMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string {
|
||||||
|
var templateMessage string
|
||||||
|
|
||||||
|
// Select appropriate template message based on status and resource type
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "down":
|
||||||
|
templateMessage = template.DownMessage
|
||||||
|
case "up":
|
||||||
|
templateMessage = template.UpMessage
|
||||||
|
case "warning":
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
case "paused":
|
||||||
|
templateMessage = template.PausedMessage
|
||||||
|
default:
|
||||||
|
// Handle resource-specific messages
|
||||||
|
switch resourceType {
|
||||||
|
case "cpu":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreCPUMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.CPUMessage
|
||||||
|
}
|
||||||
|
case "ram", "memory":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreRAMMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.RAMMessage
|
||||||
|
}
|
||||||
|
case "disk":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreDiskMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.DiskMessage
|
||||||
|
}
|
||||||
|
case "network":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreNetworkMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.NetworkMessage
|
||||||
|
}
|
||||||
|
case "cpu_temp", "cpu_temperature":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreCPUTempMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.CPUTempMessage
|
||||||
|
}
|
||||||
|
case "disk_io":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreDiskIOMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.DiskIOMessage
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no template message found, use a default
|
||||||
|
if templateMessage == "" {
|
||||||
|
templateMessage = ss.generateDefaultServerMessage(payload, resourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss.replacePlaceholders(templateMessage, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateServiceMessage creates a message for service notifications using service template
|
||||||
|
func (ss *SlackService) generateServiceMessage(payload *NotificationPayload, template *ServiceNotificationTemplate) string {
|
||||||
|
var templateMessage string
|
||||||
|
|
||||||
|
// Select appropriate template message based on status
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
templateMessage = template.UpMessage
|
||||||
|
case "down":
|
||||||
|
templateMessage = template.DownMessage
|
||||||
|
case "maintenance":
|
||||||
|
templateMessage = template.MaintenanceMessage
|
||||||
|
case "incident":
|
||||||
|
templateMessage = template.IncidentMessage
|
||||||
|
case "resolved":
|
||||||
|
templateMessage = template.ResolvedMessage
|
||||||
|
case "warning":
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
default:
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no template message found, use a default
|
||||||
|
if templateMessage == "" {
|
||||||
|
templateMessage = ss.generateDefaultUptimeMessage(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss.replacePlaceholders(templateMessage, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// replacePlaceholders replaces all placeholders in the message with actual values
|
||||||
|
func (ss *SlackService) replacePlaceholders(message string, payload *NotificationPayload) string {
|
||||||
|
// Replace basic placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
|
||||||
|
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||||
|
message = strings.ReplaceAll(message, "${host}", ss.safeString(payload.Host))
|
||||||
|
message = strings.ReplaceAll(message, "${hostname}", ss.safeString(payload.Hostname))
|
||||||
|
|
||||||
|
// Replace URL with fallback to host
|
||||||
|
url := ss.safeString(payload.URL)
|
||||||
|
if url == "N/A" && payload.Host != "" {
|
||||||
|
url = payload.Host
|
||||||
|
}
|
||||||
|
message = strings.ReplaceAll(message, "${url}", url)
|
||||||
|
|
||||||
|
// Replace domain
|
||||||
|
message = strings.ReplaceAll(message, "${domain}", ss.safeString(payload.Domain))
|
||||||
|
|
||||||
|
// Replace service type
|
||||||
|
if payload.ServiceType != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${service_type}", strings.ToUpper(payload.ServiceType))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${service_type}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace region and agent info
|
||||||
|
message = strings.ReplaceAll(message, "${region_name}", ss.safeString(payload.RegionName))
|
||||||
|
message = strings.ReplaceAll(message, "${agent_id}", ss.safeString(payload.AgentID))
|
||||||
|
|
||||||
|
// Handle numeric fields safely
|
||||||
|
if payload.Port > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${port}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.ResponseTime > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${response_time}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Uptime > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${uptime}", fmt.Sprintf("%d%%", payload.Uptime))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${uptime}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace server monitoring fields
|
||||||
|
message = strings.ReplaceAll(message, "${cpu_usage}", ss.safeString(payload.CPUUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${ram_usage}", ss.safeString(payload.RAMUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${disk_usage}", ss.safeString(payload.DiskUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${network_usage}", ss.safeString(payload.NetworkUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${cpu_temp}", ss.safeString(payload.CPUTemp))
|
||||||
|
message = strings.ReplaceAll(message, "${disk_io}", ss.safeString(payload.DiskIO))
|
||||||
|
message = strings.ReplaceAll(message, "${threshold}", ss.safeString(payload.Threshold))
|
||||||
|
|
||||||
|
// Replace SSL certificate fields
|
||||||
|
message = strings.ReplaceAll(message, "${certificate_name}", ss.safeString(payload.CertificateName))
|
||||||
|
message = strings.ReplaceAll(message, "${expiry_date}", ss.safeString(payload.ExpiryDate))
|
||||||
|
message = strings.ReplaceAll(message, "${days_left}", ss.safeString(payload.DaysLeft))
|
||||||
|
message = strings.ReplaceAll(message, "${issuer_cn}", ss.safeString(payload.IssuerCN))
|
||||||
|
message = strings.ReplaceAll(message, "${serial_number}", ss.safeString(payload.SerialNumber))
|
||||||
|
|
||||||
|
// Replace error message - important for uptime services
|
||||||
|
message = strings.ReplaceAll(message, "${error_message}", ss.safeString(payload.ErrorMessage))
|
||||||
|
message = strings.ReplaceAll(message, "${error}", ss.safeString(payload.ErrorMessage))
|
||||||
|
|
||||||
|
// Replace time placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
message = strings.ReplaceAll(message, "${date}", payload.Timestamp.Format("2006-01-02"))
|
||||||
|
|
||||||
|
// Replace message placeholder
|
||||||
|
if payload.Message != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${message}", payload.Message)
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${message}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeString returns the string value or "N/A" if empty
|
||||||
|
func (ss *SlackService) safeString(value string) string {
|
||||||
|
if value == "" {
|
||||||
|
return "N/A"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDefaultUptimeMessage creates a default uptime message with proper formatting
|
||||||
|
func (ss *SlackService) generateDefaultUptimeMessage(payload *NotificationPayload) string {
|
||||||
|
// Status emoji mapping
|
||||||
|
statusEmoji := "🔵"
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
statusEmoji = "🟢"
|
||||||
|
case "down":
|
||||||
|
statusEmoji = "🔴"
|
||||||
|
case "warning":
|
||||||
|
statusEmoji = "🟡"
|
||||||
|
case "maintenance", "paused":
|
||||||
|
statusEmoji = "🟠"
|
||||||
|
}
|
||||||
|
|
||||||
|
message := fmt.Sprintf("%s Service %s is %s", statusEmoji, payload.ServiceName, strings.ToUpper(payload.Status))
|
||||||
|
|
||||||
|
// Build formatted details
|
||||||
|
details := []string{}
|
||||||
|
|
||||||
|
// Add URL or host
|
||||||
|
if payload.URL != "" {
|
||||||
|
details = append(details, fmt.Sprintf("• Host URL: %s", payload.URL))
|
||||||
|
} else if payload.Host != "" {
|
||||||
|
details = append(details, fmt.Sprintf("• Host: %s", payload.Host))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add service type
|
||||||
|
if payload.ServiceType != "" {
|
||||||
|
details = append(details, fmt.Sprintf("• Type: %s", strings.ToUpper(payload.ServiceType)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add port if available
|
||||||
|
if payload.Port > 0 {
|
||||||
|
details = append(details, fmt.Sprintf("• Port: %d", payload.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add domain if available
|
||||||
|
if payload.Domain != "" {
|
||||||
|
details = append(details, fmt.Sprintf("• Domain: %s", payload.Domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add response time
|
||||||
|
if payload.ResponseTime > 0 {
|
||||||
|
details = append(details, fmt.Sprintf("• Response time: %dms", payload.ResponseTime))
|
||||||
|
} else {
|
||||||
|
details = append(details, "• Response time: N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add region info
|
||||||
|
if payload.RegionName != "" {
|
||||||
|
details = append(details, fmt.Sprintf("• Region: %s", payload.RegionName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add agent info
|
||||||
|
if payload.AgentID != "" {
|
||||||
|
details = append(details, fmt.Sprintf("• Agent: %s", payload.AgentID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add uptime if available
|
||||||
|
if payload.Uptime > 0 {
|
||||||
|
details = append(details, fmt.Sprintf("• Uptime: %d%%", payload.Uptime))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timestamp
|
||||||
|
details = append(details, fmt.Sprintf("• Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05")))
|
||||||
|
|
||||||
|
// Combine message with details
|
||||||
|
if len(details) > 0 {
|
||||||
|
message += "\n" + strings.Join(details, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDefaultServerMessage creates a default server message
|
||||||
|
func (ss *SlackService) generateDefaultServerMessage(payload *NotificationPayload, resourceType string) string {
|
||||||
|
statusEmoji := "🔵"
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
statusEmoji = "🟢"
|
||||||
|
case "down":
|
||||||
|
statusEmoji = "🔴"
|
||||||
|
case "warning":
|
||||||
|
statusEmoji = "🟡"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s 🖥️ Server %s (%s) status: %s", statusEmoji, payload.ServiceName, payload.Hostname, strings.ToUpper(payload.Status))
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"service-operation/pocketbase"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SSLNotificationTemplate represents an SSL notification template
|
||||||
|
type SSLNotificationTemplate struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Expired string `json:"expired"`
|
||||||
|
ExpiringSoon string `json:"exiring_soon"`
|
||||||
|
Warning string `json:"warning"`
|
||||||
|
Placeholder string `json:"placeholder"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSLNotificationManager handles SSL certificate notifications
|
||||||
|
type SSLNotificationManager struct {
|
||||||
|
pbClient *pocketbase.PocketBaseClient
|
||||||
|
services map[string]NotificationService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSSLNotificationManager creates a new SSL notification manager
|
||||||
|
func NewSSLNotificationManager(pbClient *pocketbase.PocketBaseClient, services map[string]NotificationService) *SSLNotificationManager {
|
||||||
|
return &SSLNotificationManager{
|
||||||
|
pbClient: pbClient,
|
||||||
|
services: services,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSSLNotification sends notification for SSL certificate status
|
||||||
|
func (snm *SSLNotificationManager) SendSSLNotification(payload *NotificationPayload, notificationID, templateID string) error {
|
||||||
|
// log.Printf("📨 [SSL-MANAGER] IMMEDIATE send for certificate: %s, status: %s", payload.Domain, payload.Status)
|
||||||
|
// log.Printf("📨 [SSL-MANAGER] Notification ID: %s, Template ID: %s", notificationID, templateID)
|
||||||
|
|
||||||
|
if notificationID == "" {
|
||||||
|
return fmt.Errorf("notification ID required for SSL certificate: %s", payload.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse notification IDs
|
||||||
|
notificationIDs := parseNotificationIDs(notificationID)
|
||||||
|
if len(notificationIDs) == 0 {
|
||||||
|
return fmt.Errorf("no valid notification IDs for SSL certificate: %s", payload.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors []string
|
||||||
|
successCount := 0
|
||||||
|
|
||||||
|
// Send to each notification channel IMMEDIATELY
|
||||||
|
for _, id := range notificationIDs {
|
||||||
|
// log.Printf("📤 [SSL-SEND] Processing notification ID: %s for certificate %s", id, payload.Domain)
|
||||||
|
|
||||||
|
// Check if enabled
|
||||||
|
if !isNotificationEnabled(snm.pbClient, id) {
|
||||||
|
// log.Printf("⚠️ Notification %s disabled for SSL certificate %s, skipping", id, payload.Domain)
|
||||||
|
_ = id
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get alert configuration
|
||||||
|
alertConfig, err := getAlertConfiguration(snm.pbClient, id)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ Failed to get alert config for %s (certificate: %s): %v", id, payload.Domain, err)
|
||||||
|
errors = append(errors, fmt.Sprintf("config error %s: %v", id, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Printf("📋 [SSL-CONFIG] Alert config for %s: Type=%s, ChatID=%s, Token present=%v",
|
||||||
|
// id, alertConfig.NotificationType, alertConfig.TelegramChatID, alertConfig.BotToken != "")
|
||||||
|
|
||||||
|
// Get SSL template
|
||||||
|
var sslTemplate *SSLNotificationTemplate
|
||||||
|
if templateID != "" {
|
||||||
|
sslTemplate, err = snm.getSSLNotificationTemplate(templateID)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("⚠️ Template error for %s (certificate: %s): %v", templateID, payload.Domain, err)
|
||||||
|
_ = err
|
||||||
|
} else {
|
||||||
|
// log.Printf("📄 [SSL-TEMPLATE] Retrieved template: Expired=%s, ExpiringSoon=%s, Warning=%s",
|
||||||
|
// sslTemplate.Expired, sslTemplate.ExpiringSoon, sslTemplate.Warning)
|
||||||
|
_ = sslTemplate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate message
|
||||||
|
message := snm.generateSSLMessage(payload, sslTemplate)
|
||||||
|
// log.Printf("📝 [SSL-MESSAGE] Generated for %s (%s): %s", payload.Domain, id, message)
|
||||||
|
|
||||||
|
// Get notification service
|
||||||
|
service, exists := snm.services[alertConfig.NotificationType]
|
||||||
|
if !exists {
|
||||||
|
// log.Printf("❌ Unsupported notification type for SSL: %s", alertConfig.NotificationType)
|
||||||
|
errors = append(errors, fmt.Sprintf("unsupported type %s", alertConfig.NotificationType))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEND IMMEDIATELY - NO DELAYS
|
||||||
|
// log.Printf("⚡ [SSL-TELEGRAM] Sending via %s for SSL certificate %s", alertConfig.NotificationType, payload.Domain)
|
||||||
|
err = service.SendNotification(alertConfig, message)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ [SSL-FAILED] Failed to send via %s for %s: %v", alertConfig.NotificationType, payload.Domain, err)
|
||||||
|
errors = append(errors, fmt.Sprintf("send failed %s: %v", alertConfig.NotificationType, err))
|
||||||
|
} else {
|
||||||
|
// log.Printf("✅ [SSL-SUCCESS] Successfully sent via %s for %s", alertConfig.NotificationType, payload.Domain)
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = alertConfig
|
||||||
|
_ = message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report results
|
||||||
|
if successCount > 0 {
|
||||||
|
// log.Printf("✅ [SSL-FINAL] Sent %d/%d SSL notifications for %s", successCount, len(notificationIDs), payload.Domain)
|
||||||
|
_ = successCount
|
||||||
|
_ = notificationIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 && successCount == 0 {
|
||||||
|
return fmt.Errorf("all SSL notifications failed for %s: %v", payload.Domain, errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSSLNotificationTemplate fetches SSL notification template from PocketBase
|
||||||
|
func (snm *SSLNotificationManager) getSSLNotificationTemplate(templateID string) (*SSLNotificationTemplate, error) {
|
||||||
|
url := fmt.Sprintf("%s/api/collections/ssl_notification_templates/records/%s", snm.pbClient.GetBaseURL(), templateID)
|
||||||
|
// log.Printf("🌐 Fetching SSL notification template from: %s", url)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("❌ HTTP error fetching SSL notification template: %v", err)
|
||||||
|
_ = err
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
// log.Printf("❌ Failed to fetch SSL notification template, status: %d", resp.StatusCode)
|
||||||
|
return nil, fmt.Errorf("failed to fetch SSL notification template, status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var template SSLNotificationTemplate
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&template); err != nil {
|
||||||
|
// log.Printf("❌ Error decoding SSL notification template JSON: %v", err)
|
||||||
|
_ = err
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Printf("✅ Successfully fetched SSL notification template: %+v", template)
|
||||||
|
_ = url
|
||||||
|
_ = template
|
||||||
|
return &template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSSLMessage creates notification message for SSL certificates
|
||||||
|
func (snm *SSLNotificationManager) generateSSLMessage(payload *NotificationPayload, template *SSLNotificationTemplate) string {
|
||||||
|
var baseMessage string
|
||||||
|
|
||||||
|
// Use template if available
|
||||||
|
if template != nil {
|
||||||
|
// log.Printf("🔧 Using SSL template for status: %s", strings.ToLower(payload.Status))
|
||||||
|
// log.Printf("🔧 [SSL-TEMPLATE-DEBUG] Available templates - Expired: '%s', ExpiringSoon: '%s', Warning: '%s'",
|
||||||
|
// template.Expired, template.ExpiringSoon, template.Warning)
|
||||||
|
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "expired":
|
||||||
|
baseMessage = template.Expired
|
||||||
|
// log.Printf("🔧 [SSL-EXPIRED] Selected expired template: '%s'", baseMessage)
|
||||||
|
case "expiring_soon":
|
||||||
|
baseMessage = template.ExpiringSoon
|
||||||
|
// log.Printf("🔧 [SSL-EXPIRING] Selected expiring soon template: '%s'", baseMessage)
|
||||||
|
case "warning":
|
||||||
|
baseMessage = template.Warning
|
||||||
|
// log.Printf("🔧 [SSL-WARNING] Selected warning template: '%s'", baseMessage)
|
||||||
|
default:
|
||||||
|
baseMessage = template.Warning
|
||||||
|
// log.Printf("🔧 [SSL-DEFAULT] Using warning template for status '%s': '%s'", payload.Status, baseMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = baseMessage
|
||||||
|
_ = template
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use default if no template or template message is empty
|
||||||
|
if baseMessage == "" {
|
||||||
|
// log.Printf("🔧 Using default SSL message (no template or empty template)")
|
||||||
|
baseMessage = snm.getDefaultSSLMessage(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace placeholders
|
||||||
|
message := snm.replaceSSLPlaceholders(baseMessage, payload)
|
||||||
|
|
||||||
|
// log.Printf("📝 Final SSL message: %s", message)
|
||||||
|
_ = message
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// replaceSSLPlaceholders replaces all placeholders in the SSL message
|
||||||
|
func (snm *SSLNotificationManager) replaceSSLPlaceholders(message string, payload *NotificationPayload) string {
|
||||||
|
// log.Printf("🔄 [SSL-REPLACE] Before replacement: %s", message)
|
||||||
|
// log.Printf("🔄 [SSL-DATA] Payload data - Domain: %s, ExpiryDate: %s, DaysLeft: %s, IssuerCN: %s",
|
||||||
|
// payload.Domain, payload.ExpiryDate, payload.DaysLeft, payload.IssuerCN)
|
||||||
|
|
||||||
|
// SSL Certificate specific placeholders - ensure all are replaced
|
||||||
|
message = strings.ReplaceAll(message, "${domain}", snm.safeString(payload.Domain))
|
||||||
|
message = strings.ReplaceAll(message, "${certificate_name}", snm.safeString(payload.CertificateName))
|
||||||
|
message = strings.ReplaceAll(message, "${expiry_date}", snm.safeString(payload.ExpiryDate))
|
||||||
|
message = strings.ReplaceAll(message, "${days_left}", snm.safeString(payload.DaysLeft))
|
||||||
|
message = strings.ReplaceAll(message, "${issuer_cn}", snm.safeString(payload.IssuerCN))
|
||||||
|
message = strings.ReplaceAll(message, "${serial_number}", snm.safeString(payload.SerialNumber))
|
||||||
|
|
||||||
|
// Basic placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||||
|
message = strings.ReplaceAll(message, "${service_name}", snm.safeString(payload.ServiceName))
|
||||||
|
message = strings.ReplaceAll(message, "${host}", snm.safeString(payload.Host))
|
||||||
|
|
||||||
|
// Time placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("15:04:05"))
|
||||||
|
message = strings.ReplaceAll(message, "${date}", payload.Timestamp.Format("2006-01-02"))
|
||||||
|
|
||||||
|
// Message placeholder
|
||||||
|
if payload.Message != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${message}", payload.Message)
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${message}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Printf("🔄 [SSL-REPLACE] After replacement: %s", message)
|
||||||
|
_ = payload
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeString returns the string value or "N/A" if empty
|
||||||
|
func (snm *SSLNotificationManager) safeString(value string) string {
|
||||||
|
if value == "" {
|
||||||
|
return "N/A"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultSSLMessage provides a default notification message for SSL certificates
|
||||||
|
func (snm *SSLNotificationManager) getDefaultSSLMessage(payload *NotificationPayload) string {
|
||||||
|
statusEmoji := "🔒"
|
||||||
|
if payload.Status == "expired" {
|
||||||
|
statusEmoji = "🚨"
|
||||||
|
} else if payload.Status == "expiring_soon" {
|
||||||
|
statusEmoji = "⚠️"
|
||||||
|
} else if payload.Status == "warning" {
|
||||||
|
statusEmoji = "🔔"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the default message with all SSL details
|
||||||
|
message := fmt.Sprintf("%s SSL certificate for %s has %s", statusEmoji, payload.Domain, strings.ToUpper(payload.Status))
|
||||||
|
|
||||||
|
// Add certificate details if available
|
||||||
|
if payload.CertificateName != "" && payload.CertificateName != payload.Domain {
|
||||||
|
message += fmt.Sprintf("\n • Certs Name: %s", payload.CertificateName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.ExpiryDate != "" {
|
||||||
|
message += fmt.Sprintf("\n • Expiry Date: %s", payload.ExpiryDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.DaysLeft != "" {
|
||||||
|
message += fmt.Sprintf("\n • Days Left: %s", payload.DaysLeft)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.IssuerCN != "" {
|
||||||
|
message += fmt.Sprintf("\n • Issuer: %s", payload.IssuerCN)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timestamp
|
||||||
|
message += fmt.Sprintf("\n • Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TelegramService handles Telegram notifications
|
||||||
|
type TelegramService struct{}
|
||||||
|
|
||||||
|
// NewTelegramService creates a new Telegram notification service
|
||||||
|
func NewTelegramService() *TelegramService {
|
||||||
|
return &TelegramService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TelegramPayload represents the payload for Telegram API
|
||||||
|
type TelegramPayload struct {
|
||||||
|
ChatID string `json:"chat_id"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendNotification sends a notification via Telegram
|
||||||
|
func (ts *TelegramService) SendNotification(config *AlertConfiguration, message string) error {
|
||||||
|
// fmt.Printf("📱 [TELEGRAM] Attempting to send notification...\n")
|
||||||
|
// fmt.Printf("📱 [TELEGRAM] Config - Chat ID: %s, Bot Token present: %v\n", config.TelegramChatID, config.BotToken != "")
|
||||||
|
// fmt.Printf("📱 [TELEGRAM] Message: %s\n", message)
|
||||||
|
|
||||||
|
if config.BotToken == "" || config.TelegramChatID == "" {
|
||||||
|
return fmt.Errorf("telegram bot token and chat ID are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", config.BotToken)
|
||||||
|
// fmt.Printf("📱 [TELEGRAM] API URL: %s\n", strings.Replace(url, config.BotToken, "[REDACTED]", 1))
|
||||||
|
|
||||||
|
payload := TelegramPayload{
|
||||||
|
ChatID: config.TelegramChatID,
|
||||||
|
Text: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("📱 [TELEGRAM] Sending POST request...\n")
|
||||||
|
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
// fmt.Printf("❌ [TELEGRAM] HTTP error: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result NotificationResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
// fmt.Printf("❌ [TELEGRAM] JSON decode error: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("📱 [TELEGRAM] Response - OK: %v, Description: %s\n", result.OK, result.Description)
|
||||||
|
|
||||||
|
if !result.OK {
|
||||||
|
// fmt.Printf("❌ [TELEGRAM] API error: %s\n", result.Description)
|
||||||
|
return fmt.Errorf("telegram API error: %s", result.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("✅ [TELEGRAM] Message sent successfully!\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendServerNotification sends a server-specific notification via Telegram
|
||||||
|
func (ts *TelegramService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error {
|
||||||
|
message := ts.generateServerMessage(payload, template, resourceType)
|
||||||
|
return ts.SendNotification(config, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendServiceNotification sends a service-specific notification via Telegram
|
||||||
|
func (ts *TelegramService) SendServiceNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServiceNotificationTemplate) error {
|
||||||
|
message := ts.generateServiceMessage(payload, template)
|
||||||
|
return ts.SendNotification(config, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateServerMessage creates a message for server notifications using server template
|
||||||
|
func (ts *TelegramService) generateServerMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string {
|
||||||
|
var templateMessage string
|
||||||
|
|
||||||
|
// Select appropriate template message based on status and resource type
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "down":
|
||||||
|
templateMessage = template.DownMessage
|
||||||
|
case "up":
|
||||||
|
templateMessage = template.UpMessage
|
||||||
|
case "warning":
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
case "paused":
|
||||||
|
templateMessage = template.PausedMessage
|
||||||
|
default:
|
||||||
|
// Handle resource-specific messages
|
||||||
|
switch resourceType {
|
||||||
|
case "cpu":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreCPUMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.CPUMessage
|
||||||
|
}
|
||||||
|
case "ram", "memory":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreRAMMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.RAMMessage
|
||||||
|
}
|
||||||
|
case "disk":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreDiskMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.DiskMessage
|
||||||
|
}
|
||||||
|
case "network":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreNetworkMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.NetworkMessage
|
||||||
|
}
|
||||||
|
case "cpu_temp", "cpu_temperature":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreCPUTempMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.CPUTempMessage
|
||||||
|
}
|
||||||
|
case "disk_io":
|
||||||
|
if strings.Contains(strings.ToLower(payload.Status), "restore") {
|
||||||
|
templateMessage = template.RestoreDiskIOMessage
|
||||||
|
} else {
|
||||||
|
templateMessage = template.DiskIOMessage
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no template message found, use a default
|
||||||
|
if templateMessage == "" {
|
||||||
|
templateMessage = ts.generateDefaultServerMessage(payload, resourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts.replacePlaceholders(templateMessage, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateServiceMessage creates a message for service notifications using service template
|
||||||
|
func (ts *TelegramService) generateServiceMessage(payload *NotificationPayload, template *ServiceNotificationTemplate) string {
|
||||||
|
var templateMessage string
|
||||||
|
|
||||||
|
// Select appropriate template message based on status
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
templateMessage = template.UpMessage
|
||||||
|
case "down":
|
||||||
|
templateMessage = template.DownMessage
|
||||||
|
case "maintenance":
|
||||||
|
templateMessage = template.MaintenanceMessage
|
||||||
|
case "incident":
|
||||||
|
templateMessage = template.IncidentMessage
|
||||||
|
case "resolved":
|
||||||
|
templateMessage = template.ResolvedMessage
|
||||||
|
case "warning":
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
default:
|
||||||
|
templateMessage = template.WarningMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no template message found, use a default
|
||||||
|
if templateMessage == "" {
|
||||||
|
templateMessage = ts.generateDefaultUptimeMessage(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts.replacePlaceholders(templateMessage, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// replacePlaceholders replaces all placeholders in the message with actual values
|
||||||
|
func (ts *TelegramService) replacePlaceholders(message string, payload *NotificationPayload) string {
|
||||||
|
// Replace basic placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
|
||||||
|
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
|
||||||
|
message = strings.ReplaceAll(message, "${host}", ts.safeString(payload.Host))
|
||||||
|
message = strings.ReplaceAll(message, "${hostname}", ts.safeString(payload.Hostname))
|
||||||
|
|
||||||
|
// Replace URL with fallback to host
|
||||||
|
url := ts.safeString(payload.URL)
|
||||||
|
if url == "N/A" && payload.Host != "" {
|
||||||
|
url = payload.Host
|
||||||
|
}
|
||||||
|
message = strings.ReplaceAll(message, "${url}", url)
|
||||||
|
|
||||||
|
// Replace domain
|
||||||
|
message = strings.ReplaceAll(message, "${domain}", ts.safeString(payload.Domain))
|
||||||
|
|
||||||
|
// Replace service type
|
||||||
|
if payload.ServiceType != "" {
|
||||||
|
message = strings.ReplaceAll(message, "${service_type}", strings.ToUpper(payload.ServiceType))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${service_type}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace region and agent info
|
||||||
|
message = strings.ReplaceAll(message, "${region_name}", ts.safeString(payload.RegionName))
|
||||||
|
message = strings.ReplaceAll(message, "${agent_id}", ts.safeString(payload.AgentID))
|
||||||
|
|
||||||
|
// Handle numeric fields safely
|
||||||
|
if payload.Port > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${port}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.ResponseTime > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${response_time}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Uptime > 0 {
|
||||||
|
message = strings.ReplaceAll(message, "${uptime}", fmt.Sprintf("%d%%", payload.Uptime))
|
||||||
|
} else {
|
||||||
|
message = strings.ReplaceAll(message, "${uptime}", "N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace server monitoring fields
|
||||||
|
message = strings.ReplaceAll(message, "${cpu_usage}", ts.safeString(payload.CPUUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${ram_usage}", ts.safeString(payload.RAMUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${disk_usage}", ts.safeString(payload.DiskUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${network_usage}", ts.safeString(payload.NetworkUsage))
|
||||||
|
message = strings.ReplaceAll(message, "${cpu_temp}", ts.safeString(payload.CPUTemp))
|
||||||
|
message = strings.ReplaceAll(message, "${disk_io}", ts.safeString(payload.DiskIO))
|
||||||
|
message = strings.ReplaceAll(message, "${threshold}", ts.safeString(payload.Threshold))
|
||||||
|
|
||||||
|
// Replace error message - important for uptime services
|
||||||
|
message = strings.ReplaceAll(message, "${error_message}", ts.safeString(payload.ErrorMessage))
|
||||||
|
message = strings.ReplaceAll(message, "${error}", ts.safeString(payload.ErrorMessage))
|
||||||
|
|
||||||
|
// Replace time placeholders
|
||||||
|
message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeString returns the string value or "N/A" if empty
|
||||||
|
func (ts *TelegramService) safeString(value string) string {
|
||||||
|
if value == "" {
|
||||||
|
return "N/A"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDefaultUptimeMessage creates a default uptime message with proper formatting
|
||||||
|
func (ts *TelegramService) generateDefaultUptimeMessage(payload *NotificationPayload) string {
|
||||||
|
// Status emoji mapping
|
||||||
|
statusEmoji := "🔵"
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
statusEmoji = "🟢"
|
||||||
|
case "down":
|
||||||
|
statusEmoji = "🔴"
|
||||||
|
case "warning":
|
||||||
|
statusEmoji = "🟡"
|
||||||
|
case "maintenance", "paused":
|
||||||
|
statusEmoji = "🟠"
|
||||||
|
}
|
||||||
|
|
||||||
|
message := fmt.Sprintf("%sService %s is %s.", statusEmoji, payload.ServiceName, strings.ToUpper(payload.Status))
|
||||||
|
|
||||||
|
// Build formatted details
|
||||||
|
details := []string{}
|
||||||
|
|
||||||
|
// Add URL or host
|
||||||
|
if payload.URL != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Host URL: %s", payload.URL))
|
||||||
|
} else if payload.Host != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Host: %s", payload.Host))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add service type
|
||||||
|
if payload.ServiceType != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Type: %s", strings.ToUpper(payload.ServiceType)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add port if available
|
||||||
|
if payload.Port > 0 {
|
||||||
|
details = append(details, fmt.Sprintf(" - Port: %d", payload.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add domain if available
|
||||||
|
if payload.Domain != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Domain: %s", payload.Domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add response time
|
||||||
|
if payload.ResponseTime > 0 {
|
||||||
|
details = append(details, fmt.Sprintf(" - Response time: %dms", payload.ResponseTime))
|
||||||
|
} else {
|
||||||
|
details = append(details, " - Response time: N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add region info
|
||||||
|
if payload.RegionName != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Region: %s", payload.RegionName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add agent info
|
||||||
|
if payload.AgentID != "" {
|
||||||
|
details = append(details, fmt.Sprintf(" - Agent: %s", payload.AgentID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add uptime if available
|
||||||
|
if payload.Uptime > 0 {
|
||||||
|
details = append(details, fmt.Sprintf(" - Uptime: %d%%", payload.Uptime))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timestamp
|
||||||
|
details = append(details, fmt.Sprintf(" - Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05")))
|
||||||
|
|
||||||
|
// Combine message with details
|
||||||
|
if len(details) > 0 {
|
||||||
|
message += "\n" + strings.Join(details, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDefaultServerMessage creates a default server message
|
||||||
|
func (ts *TelegramService) generateDefaultServerMessage(payload *NotificationPayload, resourceType string) string {
|
||||||
|
statusEmoji := "🔵"
|
||||||
|
switch strings.ToLower(payload.Status) {
|
||||||
|
case "up":
|
||||||
|
statusEmoji = "🟢"
|
||||||
|
case "down":
|
||||||
|
statusEmoji = "🔴"
|
||||||
|
case "warning":
|
||||||
|
statusEmoji = "🟡"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s🖥️ Server %s (%s) status: %s", statusEmoji, payload.ServiceName, payload.Hostname, strings.ToUpper(payload.Status))
|
||||||
|
}
|
||||||