"use client" import { useState, useEffect, useCallback } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card" import { Tabs, TabsList, TabsTrigger, TabsContent } from "./ui/tabs" import { Input } from "./ui/input" import { Label } from "./ui/label" import { Badge } from "./ui/badge" import { Checkbox } from "./ui/checkbox" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { fetchApi } from "../lib/api-config" import { Bell, BellOff, Send, CheckCircle2, XCircle, Loader2, AlertTriangle, Info, Settings2, Zap, Eye, EyeOff, Trash2, ChevronDown, ChevronUp, TestTube2, Mail, Webhook } from "lucide-react" interface ChannelConfig { enabled: boolean bot_token?: string chat_id?: string url?: string token?: string webhook_url?: string // Email channel fields host?: string port?: string username?: string password?: string tls_mode?: string from_address?: string to_addresses?: string subject_prefix?: string } interface NotificationConfig { enabled: boolean channels: Record severity_filter: string event_categories: Record ai_enabled: boolean ai_provider: string ai_api_key: string ai_model: string hostname: string webhook_secret: string webhook_allowed_ips: string } interface ServiceStatus { enabled: boolean running: boolean channels: Record queue_size: number last_sent: string | null total_sent_24h: number } interface HistoryEntry { id: number event_type: string channel: string title: string severity: string sent_at: string success: boolean error_message: string | null } const SEVERITY_OPTIONS = [ { value: "critical", label: "Critical only" }, { value: "warning", label: "Warning + Critical" }, { value: "info", label: "All (Info + Warning + Critical)" }, ] const EVENT_CATEGORIES = [ { key: "system", label: "System", desc: "Startup, shutdown, kernel events" }, { key: "vm_ct", label: "VM / CT", desc: "Start, stop, crash, migration" }, { key: "backup", label: "Backups", desc: "Backup start, complete, fail" }, { key: "resources", label: "Resources", desc: "CPU, memory, temperature" }, { key: "storage", label: "Storage", desc: "Disk space, I/O errors, SMART" }, { key: "network", label: "Network", desc: "Connectivity, bond, latency" }, { key: "security", label: "Security", desc: "Auth failures, fail2ban, firewall" }, { key: "cluster", label: "Cluster", desc: "Quorum, split-brain, HA fencing" }, ] const AI_PROVIDERS = [ { value: "openai", label: "OpenAI" }, { value: "groq", label: "Groq" }, ] const DEFAULT_CONFIG: NotificationConfig = { enabled: false, channels: { telegram: { enabled: false }, gotify: { enabled: false }, discord: { enabled: false }, email: { enabled: false }, }, severity_filter: "warning", event_categories: { system: true, vm_ct: true, backup: true, resources: true, storage: true, network: true, security: true, cluster: true, }, ai_enabled: false, ai_provider: "openai", ai_api_key: "", ai_model: "", hostname: "", webhook_secret: "", webhook_allowed_ips: "", } export function NotificationSettings() { const [config, setConfig] = useState(DEFAULT_CONFIG) const [status, setStatus] = useState(null) const [history, setHistory] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) const [testing, setTesting] = useState(null) const [testResult, setTestResult] = useState<{ channel: string; success: boolean; message: string } | null>(null) const [showHistory, setShowHistory] = useState(false) const [showAdvanced, setShowAdvanced] = useState(false) const [showSecrets, setShowSecrets] = useState>({}) const [editMode, setEditMode] = useState(false) const [hasChanges, setHasChanges] = useState(false) const [originalConfig, setOriginalConfig] = useState(DEFAULT_CONFIG) const [webhookSetup, setWebhookSetup] = useState<{ status: "idle" | "running" | "success" | "failed" fallback_commands: string[] error: string }>({ status: "idle", fallback_commands: [], error: "" }) const loadConfig = useCallback(async () => { try { const data = await fetchApi<{ success: boolean; config: NotificationConfig }>("/api/notifications/settings") if (data.success && data.config) { setConfig(data.config) setOriginalConfig(data.config) } } catch (err) { console.error("Failed to load notification settings:", err) } finally { setLoading(false) } }, []) const loadStatus = useCallback(async () => { try { const data = await fetchApi<{ success: boolean } & ServiceStatus>("/api/notifications/status") if (data.success) { setStatus(data) } } catch { // Service may not be running yet } }, []) const loadHistory = useCallback(async () => { try { const data = await fetchApi<{ success: boolean; history: HistoryEntry[]; total: number }>("/api/notifications/history?limit=20") if (data.success) { setHistory(data.history || []) } } catch { // Ignore } }, []) useEffect(() => { loadConfig() loadStatus() }, [loadConfig, loadStatus]) useEffect(() => { if (showHistory) loadHistory() }, [showHistory, loadHistory]) const updateConfig = (updater: (prev: NotificationConfig) => NotificationConfig) => { setConfig(prev => { const next = updater(prev) setHasChanges(true) return next }) } const updateChannel = (channel: string, field: string, value: string | boolean) => { updateConfig(prev => ({ ...prev, channels: { ...prev.channels, [channel]: { ...prev.channels[channel], [field]: value }, }, })) } const handleSave = async () => { setSaving(true) try { await fetchApi("/api/notifications/settings", { method: "POST", body: JSON.stringify(config), }) setOriginalConfig(config) setHasChanges(false) setEditMode(false) setSaved(true) setTimeout(() => setSaved(false), 3000) loadStatus() } catch (err) { console.error("Failed to save notification settings:", err) } finally { setSaving(false) } } const handleCancel = () => { setConfig(originalConfig) setHasChanges(false) setEditMode(false) } const handleTest = async (channel: string) => { setTesting(channel) setTestResult(null) try { const data = await fetchApi<{ success: boolean; message: string }>("/api/notifications/test", { method: "POST", body: JSON.stringify({ channel }), }) setTestResult({ channel, success: data.success, message: data.message }) } catch (err) { setTestResult({ channel, success: false, message: String(err) }) } finally { setTesting(null) setTimeout(() => setTestResult(null), 5000) } } const handleClearHistory = async () => { try { await fetchApi("/api/notifications/history", { method: "DELETE" }) setHistory([]) } catch { // Ignore } } const toggleSecret = (key: string) => { setShowSecrets(prev => ({ ...prev, [key]: !prev[key] })) } if (loading) { return (
Notifications
) } const activeChannels = Object.entries(config.channels).filter(([, ch]) => ch.enabled).length const handleEnable = async () => { setSaving(true) setWebhookSetup({ status: "running", fallback_commands: [], error: "" }) try { // 1) Save enabled=true const newConfig = { ...config, enabled: true } await fetchApi("/api/notifications/settings", { method: "POST", body: JSON.stringify(newConfig), }) setConfig(newConfig) setOriginalConfig(newConfig) // 2) Auto-configure PVE webhook try { const setup = await fetchApi<{ configured: boolean secret?: string fallback_commands?: string[] error?: string }>("/api/notifications/proxmox/setup-webhook", { method: "POST" }) if (setup.configured) { setWebhookSetup({ status: "success", fallback_commands: [], error: "" }) // Update secret in local config if one was generated if (setup.secret) { const updated = { ...newConfig, webhook_secret: setup.secret } setConfig(updated) setOriginalConfig(updated) } } else { setWebhookSetup({ status: "failed", fallback_commands: setup.fallback_commands || [], error: setup.error || "Unknown error", }) } } catch { setWebhookSetup({ status: "failed", fallback_commands: [], error: "Could not reach setup endpoint", }) } setEditMode(true) loadStatus() } catch (err) { console.error("Failed to enable notifications:", err) setWebhookSetup({ status: "idle", fallback_commands: [], error: "" }) } finally { setSaving(false) } } // ── Disabled state: show activation card ── if (!config.enabled && !editMode) { return (
Notifications Disabled
Get real-time alerts about your Proxmox environment via Telegram, Discord, Gotify, or Email.

Enable notification service

Monitor system health, VM/CT events, backups, security alerts, and cluster status. PVE webhook integration is configured automatically.

{/* Webhook setup result */} {webhookSetup.status === "success" && (

PVE webhook configured automatically. Proxmox will send notifications to ProxMenux.

)} {webhookSetup.status === "failed" && (

Automatic PVE configuration failed: {webhookSetup.error}

Notifications are enabled. Run the commands below on the PVE host to complete webhook setup.

{webhookSetup.fallback_commands.length > 0 && (
{webhookSetup.fallback_commands.join('\n')}
                    
)}
)}
{/* PBS manual section (collapsible) */}
Configure PBS notifications (manual)

PVE backups launched from the PVE interface are covered automatically by the PVE webhook above.

However, PBS has its own internal jobs (Verify, Prune, GC, Sync) that generate separate notifications. These must be configured directly on the PBS server.

Add to /etc/proxmox-backup/notifications.cfg on the PBS host:

{`webhook: proxmenux-webhook
\turl http://:8008/api/notifications/webhook
\tmethod post
\theader Content-Type:application/json
\theader X-Webhook-Secret:{{ secrets.proxmenux_secret }}

matcher: proxmenux-pbs
\ttarget proxmenux-webhook
\tmatch-severity warning,error`}
                  

Add to /etc/proxmox-backup/notifications-priv.cfg:

{`webhook: proxmenux-webhook
\tsecret proxmenux_secret `}
                  

{"Replace with the IP of this PVE node (not 127.0.0.1, unless PBS runs on the same host)."}

{"Replace with the webhook secret shown in your notification settings."}

) } return (
Notifications {config.enabled && ( Active )}
{saved && ( Saved )} {editMode ? ( <> ) : ( )}
Configure notification channels and event filters. Receive alerts via Telegram, Gotify, Discord, or Email.
{/* ── Service Status ── */} {status && (
{status.running ? "Service running" : "Service stopped"} {status.total_sent_24h > 0 && ( {status.total_sent_24h} sent in last 24h )}
{activeChannels > 0 && ( {activeChannels} channel{activeChannels > 1 ? "s" : ""} )}
)} {/* ── Enable/Disable ── */}
{config.enabled ? ( ) : ( )}
Enable Notifications

Activate the notification service

{config.enabled && ( <> {/* ── Channel Configuration ── */}
Channels
Telegram Gotify Discord Email {/* Telegram */}
{config.channels.telegram?.enabled && ( <>
updateChannel("telegram", "bot_token", e.target.value)} disabled={!editMode} />
updateChannel("telegram", "chat_id", e.target.value)} disabled={!editMode} />
{!editMode && config.channels.telegram?.bot_token && ( )} )}
{/* Gotify */}
{config.channels.gotify?.enabled && ( <>
updateChannel("gotify", "url", e.target.value)} disabled={!editMode} />
updateChannel("gotify", "token", e.target.value)} disabled={!editMode} />
{!editMode && config.channels.gotify?.url && ( )} )}
{/* Discord */}
{config.channels.discord?.enabled && ( <>
updateChannel("discord", "webhook_url", e.target.value)} disabled={!editMode} />
{!editMode && config.channels.discord?.webhook_url && ( )} )}
{/* Email */}
{config.channels.email?.enabled && ( <>
updateChannel("email", "host", e.target.value)} disabled={!editMode} />
updateChannel("email", "port", e.target.value)} disabled={!editMode} />
updateChannel("email", "username", e.target.value)} disabled={!editMode} />
updateChannel("email", "password", e.target.value)} disabled={!editMode} />
updateChannel("email", "from_address", e.target.value)} disabled={!editMode} />
updateChannel("email", "to_addresses", e.target.value)} disabled={!editMode} />
updateChannel("email", "subject_prefix", e.target.value)} disabled={!editMode} />

Leave SMTP Host empty to use local sendmail (must be installed on the server). For Gmail, use an App Password instead of your account password.

{!editMode && config.channels.email?.to_addresses && ( )} )}
{/* Test Result */} {testResult && (
{testResult.success ? ( ) : ( )} {testResult.message}
)}
{/* ── Severity Filter ── */}
Severity Filter
{/* ── Event Categories ── */}
Event Categories
{EVENT_CATEGORIES.map(cat => ( ))}
{/* ── Proxmox Webhook ── */}
Proxmox Webhook
{!editMode && ( )}
{/* Setup status inline */} {webhookSetup.status === "success" && (

PVE webhook configured successfully.

)} {webhookSetup.status === "failed" && (

PVE auto-config failed: {webhookSetup.error}

{webhookSetup.fallback_commands.length > 0 && (
{webhookSetup.fallback_commands.join('\n')}
                    
)}
)}
updateConfig(p => ({ ...p, webhook_secret: e.target.value }))} disabled={!editMode} />

{"Proxmox must send this value in the X-Webhook-Secret header. Auto-generated on first enable."}

updateConfig(p => ({ ...p, webhook_allowed_ips: e.target.value }))} disabled={!editMode} />

{"Localhost (127.0.0.1) is always allowed. This restricts remote callers only."}

{/* PBS manual guide (collapsible) */}
Configure PBS notifications (manual)

Backups launched from PVE are covered by the PVE webhook. PBS internal jobs (Verify, Prune, GC, Sync) require separate configuration on the PBS server.

Add to /etc/proxmox-backup/notifications.cfg:

{`webhook: proxmenux-webhook
\turl http://:8008/api/notifications/webhook
\tmethod post
\theader Content-Type:application/json
\theader X-Webhook-Secret:{{ secrets.proxmenux_secret }}

matcher: proxmenux-pbs
\ttarget proxmenux-webhook
\tmatch-severity warning,error`}
                  

Add to /etc/proxmox-backup/notifications-priv.cfg:

{`webhook: proxmenux-webhook
\tsecret proxmenux_secret `}
                  

{"Replace with this node's IP and with the webhook secret above."}

{/* ── Advanced: AI Enhancement ── */}
{showAdvanced && (
AI-Enhanced Messages

Use AI to generate contextual notification messages

{config.ai_enabled && ( <>
updateConfig(p => ({ ...p, ai_api_key: e.target.value }))} disabled={!editMode} />
updateConfig(p => ({ ...p, ai_model: e.target.value }))} disabled={!editMode} />

AI enhancement is optional. When enabled, notifications include contextual analysis and recommended actions. If the AI service is unavailable, standard templates are used as fallback.

)}
)}
{/* ── Notification History ── */}
{showHistory && (
{history.length === 0 ? (

No notifications sent yet

) : ( <>
{history.map(entry => (
{entry.success ? ( ) : ( )}
{entry.title || entry.event_type} {entry.channel} - {new Date(entry.sent_at).toLocaleString()}
{entry.severity}
))}
)}
)}
)} {/* ── Footer info ── */}

{config.enabled ? "Notifications are active. Events matching your severity filter and category selection will be sent to configured channels." : "Enable notifications to receive alerts about system events, health status changes, and security incidents via Telegram, Gotify, Discord, or Email."}

) }