/** * AlertsPage — Active alerts and alert history with filtering, acknowledge, and silence. */ import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Link } from '@tanstack/react-router' import { Bell, BellOff, BellRing, Building2, CheckCircle, AlertTriangle, ChevronLeft, ChevronRight, } from 'lucide-react' import { alertsApi, type AlertEvent, type AlertsFilterParams } from '@/lib/alertsApi' import { useAuth, isSuperAdmin } from '@/lib/auth' import { useUIStore } from '@/lib/store' import { Button } from '@/components/ui/button' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { toast } from '@/components/ui/toast' import { cn, formatDateTime } from '@/lib/utils' import { TableSkeleton } from '@/components/ui/page-skeleton' import { EmptyState } from '@/components/ui/empty-state' function SeverityBadge({ severity }: { severity: string }) { const config: Record = { critical: 'bg-error/20 text-error border-error/40', warning: 'bg-warning/20 text-warning border-warning/40', info: 'bg-info/20 text-info border-info/40', } return ( {severity} ) } function StatusIcon({ status }: { status: string }) { if (status === 'firing') return if (status === 'resolved') return if (status === 'flapping') return return } function timeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime() const mins = Math.floor(diff / 60000) if (mins < 1) return 'just now' if (mins < 60) return `${mins}m ago` const hours = Math.floor(mins / 60) if (hours < 24) return `${hours}h ago` const days = Math.floor(hours / 24) return `${days}d ago` } function AlertRow({ alert, tenantId, onAcknowledge, onSilence, }: { alert: AlertEvent tenantId: string onAcknowledge: (alertId: string) => void onSilence: (alertId: string, minutes: number) => void }) { const isSilenced = alert.silenced_until && new Date(alert.silenced_until) > new Date() return (
{alert.message ?? `${alert.metric} ${alert.value ?? ''}`} {alert.is_flapping && ( flapping )} {isSilenced && }
{alert.device_hostname ?? alert.device_id.slice(0, 8)} {alert.rule_name && {alert.rule_name}} {alert.threshold != null && ( {alert.value != null ? alert.value.toFixed(1) : '?'} / {alert.threshold} )} {timeAgo(alert.fired_at)} {alert.resolved_at && ( resolved {timeAgo(alert.resolved_at)} )}
{alert.status === 'firing' && !alert.acknowledged_at && ( )} {alert.status === 'firing' && ( onSilence(alert.id, 15)}>15 min onSilence(alert.id, 60)}>1 hour onSilence(alert.id, 240)}>4 hours onSilence(alert.id, 480)}>8 hours onSilence(alert.id, 1440)}>24 hours )}
) } export function AlertsPage() { const { user } = useAuth() const queryClient = useQueryClient() const [tab, setTab] = useState('active') const [severity, setSeverity] = useState('') const [page, setPage] = useState(1) // For super_admin, use global org context; for normal users, use their tenant const { selectedTenantId } = useUIStore() const tenantId = isSuperAdmin(user) ? (selectedTenantId ?? '') : (user?.tenant_id ?? '') // Build filter params const params: AlertsFilterParams = { page, per_page: 50, } if (tab === 'active') { params.status = 'firing' } if (severity) { params.severity = severity } const { data: alertsData, isLoading } = useQuery({ queryKey: ['alerts', tenantId, tab, severity, page], queryFn: () => alertsApi.getAlerts(tenantId, params), enabled: !!tenantId, refetchInterval: tab === 'active' ? 30_000 : undefined, }) const acknowledgeMutation = useMutation({ mutationFn: (alertId: string) => alertsApi.acknowledgeAlert(tenantId, alertId), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ['alerts'] }) void queryClient.invalidateQueries({ queryKey: ['alert-active-count'] }) toast({ title: 'Alert acknowledged' }) }, onError: () => toast({ title: 'Failed to acknowledge', variant: 'destructive' }), }) const silenceMutation = useMutation({ mutationFn: ({ alertId, minutes }: { alertId: string; minutes: number }) => alertsApi.silenceAlert(tenantId, alertId, minutes), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ['alerts'] }) void queryClient.invalidateQueries({ queryKey: ['alert-active-count'] }) toast({ title: 'Alert silenced' }) }, onError: () => toast({ title: 'Failed to silence', variant: 'destructive' }), }) const alerts = alertsData?.items ?? [] const total = alertsData?.total ?? 0 const totalPages = Math.ceil(total / 50) return (

Alerts

{/* Filters */}
{ setTab(v); setPage(1) }}> Active {tab === 'active' && total > 0 && ( {total} )} History {!tenantId ? (

Select an organization from the header to view alerts.

) : isLoading ? ( ) : alerts.length === 0 ? ( ) : (
{alerts.map((alert) => ( acknowledgeMutation.mutate(id)} onSilence={(id, mins) => silenceMutation.mutate({ alertId: id, minutes: mins }) } /> ))}
)}
{!tenantId ? (

Select an organization from the header to view alerts.

) : isLoading ? ( ) : alerts.length === 0 ? ( ) : (
{/* Table header */}
Severity Status Details Fired Resolved
{alerts.map((alert) => (
{alert.status}
{alert.message ?? alert.metric ?? 'System alert'} {alert.device_hostname ?? alert.device_id.slice(0, 8)} {alert.rule_name && ` — ${alert.rule_name}`}
{formatDateTime(alert.fired_at)} {alert.resolved_at ? formatDateTime(alert.resolved_at) : '—'}
))}
)}
{/* Pagination */} {totalPages > 1 && (
{total} alert{total !== 1 ? 's' : ''} total
{page} / {totalPages}
)}
) }