"use client"; import { useEffect, useState, useCallback } from "react"; import { toast } from "sonner"; import { fetchCapacitySummary, type CapacitySummary, type RoomCapacity, type RackCapacity } from "@/lib/api"; import { PageShell } from "@/components/layout/page-shell"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ReferenceLine, ResponsiveContainer, Cell } from "recharts"; import { Zap, Wind, Server, RefreshCw, AlertTriangle, TrendingDown, TrendingUp, Clock } from "lucide-react"; import { cn } from "@/lib/utils"; const SITE_ID = "sg-01"; const ROOM_LABELS: Record = { "hall-a": "Hall A", "hall-b": "Hall B" }; // ── Radial gauge ────────────────────────────────────────────────────────────── function RadialGauge({ pct, warn, crit, headroom, unit }: { pct: number; warn: number; crit: number; headroom?: number; unit?: string }) { const r = 36; const circumference = 2 * Math.PI * r; const arc = circumference * 0.75; // 270° sweep const filled = Math.min(pct / 100, 1) * arc; const color = pct >= crit ? "#ef4444" : pct >= warn ? "#f59e0b" : "#22c55e"; const textColor = pct >= crit ? "text-destructive" : pct >= warn ? "text-amber-400" : "text-green-400"; return (
{/* Track */} {/* Fill */}
{pct.toFixed(1)} % {headroom !== undefined && unit !== undefined && (

{headroom.toFixed(1)} {unit}

)}
); } // ── Capacity gauge card ──────────────────────────────────────────────────────── function CapacityGauge({ label, used, capacity, unit, pct, headroom, icon: Icon, warn = 70, crit = 85, }: { label: string; used: number; capacity: number; unit: string; pct: number; headroom: number; icon: React.ElementType; warn?: number; crit?: number; }) { const textColor = pct >= crit ? "text-destructive" : pct >= warn ? "text-amber-400" : "text-green-400"; const status = pct >= crit ? "Critical" : pct >= warn ? "Warning" : "OK"; return (
{label}
= crit ? "bg-destructive/10" : pct >= warn ? "bg-amber-500/10" : "bg-green-500/10" )}> {status}
{used.toFixed(1)} {unit} used {capacity.toFixed(0)} {unit} rated
= crit ? "bg-destructive/10 text-destructive" : pct >= warn ? "bg-amber-500/10 text-amber-400" : "bg-green-500/10 text-green-400" )}> {headroom.toFixed(1)} {unit} headroom remaining
); } // ── Capacity runway component ────────────────────────────────────── // Assumes ~0.5 kW/week average growth rate to forecast when limits are hit const GROWTH_KW_WEEK = 0.5; const WARN_PCT = 85; function RunwayCard({ rooms }: { rooms: RoomCapacity[] }) { return ( Capacity Runway (assuming {GROWTH_KW_WEEK} kW/week growth)
{rooms.map((room) => { const powerHeadroomToWarn = Math.max(0, room.power.capacity_kw * (WARN_PCT / 100) - room.power.used_kw); const coolHeadroomToWarn = Math.max(0, room.cooling.capacity_kw * (WARN_PCT / 100) - room.cooling.load_kw); const powerRunwayWeeks = Math.round(powerHeadroomToWarn / GROWTH_KW_WEEK); const coolRunwayWeeks = Math.round(coolHeadroomToWarn / GROWTH_KW_WEEK); const constrainedBy = powerRunwayWeeks <= coolRunwayWeeks ? "power" : "cooling"; const minRunway = Math.min(powerRunwayWeeks, coolRunwayWeeks); const runwayColor = minRunway < 4 ? "text-destructive" : minRunway < 12 ? "text-amber-400" : "text-green-400"; // N+1 cooling: at 1 CRAC per room, losing it means all load hits chillers/other rooms const n1Margin = room.cooling.capacity_kw - room.cooling.load_kw; const n1Ok = n1Margin > room.cooling.capacity_kw * 0.2; // 20% spare = N+1 safe return (
{ROOM_LABELS[room.room_id] ?? room.room_id}
{n1Ok ? "N+1 OK" : "N+1 marginal"}

{minRunway}w

≈{(minRunway / 4.33).toFixed(1)}mo

until {WARN_PCT}% {constrainedBy} limit

Power runway

{powerRunwayWeeks}w / ≈{(powerRunwayWeeks / 4.33).toFixed(1)}mo

{powerHeadroomToWarn.toFixed(1)} kW free

Cooling runway

{coolRunwayWeeks}w / ≈{(coolRunwayWeeks / 4.33).toFixed(1)}mo

{coolHeadroomToWarn.toFixed(1)} kW free

); })}
); } // ── Room summary strip ──────────────────────────────────────────────────────── function RoomSummaryStrip({ rooms }: { rooms: RoomCapacity[] }) { return (
{rooms.map((room) => { const powerPct = room.power.pct; const coolPct = room.cooling.pct; const worstPct = Math.max(powerPct, coolPct); const worstColor = worstPct >= 85 ? "border-destructive/40 bg-destructive/5" : worstPct >= 70 ? "border-amber-500/40 bg-amber-500/5" : "border-border bg-muted/10"; return (
{ROOM_LABELS[room.room_id] ?? room.room_id} = 85 ? "bg-destructive/10 text-destructive" : worstPct >= 70 ? "bg-amber-500/10 text-amber-400" : "bg-green-500/10 text-green-400" )}> {worstPct >= 85 ? "Critical" : worstPct >= 70 ? "Warning" : "OK"}

Power

= 85 ? "text-destructive" : powerPct >= 70 ? "text-amber-400" : "text-green-400")}> {powerPct.toFixed(1)}%

{room.power.used_kw.toFixed(1)} / {room.power.capacity_kw} kW

Cooling

= 80 ? "text-destructive" : coolPct >= 65 ? "text-amber-400" : "text-green-400")}> {coolPct.toFixed(1)}%

{room.cooling.load_kw.toFixed(1)} / {room.cooling.capacity_kw} kW

Space

{room.space.racks_populated} / {room.space.racks_total}

{room.space.racks_total - room.space.racks_populated} slots free

); })}
); } // ── Room capacity section ───────────────────────────────────────────────────── function RoomCapacityPanel({ room, racks, config }: { room: RoomCapacity; racks: RackCapacity[]; config: CapacitySummary["config"]; }) { const roomRacks = racks.filter((r) => r.room_id === room.room_id); const chartData = roomRacks .map((r) => ({ rack: r.rack_id.replace("rack-", "").toUpperCase(), rack_id: r.rack_id, pct: r.power_pct ?? 0, kw: r.power_kw ?? 0, temp: r.temp, })) .sort((a, b) => b.pct - a.pct); const forecastPct = Math.min(100, (chartData.reduce((s, d) => s + d.pct, 0) / Math.max(1, chartData.length)) + (GROWTH_KW_WEEK * 13 / config.rack_power_kw * 100)); const highLoad = roomRacks.filter((r) => (r.power_pct ?? 0) >= 75); const stranded = roomRacks.filter((r) => r.power_kw !== null && (r.power_pct ?? 0) < 20); const strandedKw = stranded.reduce((s, r) => s + ((config.rack_power_kw - (r.power_kw ?? 0))), 0); return (
Space
{room.space.racks_populated} /{room.space.racks_total}
{room.space.racks_populated} active {room.space.racks_total - room.space.racks_populated} free
Each rack rated {config.rack_u_total}U / {config.rack_power_kw} kW max
Per-rack Power Utilisation {chartData.length === 0 ? (
No rack data available
) : ( `${v}%`} /> [ `${Number(v).toFixed(1)}% (${props.payload.kw.toFixed(2)} kW)`, "Power load" ]} /> {chartData.map((d) => ( = 90 ? "oklch(0.55 0.22 25)" : d.pct >= 75 ? "oklch(0.65 0.20 45)" : d.pct >= 50 ? "oklch(0.68 0.14 162)" : "oklch(0.62 0.17 212)" } /> ))} )}
High Load Racks {highLoad.length === 0 ? (

All racks within normal limits

) : (
{highLoad.sort((a, b) => (b.power_pct ?? 0) - (a.power_pct ?? 0)).map((r) => (
{r.rack_id.toUpperCase()}
{r.power_kw?.toFixed(1)} kW = 90 ? "text-destructive" : "text-amber-400" )}>{r.power_pct?.toFixed(1)}%
))}
)}
Stranded Capacity {stranded.length > 0 && ( {strandedKw.toFixed(1)} kW recoverable )}
{stranded.length === 0 ? (

No underutilised racks detected

) : (
{stranded.sort((a, b) => (a.power_pct ?? 0) - (b.power_pct ?? 0)).map((r) => (
{r.rack_id.toUpperCase()}
{r.power_kw?.toFixed(1)} kW {r.power_pct?.toFixed(1)}% utilised
))}

{stranded.length} rack{stranded.length > 1 ? "s" : ""} below 20% — consider consolidation

)}
); } // ── Page ────────────────────────────────────────────────────────────────────── export default function CapacityPage() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [activeRoom, setActiveRoom] = useState("hall-a"); const load = useCallback(async () => { try { setData(await fetchCapacitySummary(SITE_ID)); } catch { toast.error("Failed to load capacity data"); } finally { setLoading(false); } }, []); useEffect(() => { load(); const id = setInterval(load, 30_000); return () => clearInterval(id); }, [load]); const sitePower = data?.rooms.reduce((s, r) => s + r.power.used_kw, 0) ?? 0; const siteCapacity = data?.rooms.reduce((s, r) => s + r.power.capacity_kw, 0) ?? 0; return (

Capacity Planning

Singapore DC01 — power, cooling & space headroom

{loading ? (
{Array.from({ length: 3 }).map((_, i) => )}
) : !data ? (
Unable to load capacity data.
) : ( <> {/* Site summary banner */}
Site IT load {" "} {sitePower.toFixed(1)} kW / {siteCapacity.toFixed(0)} kW rated
Site load {" "} = 85 ? "text-destructive" : (sitePower / siteCapacity * 100) >= 70 ? "text-amber-400" : "text-green-400" )}> {(sitePower / siteCapacity * 100).toFixed(1)}%
Capacity config: {data.config.rack_power_kw} kW/rack ·{" "} {data.config.crac_cooling_kw} kW CRAC ·{" "} {data.config.rack_u_total}U/rack
{/* Room comparison strip */} {/* Capacity runway + N+1 */} {/* Per-room detail tabs */}
{data.rooms.map((r) => ( {ROOM_LABELS[r.room_id] ?? r.room_id} {r.power.pct >= 85 && ( )} ))}
{data.rooms .filter((r) => r.room_id === activeRoom) .map((room) => ( ))}
)}
); }