Files
helix-engage/src/components/layout/notification-bell.tsx
saridsa2 180613a2f3 feat(notifications): poll real PerformanceAlert rows from sidecar
usePerformanceAlerts now fetches /api/supervisor/performance-alerts
every 60s instead of computing client-side. Dismiss + dismiss-all hit
the sidecar so state survives reload. Toast fires when new alerts arrive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:02:20 +05:30

111 lines
5.8 KiB
TypeScript

import { useState, useRef, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faBell, faTriangleExclamation, faXmark, faCheck } from '@fortawesome/pro-duotone-svg-icons';
import { Badge } from '@/components/base/badges/badges';
import { usePerformanceAlerts } from '@/hooks/use-performance-alerts';
import { cx } from '@/utils/cx';
export const NotificationBell = () => {
const { alerts, dismiss, dismissAll } = usePerformanceAlerts();
const [open, setOpen] = useState(false);
const panelRef = useRef<HTMLDivElement>(null);
// Close on outside click
useEffect(() => {
if (!open) return;
const handler = (e: MouseEvent) => {
if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
setOpen(false);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [open]);
return (
<div className="relative" ref={panelRef}>
<button
onClick={() => setOpen(!open)}
className={cx(
'relative flex size-9 items-center justify-center rounded-lg border transition duration-100 ease-linear',
alerts.length > 0
? 'border-error bg-error-primary text-fg-error-primary hover:bg-error-secondary'
: open
? 'border-brand bg-active text-brand-secondary'
: 'border-secondary bg-primary text-fg-secondary hover:bg-primary_hover',
)}
title="Notifications"
>
<FontAwesomeIcon icon={faBell} className="size-5" />
{alerts.length > 0 && (
<span className="absolute -top-1.5 -right-1.5 flex size-5 items-center justify-center rounded-full bg-error-solid text-[10px] font-bold text-white ring-2 ring-white">
{alerts.length > 9 ? '9+' : alerts.length}
</span>
)}
</button>
{open && (
<div className="absolute top-full right-0 mt-2 w-96 rounded-xl bg-primary shadow-xl ring-1 ring-secondary z-50 overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-secondary">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-primary">Notifications</span>
{alerts.length > 0 && (
<Badge size="sm" color="error" type="pill-color">{alerts.length}</Badge>
)}
</div>
{alerts.length > 0 && (
<button
onClick={dismissAll}
className="text-xs font-medium text-brand-secondary hover:text-brand-secondary_hover transition duration-100 ease-linear"
>
Clear all
</button>
)}
</div>
{/* Alert list */}
<div className="max-h-96 overflow-y-auto">
{alerts.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<FontAwesomeIcon icon={faCheck} className="size-5 text-fg-success-primary mb-2" />
<p className="text-xs text-tertiary">No active alerts</p>
</div>
) : (
alerts.map(alert => (
<div
key={alert.id}
className={cx(
'flex items-center gap-3 px-4 py-3 border-b border-secondary last:border-b-0',
alert.severity === 'error' ? 'bg-error-primary' : 'bg-warning-primary',
)}
>
<FontAwesomeIcon
icon={faTriangleExclamation}
className={cx(
'size-4 shrink-0',
alert.severity === 'error' ? 'text-fg-error-primary' : 'text-fg-warning-primary',
)}
/>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-primary">{alert.agent}</p>
<p className="text-xs text-tertiary">{alert.type}</p>
</div>
<Badge size="sm" color={alert.severity === 'error' ? 'error' : alert.severity === 'warning' ? 'warning' : 'gray'} type="pill-color">{alert.value}</Badge>
<button
onClick={() => dismiss(alert.id)}
className="text-fg-quaternary hover:text-fg-secondary shrink-0 transition duration-100 ease-linear"
title="Dismiss"
>
<FontAwesomeIcon icon={faXmark} className="size-3.5" />
</button>
</div>
))
)}
</div>
</div>
)}
</div>
);
};