"use client"; import { useEffect, useState, useCallback, useMemo, useRef } from "react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { fetchAlarms, fetchAlarmStats, acknowledgeAlarm, resolveAlarm, type Alarm, type AlarmStats, } from "@/lib/api"; import { RackDetailSheet } from "@/components/dashboard/rack-detail-sheet"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AlertTriangle, CheckCircle2, Clock, XCircle, Bell, ChevronsUpDown, ChevronUp, ChevronDown, Activity, } from "lucide-react"; import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, Cell, } from "recharts"; import { cn } from "@/lib/utils"; const SITE_ID = "sg-01"; const PAGE_SIZE = 25; type StateFilter = "active" | "acknowledged" | "resolved" | "all"; type SeverityFilter = "all" | "critical" | "warning" | "info"; type SortKey = "severity" | "triggered_at" | "state"; type SortDir = "asc" | "desc"; function timeAgo(iso: string): string { const diff = Date.now() - new Date(iso).getTime(); const m = Math.floor(diff / 60000); if (m < 1) return "just now"; if (m < 60) return `${m}m`; const h = Math.floor(m / 60); if (h < 24) return `${h}h`; return `${Math.floor(h / 24)}d`; } function useNow(intervalMs = 30_000): number { const [now, setNow] = useState(() => Date.now()); useEffect(() => { const id = setInterval(() => setNow(Date.now()), intervalMs); return () => clearInterval(id); }, [intervalMs]); return now; } function escalationMinutes(triggeredAt: string, now: number): number { return Math.floor((now - new Date(triggeredAt).getTime()) / 60_000); } function EscalationTimer({ triggeredAt, now }: { triggeredAt: string; now: number }) { const mins = escalationMinutes(triggeredAt, now); const h = Math.floor(mins / 60); const m = mins % 60; const label = h > 0 ? `${h}h ${m}m` : `${m}m`; const colorClass = mins >= 60 ? "text-destructive" : mins >= 15 ? "text-amber-400" : mins >= 5 ? "text-amber-300" : "text-muted-foreground"; const pulse = mins >= 60; return ( {label} ); } function alarmCategory(sensorId: string | null | undefined): { label: string; className: string } { if (!sensorId) return { label: "System", className: "bg-muted/50 text-muted-foreground" }; const s = sensorId.toLowerCase(); if (s.includes("cooling") || s.includes("crac") || s.includes("refrigerant") || s.includes("cop")) return { label: "Refrigerant", className: "bg-cyan-500/10 text-cyan-400" }; if (s.includes("temp") || s.includes("thermal") || s.includes("humidity") || s.includes("hum")) return { label: "Thermal", className: "bg-orange-500/10 text-orange-400" }; if (s.includes("power") || s.includes("ups") || s.includes("pdu") || s.includes("kw") || s.includes("watt")) return { label: "Power", className: "bg-yellow-500/10 text-yellow-400" }; if (s.includes("leak") || s.includes("water") || s.includes("flood")) return { label: "Leak", className: "bg-blue-500/10 text-blue-400" }; return { label: "System", className: "bg-muted/50 text-muted-foreground" }; } const severityConfig: Record = { critical: { label: "Critical", bg: "bg-destructive/15 text-destructive border-destructive/30", dot: "bg-destructive" }, warning: { label: "Warning", bg: "bg-amber-500/15 text-amber-400 border-amber-500/30", dot: "bg-amber-500" }, info: { label: "Info", bg: "bg-blue-500/15 text-blue-400 border-blue-500/30", dot: "bg-blue-500" }, }; const stateConfig: Record = { active: { label: "Active", className: "bg-destructive/10 text-destructive" }, acknowledged: { label: "Acknowledged", className: "bg-amber-500/10 text-amber-400" }, resolved: { label: "Resolved", className: "bg-green-500/10 text-green-400" }, }; function SeverityBadge({ severity }: { severity: string }) { const c = severityConfig[severity] ?? severityConfig.info; return ( {c.label} ); } function StatCard({ label, value, icon: Icon, highlight }: { label: string; value: number; icon: React.ElementType; highlight?: boolean }) { return (
0 ? "bg-destructive/10" : "bg-muted")}> 0 ? "text-destructive" : "text-muted-foreground")} />

0 ? "text-destructive" : "")}>{value}

{label}

); } function AvgAgeCard({ alarms }: { alarms: Alarm[] }) { const activeAlarms = alarms.filter(a => a.state === "active"); const avgMins = useMemo(() => { if (activeAlarms.length === 0) return 0; const now = Date.now(); const totalMins = activeAlarms.reduce((sum, a) => { return sum + Math.floor((now - new Date(a.triggered_at).getTime()) / 60_000); }, 0); return Math.round(totalMins / activeAlarms.length); }, [activeAlarms]); const label = avgMins >= 60 ? `${Math.floor(avgMins / 60)}h ${avgMins % 60}m` : `${avgMins}m`; const colorClass = avgMins > 60 ? "text-destructive" : avgMins > 15 ? "text-amber-400" : "text-green-400"; const iconColor = avgMins > 60 ? "text-destructive" : avgMins > 15 ? "text-amber-400" : "text-muted-foreground"; const bgColor = avgMins > 60 ? "bg-destructive/10" : avgMins > 15 ? "bg-amber-500/10" : "bg-muted"; return (

0 ? colorClass : "")}>{activeAlarms.length > 0 ? label : "—"}

Avg Age

); } type Correlation = { id: string title: string severity: "critical" | "warning" description: string alarmIds: number[] } function correlateAlarms(alarms: Alarm[]): Correlation[] { const active = alarms.filter(a => a.state === "active"); const results: Correlation[] = []; // Rule 1: ≥2 thermal alarms in the same room → probable CRAC issue const thermalByRoom = new Map(); for (const a of active) { const isThermal = a.sensor_id ? /temp|thermal|humidity|hum/i.test(a.sensor_id) : /temp|thermal|hot|cool/i.test(a.message); const room = a.room_id; if (isThermal && room) { if (!thermalByRoom.has(room)) thermalByRoom.set(room, []); thermalByRoom.get(room)!.push(a); } } for (const [room, roomAlarms] of thermalByRoom.entries()) { if (roomAlarms.length >= 2) { results.push({ id: `thermal-${room}`, title: `Thermal event — ${room.replace("hall-", "Hall ")}`, severity: roomAlarms.some(a => a.severity === "critical") ? "critical" : "warning", description: `${roomAlarms.length} thermal alarms in the same room. Probable cause: CRAC cooling degradation or containment breach.`, alarmIds: roomAlarms.map(a => a.id), }); } } // Rule 2: ≥3 power alarms across different racks → PDU or UPS path issue const powerAlarms = active.filter(a => a.sensor_id ? /power|pdu|ups|kw|watt/i.test(a.sensor_id) : /power|overload|circuit/i.test(a.message) ); const powerRacks = new Set(powerAlarms.map(a => a.rack_id).filter(Boolean)); if (powerRacks.size >= 2) { results.push({ id: "power-multi-rack", title: "Multi-rack power event", severity: powerAlarms.some(a => a.severity === "critical") ? "critical" : "warning", description: `Power alarms on ${powerRacks.size} racks simultaneously. Probable cause: upstream PDU, busway tap, or UPS transfer.`, alarmIds: powerAlarms.map(a => a.id), }); } // Rule 3: Generator + ATS alarms together → power path / utility failure const genAlarm = active.find(a => a.sensor_id ? /gen/i.test(a.sensor_id) : /generator/i.test(a.message)); const atsAlarm = active.find(a => a.sensor_id ? /ats/i.test(a.sensor_id) : /transfer|utility/i.test(a.message)); if (genAlarm && atsAlarm) { results.push({ id: "gen-ats-event", title: "Power path event — generator + ATS", severity: "critical", description: "Generator and ATS alarms are co-active. Possible utility failure with generator transfer in progress.", alarmIds: [genAlarm.id, atsAlarm.id], }); } // Rule 4: ≥2 leak alarms → site-wide leak / pipe burst const leakAlarms = active.filter(a => a.sensor_id ? /leak|water|flood/i.test(a.sensor_id) : /leak|water/i.test(a.message) ); if (leakAlarms.length >= 2) { results.push({ id: "multi-leak", title: "Multiple leak sensors triggered", severity: "critical", description: `${leakAlarms.length} leak sensors active. Probable cause: pipe burst, chilled water leak, or CRAC drain overflow.`, alarmIds: leakAlarms.map(a => a.id), }); } // Rule 5: VESDA + high temp in same room → fire / smoke event const vesdaAlarm = active.find(a => a.sensor_id ? /vesda|fire/i.test(a.sensor_id) : /fire|smoke|vesda/i.test(a.message)); const hotRooms = new Set(active.filter(a => a.severity === "critical" && a.room_id && /temp/i.test(a.message + (a.sensor_id ?? ""))).map(a => a.room_id)); if (vesdaAlarm && hotRooms.size > 0) { results.push({ id: "fire-temp-event", title: "Fire / smoke event suspected", severity: "critical", description: "VESDA alarm co-active with critical temperature alarms. Possible fire or smoke event — check fire safety systems immediately.", alarmIds: active.filter(a => hotRooms.has(a.room_id)).map(a => a.id).concat(vesdaAlarm.id), }); } return results; } function RootCausePanel({ alarms }: { alarms: Alarm[] }) { const correlations = correlateAlarms(alarms); if (correlations.length === 0) return null; return ( Root Cause Analysis {correlations.length} pattern{correlations.length > 1 ? "s" : ""} detected {correlations.map(c => (

{c.title} ({c.alarmIds.length} alarm{c.alarmIds.length !== 1 ? "s" : ""})

{c.description}

))}
); } export default function AlarmsPage() { const router = useRouter(); const now = useNow(30_000); const [alarms, setAlarms] = useState([]); const [allAlarms, setAllAlarms] = useState([]); const [stats, setStats] = useState(null); const [stateFilter, setStateFilter] = useState("active"); const [sevFilter, setSevFilter] = useState("all"); const [sortKey, setSortKey] = useState("triggered_at"); const [sortDir, setSortDir] = useState("desc"); const [loading, setLoading] = useState(true); const [acting, setActing] = useState(null); const [selected, setSelected] = useState>(new Set()); const [bulkActing, setBulkActing] = useState(false); const [selectedRack, setSelectedRack] = useState(null); const [assignments, setAssignments] = useState>({}); const [page, setPage] = useState(1); useEffect(() => { try { setAssignments(JSON.parse(localStorage.getItem("alarm-assignments") ?? "{}")); } catch {} }, []); function setAssignment(id: number, assignee: string) { const next = { ...assignments, [id]: assignee }; setAssignments(next); localStorage.setItem("alarm-assignments", JSON.stringify(next)); } const load = useCallback(async () => { try { const [a, s, all] = await Promise.all([ fetchAlarms(SITE_ID, stateFilter), fetchAlarmStats(SITE_ID), fetchAlarms(SITE_ID, "all", 200), ]); setAlarms(a); setStats(s); setAllAlarms(all); } catch { toast.error("Failed to load alarms"); } finally { setLoading(false); } }, [stateFilter]); useEffect(() => { setLoading(true); load(); const id = setInterval(load, 15_000); return () => clearInterval(id); }, [load]); // Reset page when filters change useEffect(() => { setPage(1); }, [stateFilter, sevFilter]); async function handleAcknowledge(id: number) { setActing(id); try { await acknowledgeAlarm(id); toast.success("Alarm acknowledged"); await load(); } finally { setActing(null); } } async function handleResolve(id: number) { setActing(id); try { await resolveAlarm(id); toast.success("Alarm resolved"); await load(); } finally { setActing(null); } } async function handleBulkResolve() { setBulkActing(true); const count = selected.size; try { await Promise.all(Array.from(selected).map((id) => resolveAlarm(id))); toast.success(`${count} alarm${count !== 1 ? "s" : ""} resolved`); setSelected(new Set()); await load(); } finally { setBulkActing(false); } } function toggleSelect(id: number) { setSelected((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); } function toggleSelectAll() { const resolvable = visible.filter((a) => a.state !== "resolved").map((a) => a.id); if (resolvable.every((id) => selected.has(id))) { setSelected(new Set()); } else { setSelected(new Set(resolvable)); } } const sevOrder: Record = { critical: 0, warning: 1, info: 2 }; const stateOrder: Record = { active: 0, acknowledged: 1, resolved: 2 }; function toggleSort(key: SortKey) { if (sortKey === key) setSortDir((d) => d === "asc" ? "desc" : "asc"); else { setSortKey(key); setSortDir("desc"); } } function SortIcon({ col }: { col: SortKey }) { if (sortKey !== col) return ; return sortDir === "asc" ? : ; } const visible = (sevFilter === "all" ? alarms : alarms.filter((a) => a.severity === sevFilter)) .slice() .sort((a, b) => { let cmp = 0; if (sortKey === "severity") cmp = (sevOrder[a.severity] ?? 9) - (sevOrder[b.severity] ?? 9); if (sortKey === "triggered_at") cmp = new Date(a.triggered_at).getTime() - new Date(b.triggered_at).getTime(); if (sortKey === "state") cmp = (stateOrder[a.state] ?? 9) - (stateOrder[b.state] ?? 9); return sortDir === "asc" ? cmp : -cmp; }); const pageCount = Math.ceil(visible.length / PAGE_SIZE); const paginated = visible.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); return (
setSelectedRack(null)} />

Alarms & Events

Singapore DC01 — refreshes every 15s

{/* Escalation banner — longest unacknowledged critical */} {(() => { const critActive = alarms.filter(a => a.severity === "critical" && a.state === "active"); if (critActive.length === 0) return null; const oldest = critActive.reduce((a, b) => new Date(a.triggered_at) < new Date(b.triggered_at) ? a : b ); const mins = escalationMinutes(oldest.triggered_at, now); const urgency = mins >= 60 ? "bg-destructive/10 border-destructive/30 text-destructive" : mins >= 15 ? "bg-amber-500/10 border-amber-500/30 text-amber-400" : "bg-amber-500/5 border-amber-500/20 text-amber-300"; return (
{critActive.length} critical alarm{critActive.length > 1 ? "s" : ""} unacknowledged {" — "}longest open for
); })()} {/* Root cause correlation panel */} {!loading && } {/* Stat cards */}
{stats ? ( <> ) : ( Array.from({ length: 4 }).map((_, i) => ) )}
{/* Sticky filter bar */}
setStateFilter(v as StateFilter)}> Active Acknowledged Resolved All
{(["all", "critical", "warning", "info"] as SeverityFilter[]).map((s) => ( ))}
{/* Bulk actions inline in filter row */} {selected.size > 0 && (
{selected.size} selected
)}
{/* Row count */} {!loading && (

{visible.length} alarm{visible.length !== 1 ? "s" : ""} matching filter

)} {/* Table */} {loading ? (
{Array.from({ length: 5 }).map((_, i) => )}
) : visible.length === 0 ? (

No alarms matching this filter

) : (
{paginated.map((alarm) => { const sc = stateConfig[alarm.state] ?? stateConfig.active; const cat = alarmCategory(alarm.sensor_id); return ( ); })}
a.state !== "resolved").every((a) => selected.has(a.id))} onChange={toggleSelectAll} /> Message Location Sensor Category Escalation Assigned Actions
{alarm.state !== "resolved" && ( toggleSelect(alarm.id)} /> )} {alarm.message} {(alarm.room_id || alarm.rack_id) ? (
{alarm.room_id && ( )} {alarm.room_id && alarm.rack_id && /} {alarm.rack_id && ( )}
) : }
{alarm.sensor_id ? ( {alarm.sensor_id.split("/").slice(-1)[0]} ) : } {cat.label} {sc.label} {timeAgo(alarm.triggered_at)} {alarm.state !== "resolved" && alarm.severity === "critical" ? ( ) : ( )}
{alarm.state === "active" && ( )} {(alarm.state === "active" || alarm.state === "acknowledged") && ( )}
)}
{/* Pagination bar */} {!loading && visible.length > PAGE_SIZE && (
Showing {(page - 1) * PAGE_SIZE + 1}–{Math.min(page * PAGE_SIZE, visible.length)} of {visible.length} alarms
{page} / {pageCount}
)}
); }