"use client"; import { useEffect, useState, useCallback, useRef } from "react"; import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; import { fetchCapacitySummary, fetchCracStatus, fetchAlarms, fetchLeakStatus, fetchFloorLayout, saveFloorLayout, type CapacitySummary, type CracStatus, type Alarm, type LeakSensorStatus, } 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 { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Wind, WifiOff, Thermometer, Zap, CheckCircle2, AlertTriangle, Droplets, Cable, Flame, Snowflake, Settings2, Plus, Trash2, GripVertical, ChevronDown, ChevronUp, } from "lucide-react"; import { cn } from "@/lib/utils"; import { useThresholds } from "@/lib/threshold-context"; const SITE_ID = "sg-01"; // ── Layout type ─────────────────────────────────────────────────────────────── type RowLayout = { label: string; racks: string[] }; type RoomLayout = { label: string; crac_id: string; rows: RowLayout[] }; type FloorLayout = Record; const DEFAULT_LAYOUT: FloorLayout = { "hall-a": { label: "Hall A", crac_id: "crac-01", rows: [ { label: "Row 1", racks: Array.from({ length: 20 }, (_, i) => `SG1A01.${String(i + 1).padStart(2, "0")}`) }, { label: "Row 2", racks: Array.from({ length: 20 }, (_, i) => `SG1A02.${String(i + 1).padStart(2, "0")}`) }, ], }, "hall-b": { label: "Hall B", crac_id: "crac-02", rows: [ { label: "Row 1", racks: Array.from({ length: 20 }, (_, i) => `SG1B01.${String(i + 1).padStart(2, "0")}`) }, { label: "Row 2", racks: Array.from({ length: 20 }, (_, i) => `SG1B02.${String(i + 1).padStart(2, "0")}`) }, ], }, }; // Derive feed from row index: even index → "A", odd → "B" function getFeed(layout: FloorLayout, rackId: string): "A" | "B" | undefined { for (const room of Object.values(layout)) { for (let i = 0; i < room.rows.length; i++) { if (room.rows[i].racks.includes(rackId)) return i % 2 === 0 ? "A" : "B"; } } return undefined; } // ── Colour helpers ──────────────────────────────────────────────────────────── function tempBg(temp: number | null, warn = 26, crit = 28) { if (temp === null) return "oklch(0.22 0.02 265)"; if (temp >= crit + 4) return "oklch(0.55 0.22 25)"; if (temp >= crit) return "oklch(0.65 0.20 45)"; if (temp >= warn) return "oklch(0.72 0.18 84)"; if (temp >= warn - 2) return "oklch(0.78 0.14 140)"; if (temp >= warn - 4) return "oklch(0.68 0.14 162)"; return "oklch(0.60 0.15 212)"; } function powerBg(pct: number | null) { if (pct === null) return "oklch(0.22 0.02 265)"; if (pct >= 90) return "oklch(0.55 0.22 25)"; if (pct >= 75) return "oklch(0.65 0.20 45)"; if (pct >= 55) return "oklch(0.72 0.18 84)"; if (pct >= 35) return "oklch(0.68 0.14 162)"; return "oklch(0.60 0.15 212)"; } type Overlay = "temp" | "power" | "alarms" | "feed" | "crac"; function alarmBg(count: number): string { if (count === 0) return "oklch(0.22 0.02 265)"; if (count >= 3) return "oklch(0.55 0.22 25)"; if (count >= 1) return "oklch(0.65 0.20 45)"; return "oklch(0.68 0.14 162)"; } function feedBg(feed: "A" | "B" | undefined): string { if (feed === "A") return "oklch(0.55 0.18 255)"; if (feed === "B") return "oklch(0.60 0.18 40)"; return "oklch(0.22 0.02 265)"; } const CRAC_ZONE_COLORS = [ "oklch(0.55 0.18 255)", // blue — zone 1 "oklch(0.60 0.18 40)", // amber — zone 2 "oklch(0.60 0.16 145)", // teal — zone 3 "oklch(0.58 0.18 310)", // purple — zone 4 ]; // ── Rack tile ───────────────────────────────────────────────────────────────── function RackTile({ rackId, temp, powerPct, alarmCount, overlay, feed, cracColor, onClick, tempWarn = 26, tempCrit = 28, }: { rackId: string; temp: number | null; powerPct: number | null; alarmCount: number; overlay: Overlay; feed?: "A" | "B"; cracColor?: string; onClick: () => void; tempWarn?: number; tempCrit?: number; }) { const offline = temp === null && powerPct === null; const bg = offline ? "oklch(0.22 0.02 265)" : overlay === "temp" ? tempBg(temp, tempWarn, tempCrit) : overlay === "power" ? powerBg(powerPct) : overlay === "feed" ? feedBg(feed) : overlay === "crac" ? (cracColor ?? "oklch(0.22 0.02 265)") : alarmBg(alarmCount); const shortId = rackId.replace("rack-", "").toUpperCase(); const mainVal = overlay === "temp" ? (temp !== null ? `${temp}°` : null) : overlay === "power" ? (powerPct !== null ? `${Math.round(powerPct)}%` : null) : overlay === "feed" ? (feed ?? null) : (alarmCount > 0 ? String(alarmCount) : null); const subVal = overlay === "temp" ? (powerPct !== null ? `${Math.round(powerPct)}%` : null) : overlay === "power" ? (temp !== null ? `${temp}°C` : null) : overlay === "feed" ? (temp !== null ? `${temp}°C` : null) : (temp !== null ? `${temp}°C` : null); return ( ); } // ── CRAC strip ──────────────────────────────────────────────────────────────── function CracStrip({ crac }: { crac: CracStatus | undefined }) { const online = crac?.state === "online"; return (
{crac?.crac_id.toUpperCase() ?? "CRAC"} {online ? : } {online ? "Online" : "Fault"}
{crac && online && (
Supply {crac.supply_temp ?? "—"}°C Return {crac.return_temp ?? "—"}°C {crac.delta !== null && ( ΔT 14 ? "text-destructive" : crac.delta > 11 ? "text-amber-400" : "text-green-400" )}>+{crac.delta}°C )} {crac.fan_pct !== null && ( Fan {crac.fan_pct}% )} {crac.cooling_capacity_pct !== null && ( Cap = 90 ? "text-destructive" : (crac.cooling_capacity_pct ?? 0) >= 75 ? "text-amber-400" : "text-foreground" )}>{crac.cooling_capacity_pct?.toFixed(0)}% )}
)}
); } // ── Room plan ───────────────────────────────────────────────────────────────── function RoomPlan({ roomId, layout, data, cracs, overlay, alarmsByRack, onRackClick, tempWarn = 26, tempCrit = 28, }: { roomId: string; layout: FloorLayout; data: CapacitySummary; cracs: CracStatus[]; overlay: Overlay; alarmsByRack: Map; onRackClick: (id: string) => void; tempWarn?: number; tempCrit?: number; }) { const roomLayout = layout[roomId]; if (!roomLayout) return null; const rackMap = new Map(data.racks.map((r) => [r.rack_id, r])); const crac = cracs.find((c) => c.crac_id === roomLayout.crac_id); const roomRacks = data.racks.filter((r) => r.room_id === roomId); const offlineCount = roomRacks.filter((r) => r.temp === null && r.power_kw === null).length; const avgTemp = (() => { const temps = roomRacks.map((r) => r.temp).filter((t): t is number => t !== null); return temps.length ? Math.round((temps.reduce((a, b) => a + b, 0) / temps.length) * 10) / 10 : null; })(); const totalPower = (() => { const powers = roomRacks.map((r) => r.power_kw).filter((p): p is number => p !== null); return powers.length ? Math.round(powers.reduce((a, b) => a + b, 0) * 10) / 10 : null; })(); return (
{roomRacks.length} racks {avgTemp !== null && ( Avg {avgTemp}°C )} {totalPower !== null && ( {totalPower} kW IT load )} {offlineCount > 0 && ( {offlineCount} offline )}
{({ zoomIn, zoomOut, resetTransform }) => ( <>
Drag to pan
HOT AISLE
{roomLayout.rows.map((row, rowIdx) => { const rowCracColor = CRAC_ZONE_COLORS[rowIdx % CRAC_ZONE_COLORS.length]; return (
{row.label}
{row.racks.map((rackId) => { const rack = rackMap.get(rackId); return ( onRackClick(rackId)} tempWarn={tempWarn} tempCrit={tempCrit} /> ); })}
{rowIdx < roomLayout.rows.length - 1 && (
COLD AISLE
)}
); })}
HOT AISLE
)}
); } // ── Leak sensor panel ───────────────────────────────────────────────────────── function LeakSensorPanel({ sensors }: { sensors: LeakSensorStatus[] }) { if (sensors.length === 0) return null; const active = sensors.filter((s) => s.state === "detected"); const byZone = sensors.reduce>((acc, s) => { const zone = s.floor_zone ?? "unknown"; (acc[zone] ??= []).push(s); return acc; }, {}); return ( 0 && "border-destructive/50")}>
0 ? "text-destructive" : "text-blue-400")} /> Leak Sensor Status 0 ? "bg-destructive/10 text-destructive" : "bg-green-500/10 text-green-400", )}> {active.length > 0 ? `${active.length} leak${active.length > 1 ? "s" : ""} detected` : "All clear"}
{Object.entries(byZone).map(([zone, zoneSensors]) => (

{zone}

{zoneSensors.map((s) => { const detected = s.state === "detected"; return (
{s.sensor_id}

{[ s.under_floor ? "Under floor" : "Surface mount", s.near_crac ? "near CRAC" : null, s.room_id ?? null, ].filter(Boolean).join(" · ")}

{detected ? "LEAK" : s.state === "unknown" ? "unknown" : "clear"}
); })}
))}
); } // ── Layout editor ───────────────────────────────────────────────────────────── function LayoutEditor({ layout, onSave, saving, }: { layout: FloorLayout; onSave: (l: FloorLayout) => void; saving?: boolean; }) { const [draft, setDraft] = useState(() => JSON.parse(JSON.stringify(layout))); const [newRoomId, setNewRoomId] = useState(""); const [newRoomLabel, setNewRoomLabel] = useState(""); const [newRoomCrac, setNewRoomCrac] = useState(""); const [expandedRoom, setExpandedRoom] = useState(Object.keys(draft)[0] ?? null); function updateRoom(roomId: string, patch: Partial) { setDraft(d => ({ ...d, [roomId]: { ...d[roomId], ...patch } })); } function deleteRoom(roomId: string) { setDraft(d => { const n = { ...d }; delete n[roomId]; return n; }); if (expandedRoom === roomId) setExpandedRoom(null); } function addRoom() { const id = newRoomId.trim().toLowerCase().replace(/\s+/g, "-"); if (!id || !newRoomLabel.trim() || draft[id]) return; setDraft(d => ({ ...d, [id]: { label: newRoomLabel.trim(), crac_id: newRoomCrac.trim(), rows: [] }, })); setNewRoomId(""); setNewRoomLabel(""); setNewRoomCrac(""); setExpandedRoom(id); } function addRow(roomId: string) { const room = draft[roomId]; const label = `Row ${room.rows.length + 1}`; updateRoom(roomId, { rows: [...room.rows, { label, racks: [] }] }); } function deleteRow(roomId: string, rowIdx: number) { const rows = draft[roomId].rows.filter((_, i) => i !== rowIdx); updateRoom(roomId, { rows }); } function updateRowLabel(roomId: string, rowIdx: number, label: string) { const rows = draft[roomId].rows.map((r, i) => i === rowIdx ? { ...r, label } : r); updateRoom(roomId, { rows }); } function addRack(roomId: string, rowIdx: number, rackId: string) { const id = rackId.trim(); if (!id) return; const rows = draft[roomId].rows.map((r, i) => i === rowIdx ? { ...r, racks: [...r.racks, id] } : r ); updateRoom(roomId, { rows }); } function removeRack(roomId: string, rowIdx: number, rackIdx: number) { const rows = draft[roomId].rows.map((r, i) => i === rowIdx ? { ...r, racks: r.racks.filter((_, j) => j !== rackIdx) } : r ); updateRoom(roomId, { rows }); } function moveRow(roomId: string, rowIdx: number, dir: -1 | 1) { const rows = [...draft[roomId].rows]; const target = rowIdx + dir; if (target < 0 || target >= rows.length) return; [rows[rowIdx], rows[target]] = [rows[target], rows[rowIdx]]; updateRoom(roomId, { rows }); } return (

Floor Layout Editor

Configure rooms, rows, and rack positions. Changes are saved for all users.

{Object.entries(draft).map(([roomId, room]) => (
{/* Room header */}
{expandedRoom === roomId && (
{/* Room fields */}
updateRoom(roomId, { label: e.target.value })} className="w-full h-7 rounded border border-border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary" />
updateRoom(roomId, { crac_id: e.target.value })} placeholder="e.g. crac-01" className="w-full h-7 rounded border border-border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary" />
{/* Rows */}

Rows

{room.rows.length === 0 && (

No rows — click Add Row

)} {room.rows.map((row, rowIdx) => (
updateRowLabel(roomId, rowIdx, e.target.value)} className="flex-1 h-6 rounded border border-border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary" />
{/* Racks in this row */}
{row.racks.map((rackId, rackIdx) => ( {rackId} ))} addRack(roomId, rowIdx, id)} />

Feed: {rowIdx % 2 === 0 ? "A (even rows)" : "B (odd rows)"} · {row.racks.length} rack{row.racks.length !== 1 ? "s" : ""}

))}
)}
))} {/* Add new room */}

Add New Room

setNewRoomId(e.target.value)} placeholder="room-id (e.g. hall-c)" className="h-7 rounded border border-border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary" /> setNewRoomLabel(e.target.value)} placeholder="Label (e.g. Hall C)" className="h-7 rounded border border-border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary" /> setNewRoomCrac(e.target.value)} placeholder="CRAC ID (e.g. crac-03)" className="h-7 rounded border border-border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary" />
{/* Footer actions */}
); } // Small inline input to add a rack ID to a row function RackAdder({ onAdd }: { onAdd: (id: string) => void }) { const [val, setVal] = useState(""); function submit() { if (val.trim()) { onAdd(val.trim()); setVal(""); } } return ( setVal(e.target.value)} onKeyDown={e => e.key === "Enter" && submit()} placeholder="rack-id" className="h-5 w-20 rounded border border-dashed border-border bg-background px-1.5 text-[10px] font-mono focus:outline-none focus:ring-1 focus:ring-primary" /> ); } // ── Page ────────────────────────────────────────────────────────────────────── export default function FloorMapPage() { const { thresholds } = useThresholds(); const [layout, setLayout] = useState(DEFAULT_LAYOUT); const [data, setData] = useState(null); const [cracs, setCracs] = useState([]); const [alarms, setAlarms] = useState([]); const [leakSensors, setLeakSensors] = useState([]); const [loading, setLoading] = useState(true); const [overlay, setOverlay] = useState("temp"); const [activeRoom, setActiveRoom] = useState(""); const [selectedRack, setSelectedRack] = useState(null); const [editorOpen, setEditorOpen] = useState(false); const [layoutSaving, setLayoutSaving] = useState(false); // Load layout from backend on mount useEffect(() => { fetchFloorLayout(SITE_ID) .then((remote) => { const parsed = remote as FloorLayout; setLayout(parsed); setActiveRoom(Object.keys(parsed)[0] ?? ""); }) .catch(() => { // No saved layout yet — use default setActiveRoom(Object.keys(DEFAULT_LAYOUT)[0] ?? ""); }); }, []); const alarmsByRack = new Map(); for (const a of alarms) { if (a.rack_id) alarmsByRack.set(a.rack_id, (alarmsByRack.get(a.rack_id) ?? 0) + 1); } const load = useCallback(async () => { try { const [d, c, a, ls] = await Promise.all([ fetchCapacitySummary(SITE_ID), fetchCracStatus(SITE_ID), fetchAlarms(SITE_ID, "active", 200), fetchLeakStatus(SITE_ID).catch(() => [] as LeakSensorStatus[]), ]); setData(d); setCracs(c); setAlarms(a); setLeakSensors(ls); } catch { /* keep stale */ } finally { setLoading(false); } }, []); useEffect(() => { load(); const id = setInterval(load, 30_000); return () => clearInterval(id); }, [load]); async function handleSaveLayout(newLayout: FloorLayout) { setLayoutSaving(true); try { await saveFloorLayout(SITE_ID, newLayout as unknown as Record); setLayout(newLayout); if (!newLayout[activeRoom]) setActiveRoom(Object.keys(newLayout)[0] ?? ""); setEditorOpen(false); } catch { // save failed — keep editor open so user can retry } finally { setLayoutSaving(false); } } const roomIds = Object.keys(layout); return (

Floor Map

Singapore DC01 — live rack layout · refreshes every 30s

{/* Overlay selector */}
{([ { val: "temp" as Overlay, icon: Thermometer, label: "Temperature" }, { val: "power" as Overlay, icon: Zap, label: "Power %" }, { val: "alarms" as Overlay, icon: AlertTriangle, label: "Alarms" }, { val: "feed" as Overlay, icon: Cable, label: "Power Feed" }, { val: "crac" as Overlay, icon: Wind, label: "CRAC Coverage" }, ]).map(({ val, icon: Icon, label }) => ( ))}
{/* Layout editor trigger */}
{loading ? ( ) : !data ? (
Unable to load floor map data.
) : ( <> setSelectedRack(null)} />
Room View {roomIds.length > 0 && ( {roomIds.map((id) => ( {layout[id].label} ))} )}
{activeRoom && layout[activeRoom] ? ( ) : (

No rooms configured

Use Edit Layout to add rooms and racks

)}
{/* Legend */}
{overlay === "alarms" ? ( <> Alarm count:
{([ { c: "oklch(0.22 0.02 265)", l: "0" }, { c: "oklch(0.65 0.20 45)", l: "1–2" }, { c: "oklch(0.55 0.22 25)", l: "3+" }, ]).map(({ c, l }) => ( {l} ))}
) : overlay === "feed" ? ( <> Power feed:
Feed A (even rows) Feed B (odd rows)
) : overlay === "crac" ? ( <> CRAC thermal zones:
{CRAC_ZONE_COLORS.slice(0, layout[activeRoom]?.rows.length ?? 2).map((c, i) => ( Zone {i + 1} ))}
) : ( <> {overlay === "temp" ? "Temperature:" : "Power utilisation:"}
{overlay === "temp" ? (["oklch(0.60 0.15 212)","oklch(0.68 0.14 162)","oklch(0.78 0.14 140)","oklch(0.72 0.18 84)","oklch(0.65 0.20 45)","oklch(0.55 0.22 25)"] as string[]).map((c, i) => ( )) : (["oklch(0.60 0.15 212)","oklch(0.68 0.14 162)","oklch(0.72 0.18 84)","oklch(0.65 0.20 45)","oklch(0.55 0.22 25)"] as string[]).map((c, i) => ( ))} {overlay === "temp" ? "Cool → Hot" : "Low → High"}
{overlay === "temp" && Warn: {thresholds.temp.warn}°C  |  Critical: {thresholds.temp.critical}°C} {overlay === "power" && Warn: 75%  |  Critical: 90%} )} Click any rack to drill down
)}
); }