"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 { 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, ChevronRight, TestTube2, Mail, Webhook, Copy, Server, Shield } from "lucide-react" interface ChannelConfig { enabled: boolean rich_format?: 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 EventTypeInfo { type: string title: string default_enabled: boolean } interface ChannelOverrides { categories: Record events: Record } interface NotificationConfig { enabled: boolean channels: Record event_categories: Record event_toggles: Record event_types_by_group: Record channel_overrides: 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 EVENT_CATEGORIES = [ { 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, 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" }, { key: "services", label: "Services", desc: "System services, shutdown, reboot" }, { key: "health", label: "Health Monitor", desc: "Health checks, degradation, recovery" }, { key: "updates", label: "Updates", desc: "System and PVE updates" }, { key: "other", label: "Other", desc: "Uncategorized notifications" }, ] const CHANNEL_TYPES = ["telegram", "gotify", "discord", "email"] as const 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 }, }, event_categories: { vm_ct: true, backup: true, resources: true, storage: true, network: true, security: true, cluster: true, services: true, health: true, updates: true, other: true, }, event_toggles: {}, event_types_by_group: {}, channel_overrides: { telegram: { categories: {}, events: {} }, gotify: { categories: {}, events: {} }, discord: { categories: {}, events: {} }, email: { categories: {}, events: {} }, }, 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 [expandedCategories, setExpandedCategories] = useState>(new Set()) 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 }, }, })) } /** Reusable 10+1 category block rendered inside each channel tab. */ const renderChannelCategories = (chName: string) => { const overrides = config.channel_overrides?.[chName] || { categories: {}, events: {} } const evtByGroup = config.event_types_by_group || {} return (
{EVENT_CATEGORIES.filter(cat => cat.key !== "other").map(cat => { const isEnabled = overrides.categories[cat.key] ?? true const isExpanded = expandedCategories.has(`${chName}.${cat.key}`) const eventsForGroup = evtByGroup[cat.key] || [] const enabledCount = eventsForGroup.filter( e => (overrides.events?.[e.type] ?? e.default_enabled) ).length return (
{/* Category row -- entire block is clickable to expand/collapse */}
{ if (!isEnabled) return setExpandedCategories(prev => { const next = new Set(prev) const key = `${chName}.${cat.key}` if (next.has(key)) next.delete(key) else next.add(key) return next }) }} > {/* Expand arrow */} {/* Label */}
{cat.label}
{/* Count badge */} {isEnabled && eventsForGroup.length > 0 && ( {enabledCount}/{eventsForGroup.length} )} {/* Toggle -- same style as channel enable toggle */}
{/* Sub-event toggles */} {isEnabled && isExpanded && eventsForGroup.length > 0 && (
{eventsForGroup.map(evt => { const evtEnabled = overrides.events?.[evt.type] ?? evt.default_enabled return (
{evt.title}
) })}
)}
) })}
) } /** 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), 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 ?? "") } } // Per-channel category & event toggles: telegram.events.vm_ct, telegram.event.vm_start, etc. // Each channel independently owns its notification preferences. if (cfg.channel_overrides) { for (const [chName, overrides] of Object.entries(cfg.channel_overrides)) { if (overrides.categories) { for (const [cat, enabled] of Object.entries(overrides.categories)) { flat[`${chName}.events.${cat}`] = String(enabled) } } if (overrides.events) { for (const [evt, enabled] of Object.entries(overrides.events)) { flat[`${chName}.event.${evt}`] = 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')}
                    
)}
)}
) } 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)} />
{/* Message format */}

Enrich notifications with contextual emojis and icons

{renderChannelCategories("telegram")} {/* Send Test */}
)}
{/* Gotify */}
{config.channels.gotify?.enabled && ( <>
updateChannel("gotify", "url", e.target.value)} />
updateChannel("gotify", "token", e.target.value)} />
{/* Message format */}

Enrich notifications with contextual emojis and icons

{renderChannelCategories("gotify")} {/* Send Test */}
)}
{/* Discord */}
{config.channels.discord?.enabled && ( <>
updateChannel("discord", "webhook_url", e.target.value)} />
{/* Message format */}

Enrich notifications with contextual emojis and icons

{renderChannelCategories("discord")} {/* Send Test */}
)}
{/* 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.

{renderChannelCategories("email")} {/* Send Test */}
)}
{/* Test Result */} {testResult && (
{testResult.success ? ( ) : ( )} {testResult.message}
)}
{/* close bordered channel container */}
{/* ── 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.

)}
)}
)} {/* ── Footer info ── */}

{config.enabled ? "Notifications are active. Each channel sends events based on its own category and event selection." : "Enable notifications to receive alerts about system events, health status changes, and security incidents via Telegram, Gotify, Discord, or Email."}

) }