diff --git a/src/features/settings/NotificationPreferences.tsx b/src/features/settings/NotificationPreferences.tsx new file mode 100644 index 0000000..690826c --- /dev/null +++ b/src/features/settings/NotificationPreferences.tsx @@ -0,0 +1,690 @@ +'use client'; + +import React, { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { toast } from 'sonner'; +import { + Bell, + Webhook, + Mail, + BellRing, + Plus, + Trash2, + Check, + X, + AlertTriangle, + Zap, + Shield, + FileText, + Settings, + Send, + Loader2, + CheckCircle, + XCircle, +} from 'lucide-react'; +import { Card } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Dialog } from '@/components/ui/dialog'; +import { + useNotificationStore, + type AlertCategory, + type AlertSeverity, +} from '@/stores'; + +// Alert category definitions +const ALERT_CATEGORIES: { category: AlertCategory; label: string; description: string; icon: React.ReactNode; severity: AlertSeverity }[] = [ + { + category: 'budget_warning', + label: 'Budget Warning', + description: 'Alert when budget usage exceeds threshold', + icon: , + severity: 'warning', + }, + { + category: 'budget_exhaustion', + label: 'Budget Exhaustion', + description: 'Critical alert when budget is nearly depleted', + icon: , + severity: 'critical', + }, + { + category: 'policy_violation', + label: 'Policy Violation', + description: 'Alert when agent violates spending policy', + icon: , + severity: 'critical', + }, + { + category: 'proposal_created', + label: 'Proposal Created', + description: 'Notification when new proposal is submitted', + icon: , + severity: 'info', + }, + { + category: 'proposal_approved', + label: 'Proposal Approved', + description: 'Notification when proposal is approved', + icon: , + severity: 'info', + }, + { + category: 'proposal_rejected', + label: 'Proposal Rejected', + description: 'Notification when proposal is rejected', + icon: , + severity: 'warning', + }, + { + category: 'agent_error', + label: 'Agent Error', + description: 'Alert when agent encounters runtime error', + icon: , + severity: 'warning', + }, + { + category: 'system_alert', + label: 'System Alert', + description: 'Critical system-wide alerts and maintenance notices', + icon: , + severity: 'info', + }, +]; + +// Webhook URL validation schema +const webhookSchema = z.object({ + name: z.string().min(1, 'Webhook name is required'), + url: z.string().url('Please enter a valid URL').refine( + (url) => url.startsWith('http://') || url.startsWith('https://'), + 'URL must start with http:// or https://' + ), +}); + +type WebhookFormValues = z.infer; + +// Email validation schema +const emailSchema = z.object({ + email: z.string().email('Please enter a valid email address'), +}); + +type EmailFormValues = z.infer; + +export function NotificationPreferences() { + const { + webhookEndpoints, + alertRules, + emailDigest, + inAppNotifications, + addWebhookEndpoint, + updateWebhookEndpoint, + removeWebhookEndpoint, + testWebhookEndpoint, + updateAlertRule, + toggleAlertRule, + updateEmailDigest, + updateInAppNotifications, + } = useNotificationStore(); + + const [isAddWebhookOpen, setIsAddWebhookOpen] = useState(false); + const [isAddEmailOpen, setIsAddEmailOpen] = useState(false); + const [testingWebhookId, setTestingWebhookId] = useState(null); + + const webhookForm = useForm({ + resolver: zodResolver(webhookSchema), + defaultValues: { + name: '', + url: '', + }, + }); + + const emailForm = useForm({ + resolver: zodResolver(emailSchema), + defaultValues: { + email: '', + }, + }); + + // Handle webhook form submission + const handleWebhookSubmit = (values: WebhookFormValues) => { + addWebhookEndpoint({ + name: values.name, + url: values.url, + isActive: true, + }); + setIsAddWebhookOpen(false); + webhookForm.reset(); + toast.success('Webhook endpoint added successfully'); + }; + + // Handle email form submission + const handleEmailSubmit = (values: EmailFormValues) => { + updateEmailDigest({ + recipients: [...emailDigest.recipients, values.email], + }); + setIsAddEmailOpen(false); + emailForm.reset(); + toast.success('Email recipient added successfully'); + }; + + // Handle webhook test + const handleTestWebhook = async (webhookId: string) => { + setTestingWebhookId(webhookId); + const result = await testWebhookEndpoint(webhookId); + setTestingWebhookId(null); + + if (result.success) { + toast.success(result.message); + } else { + toast.error(result.message); + } + }; + + // Handle remove email recipient + const handleRemoveEmail = (email: string) => { + updateEmailDigest({ + recipients: emailDigest.recipients.filter((e) => e !== email), + }); + toast.success('Email recipient removed'); + }; + + // Get severity badge variant + const getSeverityBadge = (severity: AlertSeverity) => { + switch (severity) { + case 'critical': + return 'danger'; + case 'warning': + return 'warning'; + case 'info': + return 'info'; + default: + return 'neutral'; + } + }; + + return ( +
+ {/* Section Header */} +
+
+

+ + Notification Preferences & Alert Rules +

+

+ Configure webhook endpoints, email digests, and threshold alerts for agent budget exhaustion or policy breaches. +

+
+
+ + {/* Alert Categories Section */} + +
+

+ + Alert Categories +

+ + {alertRules.filter((r) => r.enabled).length} Active + +
+ +
+ {ALERT_CATEGORIES.map((alertDef) => { + const rule = alertRules.find((r) => r.category === alertDef.category); + const isEnabled = rule?.enabled ?? false; + const threshold = rule?.threshold; + + return ( +
+
+
{alertDef.icon}
+
+
+ {alertDef.label} + + {alertDef.severity} + +
+

{alertDef.description}

+
+
+ +
+ {/* Threshold input for budget-related alerts */} + {(alertDef.category === 'budget_warning' || alertDef.category === 'budget_exhaustion') && ( +
+ + { + if (rule) { + updateAlertRule(rule.id, { + threshold: Math.min(100, Math.max(0, parseInt(e.target.value) || 0)), + }); + } + }} + className="w-16 rounded-button border border-border bg-surface px-2 py-1 text-xs text-foreground text-center focus:border-gold focus:outline-none" + disabled={!isEnabled} + /> + % +
+ )} + + {/* Toggle switch */} + +
+
+ ); + })} +
+
+ + {/* Webhook Endpoints Section */} + +
+

+ + Webhook Endpoints +

+ +
+ +
+ {webhookEndpoints.length === 0 ? ( +
+ +

No webhook endpoints configured

+

Add a webhook to receive real-time notifications

+
+ ) : ( +
+ {webhookEndpoints.map((webhook) => ( +
+
+
+ {webhook.name} + + {webhook.isActive ? 'Active' : 'Inactive'} + + {webhook.lastTestStatus && ( + + {webhook.lastTestStatus === 'success' && } + {webhook.lastTestStatus === 'failed' && } + {webhook.lastTestStatus === 'pending' && } + {webhook.lastTestStatus} + + )} +
+

{webhook.url}

+
+ +
+ + + + + +
+
+ ))} +
+ )} +
+
+ + {/* Email Digest Configuration */} + +
+

+ + Email Digest Configuration +

+ +
+ +
+ {/* Enable/disable email digest */} +
+
+ Enable Email Digest +

Receive summarized notifications via email

+
+ +
+ + {/* Frequency selection */} + {emailDigest.enabled && ( +
+ + +
+ )} + + {/* Email recipients list */} + {emailDigest.enabled && emailDigest.recipients.length > 0 && ( +
+ +
+ {emailDigest.recipients.map((email) => ( +
+ {email} + +
+ ))} +
+
+ )} + + {/* Include summary toggle */} + {emailDigest.enabled && ( +
+
+ Include Summary +

Include a digest summary in each email

+
+ +
+ )} +
+
+ + {/* In-App Notification Settings */} + +
+

+ + In-App Notification Settings +

+
+ +
+ {/* Enable in-app notifications */} +
+
+ Enable In-App Notifications +

Show notifications within the application

+
+ +
+ + {/* Sound notifications */} + {inAppNotifications.enabled && ( +
+
+ Notification Sound +

Play a sound when notifications arrive

+
+ +
+ )} + + {/* Desktop notifications */} + {inAppNotifications.enabled && ( +
+
+ Desktop Notifications +

Show browser desktop notifications

+
+ +
+ )} +
+
+ + {/* Add Webhook Modal */} + {isAddWebhookOpen && ( + setIsAddWebhookOpen(false)} title="Add Webhook Endpoint" size="sm"> +
+
+ + + {webhookForm.formState.errors.name && ( +

{webhookForm.formState.errors.name.message}

+ )} +
+ +
+ + + {webhookForm.formState.errors.url && ( +

{webhookForm.formState.errors.url.message}

+ )} +

+ We'll send a POST request with a test payload to verify the endpoint +

+
+ +
+ + +
+
+
+ )} + + {/* Add Email Recipient Modal */} + {isAddEmailOpen && ( + setIsAddEmailOpen(false)} title="Add Email Recipient" size="sm"> +
+
+ + + {emailForm.formState.errors.email && ( +

{emailForm.formState.errors.email.message}

+ )} +
+ +
+ + +
+
+
+ )} +
+ ); +}