"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, Copy, Server, Shield } 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 pbs_host: string pve_host: string pbs_trusted_sources: 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: "", pbs_host: "", pve_host: "", pbs_trusted_sources: "", } 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 }, }, })) } /** Flatten the nested NotificationConfig into the flat key-value map the backend expects. */ const flattenConfig = (cfg: NotificationConfig): Record => { const flat: Record = { enabled: String(cfg.enabled), severity_filter: cfg.severity_filter, ai_enabled: String(cfg.ai_enabled), ai_provider: cfg.ai_provider, ai_api_key: cfg.ai_api_key, ai_model: cfg.ai_model, hostname: cfg.hostname, webhook_secret: cfg.webhook_secret, webhook_allowed_ips: cfg.webhook_allowed_ips, pbs_host: cfg.pbs_host, pve_host: cfg.pve_host, pbs_trusted_sources: cfg.pbs_trusted_sources, } // Flatten channels: { telegram: { enabled, bot_token, chat_id } } -> telegram.enabled, telegram.bot_token, ... for (const [chName, chCfg] of Object.entries(cfg.channels)) { for (const [field, value] of Object.entries(chCfg)) { flat[`${chName}.${field}`] = String(value ?? "") } } // Flatten event_categories: { system: true, backups: false } -> events.system, events.backups for (const [cat, enabled] of Object.entries(cfg.event_categories)) { flat[`events.${cat}`] = String(enabled) } return flat } const handleSave = async () => { setSaving(true) try { // If notifications are being disabled, clean up PVE webhook first const wasEnabled = originalConfig.enabled const isNowDisabled = !config.enabled if (wasEnabled && isNowDisabled) { try { await fetchApi("/api/notifications/proxmox/cleanup-webhook", { method: "POST" }) } catch { // Non-fatal: webhook cleanup failed but we still save settings } } const payload = flattenConfig(config) await fetchApi("/api/notifications/settings", { method: "POST", body: JSON.stringify(payload), }) 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 { // Auto-save current config before testing so backend has latest channel data const payload = flattenConfig(config) await fetchApi("/api/notifications/settings", { method: "POST", body: JSON.stringify(payload), }) setOriginalConfig(config) setHasChanges(false) const data = await fetchApi<{ success: boolean message?: string error?: string results?: Record }>("/api/notifications/test", { method: "POST", body: JSON.stringify({ channel }), }) // Extract message from the results object if present let message = data.message || "" if (!message && data.results) { const channelResult = data.results[channel] if (channelResult) { message = channelResult.success ? "Test notification sent successfully" : channelResult.error || "Test failed" } } if (!message && data.error) { message = data.error } if (!message) { message = data.success ? "Test notification sent successfully" : "Test failed" } setTestResult({ channel, success: data.success, message }) } catch (err) { setTestResult({ channel, success: false, message: String(err) }) } finally { setTesting(null) setTimeout(() => setTestResult(null), 8000) } } 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.

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

{`webhook: proxmenux-webhook
\tmethod post
\turl http://:8008/api/notifications/webhook

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

{"Replace with the IP of this PVE node (not 127.0.0.1, unless PBS runs on the same host). Append at the end -- do not delete existing content."}

) } 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)} />
updateChannel("telegram", "chat_id", e.target.value)} />
{/* Per-channel action bar */}
)}
{/* Gotify */}
{config.channels.gotify?.enabled && ( <>
updateChannel("gotify", "url", e.target.value)} />
updateChannel("gotify", "token", e.target.value)} />
{/* Per-channel action bar */}
)}
{/* Discord */}
{config.channels.discord?.enabled && ( <>
updateChannel("discord", "webhook_url", e.target.value)} />
{/* Per-channel action bar */}
)}
{/* Email */}
{config.channels.email?.enabled && ( <>
updateChannel("email", "host", e.target.value)} />
updateChannel("email", "port", e.target.value)} />
updateChannel("email", "username", e.target.value)} />
updateChannel("email", "password", e.target.value)} />
updateChannel("email", "from_address", e.target.value)} />
updateChannel("email", "to_addresses", e.target.value)} />
updateChannel("email", "subject_prefix", e.target.value)} />

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.

{/* Per-channel action bar */}
)}
{/* Test Result */} {testResult && (
{testResult.success ? ( ) : ( )} {testResult.message}
)}
{/* close bordered channel container */}
{/* ── Filters ── */}
Filters & Events
{/* Severity */}
{/* Event Categories */}
{EVENT_CATEGORIES.map(cat => ( ))}
{/* close bordered filters container */}
{/* ── Proxmox Webhook ── */}
Proxmox Webhook
PVE Webhook Configuration
{!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} />

{"Used for remote connections only (e.g. PBS on another host). Local PVE webhook runs on localhost and does not need this header."}

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

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

{/* close bordered webhook container */} {/* 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.

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

{`webhook: proxmenux-webhook
\tmethod post
\turl http://:8008/api/notifications/webhook

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

{"Replace with this node's IP. Append at the end -- do not delete existing content."}

{/* ── 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."}

) }