first commit

This commit is contained in:
mega 2026-03-19 11:32:17 +00:00
commit 4b98219bf7
144 changed files with 31561 additions and 0 deletions

View file

@ -0,0 +1,610 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { toast } from "sonner";
import { fetchCracStatus, fetchChillerStatus, type CracStatus, type ChillerStatus } from "@/lib/api";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { CracDetailSheet } from "@/components/dashboard/crac-detail-sheet";
import {
Wind, AlertTriangle, CheckCircle2, Zap, ChevronRight, ArrowRight, Waves, Filter,
ChevronUp, ChevronDown,
} from "lucide-react";
import { cn } from "@/lib/utils";
const SITE_ID = "sg-01";
const roomLabels: Record<string, string> = { "hall-a": "Hall A", "hall-b": "Hall B" };
function fmt(v: number | null | undefined, dec = 1, unit = "") {
if (v == null) return "—";
return `${v.toFixed(dec)}${unit}`;
}
function FillBar({
value, max, color, warn, crit, height = "h-2",
}: {
value: number | null; max: number; color: string;
warn?: number; crit?: number; height?: string;
}) {
const pct = value != null ? Math.min(100, (value / max) * 100) : 0;
const barColor =
crit && value != null && value >= crit ? "#ef4444" :
warn && value != null && value >= warn ? "#f59e0b" :
color;
return (
<div className={cn("rounded-full bg-muted overflow-hidden w-full", height)}>
<div
className="h-full rounded-full transition-all duration-500"
style={{ width: `${pct}%`, backgroundColor: barColor }}
/>
</div>
);
}
function KpiTile({ label, value, sub, warn }: {
label: string; value: string; sub?: string; warn?: boolean;
}) {
return (
<div className="bg-muted/30 rounded-lg px-4 py-3 flex-1 min-w-0">
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">{label}</p>
<p className={cn("text-xl font-bold tabular-nums", warn && "text-amber-400")}>{value}</p>
{sub && <p className="text-[10px] text-muted-foreground mt-0.5">{sub}</p>}
</div>
);
}
function CracCard({ crac, onOpen }: { crac: CracStatus; onOpen: () => void }) {
const [showCompressor, setShowCompressor] = useState(false);
const online = crac.state === "online";
const deltaWarn = (crac.delta ?? 0) > 11;
const deltaCrit = (crac.delta ?? 0) > 14;
const capWarn = (crac.cooling_capacity_pct ?? 0) > 75;
const capCrit = (crac.cooling_capacity_pct ?? 0) > 90;
const copWarn = (crac.cop ?? 99) < 1.5;
const filterWarn = (crac.filter_dp_pa ?? 0) > 80;
const filterCrit = (crac.filter_dp_pa ?? 0) > 120;
const compWarn = (crac.compressor_load_pct ?? 0) > 95;
const hiPWarn = (crac.high_pressure_bar ?? 0) > 22;
const loPWarn = (crac.low_pressure_bar ?? 99) < 3;
return (
<Card
className={cn(
"border cursor-pointer hover:border-primary/50 transition-colors",
!online && "border-destructive/40",
)}
onClick={onOpen}
>
{/* ── Header ───────────────────────────────────────────────── */}
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Wind className={cn("w-4 h-4", online ? "text-primary" : "text-destructive")} />
<div>
<CardTitle className="text-base font-semibold leading-none">
{crac.crac_id.toUpperCase()}
</CardTitle>
{crac.room_id && (
<p className="text-xs text-muted-foreground mt-0.5">
{roomLabels[crac.room_id] ?? crac.room_id}
</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
{online && (
<span className={cn(
"text-[10px] font-semibold px-2 py-0.5 rounded-full uppercase tracking-wide",
deltaCrit || capCrit ? "bg-destructive/10 text-destructive" :
deltaWarn || capWarn || filterWarn || copWarn ? "bg-amber-500/10 text-amber-400" :
"bg-green-500/10 text-green-400",
)}>
{deltaCrit || capCrit ? "Critical" : deltaWarn || capWarn || filterWarn || copWarn ? "Warning" : "Normal"}
</span>
)}
<span className={cn(
"flex items-center gap-1 text-[10px] font-semibold px-2.5 py-1 rounded-full uppercase tracking-wide",
online ? "bg-green-500/10 text-green-400" : "bg-destructive/10 text-destructive",
)}>
{online
? <><CheckCircle2 className="w-3 h-3" /> Online</>
: <><AlertTriangle className="w-3 h-3" /> Fault</>}
</span>
<ChevronRight className="w-4 h-4 text-muted-foreground" />
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{!online ? (
<div className="rounded-md px-3 py-2 text-xs bg-destructive/10 text-destructive">
Unit offline cooling capacity in this room is degraded.
</div>
) : (
<>
{/* ── Thermal hero ─────────────────────────────────────── */}
<div className="rounded-lg bg-muted/20 px-4 py-3">
<div className="flex items-center justify-between">
<div className="text-center">
<p className="text-[10px] text-muted-foreground uppercase tracking-wide mb-0.5">Supply</p>
<p className="text-3xl font-bold tabular-nums text-blue-400">
{fmt(crac.supply_temp, 1)}°C
</p>
</div>
<div className="flex-1 flex flex-col items-center gap-1 px-4">
<p className={cn(
"text-base font-bold tabular-nums",
deltaCrit ? "text-destructive" : deltaWarn ? "text-amber-400" : "text-muted-foreground",
)}>
ΔT {fmt(crac.delta, 1)}°C
</p>
<div className="flex items-center gap-1 w-full">
<div className="flex-1 h-px bg-muted-foreground/30" />
<ArrowRight className="w-3 h-3 text-muted-foreground/50 shrink-0" />
<div className="flex-1 h-px bg-muted-foreground/30" />
</div>
</div>
<div className="text-center">
<p className="text-[10px] text-muted-foreground uppercase tracking-wide mb-0.5">Return</p>
<p className={cn(
"text-3xl font-bold tabular-nums",
deltaCrit ? "text-destructive" : deltaWarn ? "text-amber-400" : "text-orange-400",
)}>
{fmt(crac.return_temp, 1)}°C
</p>
</div>
</div>
</div>
{/* ── Cooling capacity ─────────────────────────────────── */}
<div>
<div className="flex justify-between items-baseline mb-1.5">
<span className="text-[10px] text-muted-foreground uppercase tracking-wide">Cooling Capacity</span>
<span className="text-xs font-mono">
<span className={cn(capCrit ? "text-destructive" : capWarn ? "text-amber-400" : "text-foreground")}>
{fmt(crac.cooling_capacity_kw, 1)} / {crac.rated_capacity_kw} kW
</span>
<span className="text-muted-foreground mx-1.5">·</span>
<span className={cn(copWarn ? "text-amber-400" : "text-foreground")}>
COP {fmt(crac.cop, 2)}
</span>
</span>
</div>
<FillBar value={crac.cooling_capacity_pct} max={100} color="#34d399" warn={75} crit={90} />
<p className={cn(
"text-[10px] mt-1 text-right",
capCrit ? "text-destructive" : capWarn ? "text-amber-400" : "text-muted-foreground",
)}>
{fmt(crac.cooling_capacity_pct, 1)}% utilised
</p>
</div>
{/* ── Fan + Filter ─────────────────────────────────────── */}
<div className="space-y-2.5">
<div>
<div className="flex justify-between text-[10px] mb-1">
<span className="text-muted-foreground">Fan</span>
<span className="font-mono text-foreground">
{fmt(crac.fan_pct, 1)}%
{crac.fan_rpm != null ? ` · ${Math.round(crac.fan_rpm).toLocaleString()} rpm` : ""}
</span>
</div>
<FillBar value={crac.fan_pct} max={100} color="#60a5fa" height="h-1.5" />
</div>
<div>
<div className="flex justify-between text-[10px] mb-1">
<span className="text-muted-foreground">Filter ΔP</span>
<span className={cn(
"font-mono",
filterCrit ? "text-destructive" : filterWarn ? "text-amber-400" : "text-foreground",
)}>
{fmt(crac.filter_dp_pa, 0)} Pa
{!filterWarn && <span className="text-green-400 ml-1.5"></span>}
</span>
</div>
<FillBar value={crac.filter_dp_pa} max={150} color="#94a3b8" warn={80} crit={120} height="h-1.5" />
</div>
</div>
{/* ── Compressor (collapsible) ─────────────────────────── */}
<div className="rounded-md bg-muted/20 px-3 py-2.5 space-y-2">
<button
onClick={(e) => { e.stopPropagation(); setShowCompressor(!showCompressor); }}
className="flex items-center justify-between w-full text-[10px] text-muted-foreground hover:text-foreground transition-colors"
>
<span className="uppercase tracking-wide font-semibold">Compressor</span>
<span className="flex items-center gap-2">
<span className="font-mono">{fmt(crac.compressor_load_pct, 1)}% · {fmt(crac.compressor_power_kw, 2)} kW</span>
{showCompressor ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
</span>
</button>
{showCompressor && (
<div className="pt-2 space-y-2">
<FillBar value={crac.compressor_load_pct} max={100} color="#e879f9" warn={80} crit={95} height="h-1.5" />
<div className="flex justify-between text-[10px] text-muted-foreground pt-0.5">
<span className={cn(hiPWarn ? "text-destructive" : "")}>
Hi {fmt(crac.high_pressure_bar, 1)} bar
</span>
<span className={cn(loPWarn ? "text-destructive" : "")}>
Lo {fmt(crac.low_pressure_bar, 2)} bar
</span>
<span>SH {fmt(crac.discharge_superheat_c, 1)}°C</span>
<span>SC {fmt(crac.liquid_subcooling_c, 1)}°C</span>
</div>
</div>
)}
</div>
{/* ── Electrical (one line) ────────────────────────────── */}
<div className="flex items-center gap-1.5 text-xs text-muted-foreground border-t border-border/30 pt-3">
<Zap className="w-3 h-3 shrink-0" />
<span className="font-mono font-medium text-foreground">{fmt(crac.total_unit_power_kw, 2)} kW</span>
<span>·</span>
<span className="font-mono">{fmt(crac.input_voltage_v, 0)} V</span>
<span>·</span>
<span className="font-mono">{fmt(crac.input_current_a, 1)} A</span>
<span>·</span>
<span className="font-mono">PF {fmt(crac.power_factor, 3)}</span>
</div>
{/* ── Status banner ────────────────────────────────────── */}
<div className={cn(
"rounded-md px-3 py-2 text-xs",
deltaCrit || capCrit
? "bg-destructive/10 text-destructive"
: deltaWarn || capWarn || filterWarn || copWarn
? "bg-amber-500/10 text-amber-400"
: "bg-green-500/10 text-green-400",
)}>
{deltaCrit || capCrit
? "Heat load is high — check airflow or redistribute rack density."
: deltaWarn || capWarn
? "Heat load is elevated — monitor for further rises."
: filterWarn
? "Filter requires attention — airflow may be restricted."
: copWarn
? "Running inefficiently — check refrigerant charge."
: "Operating efficiently within normal parameters."}
</div>
</>
)}
</CardContent>
</Card>
);
}
// ── Filter replacement estimate ────────────────────────────────────
// Assumes ~1.2 Pa/day rate of rise — replace at 120 Pa threshold
const FILTER_REPLACE_PA = 120;
const FILTER_RATE_PA_DAY = 1.2;
function FilterEstimate({ cracs }: { cracs: CracStatus[] }) {
const units = cracs
.filter((c) => c.state === "online" && c.filter_dp_pa != null)
.map((c) => {
const dp = c.filter_dp_pa!;
const days = Math.max(0, Math.round((FILTER_REPLACE_PA - dp) / FILTER_RATE_PA_DAY));
const urgent = dp >= 120;
const warn = dp >= 80;
return { crac_id: c.crac_id, dp, days, urgent, warn };
})
.sort((a, b) => a.days - b.days);
if (units.length === 0) return null;
const anyUrgent = units.some((u) => u.urgent);
const anyWarn = units.some((u) => u.warn);
return (
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<Filter className="w-4 h-4 text-muted-foreground" />
Predictive Filter Replacement
</CardTitle>
<span className={cn(
"text-[10px] font-semibold px-2 py-0.5 rounded-full uppercase",
anyUrgent ? "bg-destructive/10 text-destructive" :
anyWarn ? "bg-amber-500/10 text-amber-400" :
"bg-green-500/10 text-green-400",
)}>
{anyUrgent ? "Overdue" : anyWarn ? "Attention needed" : "All filters OK"}
</span>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
{units.map((u) => (
<div key={u.crac_id}>
<div className="flex items-center justify-between text-xs mb-1">
<span className="font-medium">{u.crac_id.toUpperCase()}</span>
<div className="flex items-center gap-3">
<span className={cn(
"font-mono",
u.urgent ? "text-destructive" : u.warn ? "text-amber-400" : "text-muted-foreground",
)}>
{u.dp} Pa
</span>
<span className={cn(
"text-[10px] px-2 py-0.5 rounded-full font-semibold",
u.urgent ? "bg-destructive/10 text-destructive" :
u.warn ? "bg-amber-500/10 text-amber-400" :
"bg-green-500/10 text-green-400",
)}>
{u.urgent ? "Replace now" : `~${u.days}d`}
</span>
</div>
</div>
<div className="rounded-full bg-muted overflow-hidden h-1.5">
<div
className="h-full rounded-full transition-all duration-500"
style={{
width: `${Math.min(100, (u.dp / FILTER_REPLACE_PA) * 100)}%`,
backgroundColor: u.urgent ? "#ef4444" : u.warn ? "#f59e0b" : "#94a3b8",
}}
/>
</div>
</div>
))}
<p className="text-[10px] text-muted-foreground pt-1">
Estimated at {FILTER_RATE_PA_DAY} Pa/day increase · replace at {FILTER_REPLACE_PA} Pa threshold
</p>
</div>
</CardContent>
</Card>
);
}
// ── Chiller card ──────────────────────────────────────────────────
function ChillerCard({ chiller }: { chiller: ChillerStatus }) {
const online = chiller.state === "online";
const loadWarn = (chiller.cooling_load_pct ?? 0) > 80;
return (
<Card className={cn("border", !online && "border-destructive/40")}>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<Waves className="w-4 h-4 text-blue-400" />
{chiller.chiller_id.toUpperCase()} Chiller Plant
</CardTitle>
<span className={cn("text-[10px] font-semibold px-2.5 py-1 rounded-full uppercase tracking-wide",
online ? "bg-green-500/10 text-green-400" : "bg-destructive/10 text-destructive",
)}>
{online ? <><CheckCircle2 className="w-3 h-3 inline mr-0.5" /> Online</> : <><AlertTriangle className="w-3 h-3 inline mr-0.5" /> Fault</>}
</span>
</div>
</CardHeader>
<CardContent className="space-y-3">
{!online ? (
<div className="rounded-md px-3 py-2 text-xs bg-destructive/10 text-destructive">
Chiller fault CHW supply lost. CRAC/CRAH units relying on local refrigerant circuits only.
</div>
) : (
<>
{/* CHW temps */}
<div className="rounded-lg bg-muted/20 px-4 py-3">
<div className="flex items-center justify-between">
<div className="text-center">
<p className="text-[10px] text-muted-foreground uppercase tracking-wide mb-0.5">CHW Supply</p>
<p className="text-2xl font-bold tabular-nums text-blue-400">{fmt(chiller.chw_supply_c, 1)}°C</p>
</div>
<div className="flex-1 flex flex-col items-center gap-1 px-4">
<p className="text-sm font-bold tabular-nums text-muted-foreground">ΔT {fmt(chiller.chw_delta_c, 1)}°C</p>
<div className="flex items-center gap-1 w-full">
<div className="flex-1 h-px bg-muted-foreground/30" />
<ArrowRight className="w-3 h-3 text-muted-foreground/50 shrink-0" />
<div className="flex-1 h-px bg-muted-foreground/30" />
</div>
</div>
<div className="text-center">
<p className="text-[10px] text-muted-foreground uppercase tracking-wide mb-0.5">CHW Return</p>
<p className="text-2xl font-bold tabular-nums text-orange-400">{fmt(chiller.chw_return_c, 1)}°C</p>
</div>
</div>
</div>
{/* Load */}
<div>
<div className="flex justify-between items-baseline mb-1.5">
<span className="text-[10px] text-muted-foreground uppercase tracking-wide">Cooling Load</span>
<span className="text-xs font-mono">
<span className={cn(loadWarn ? "text-amber-400" : "")}>{fmt(chiller.cooling_load_kw, 1)} kW</span>
<span className="text-muted-foreground mx-1.5">·</span>
<span>COP {fmt(chiller.cop, 2)}</span>
</span>
</div>
<FillBar value={chiller.cooling_load_pct} max={100} color="#34d399" warn={80} crit={95} />
<p className="text-[10px] mt-1 text-right text-muted-foreground">{fmt(chiller.cooling_load_pct, 1)}% load</p>
</div>
{/* Details */}
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 text-[11px]">
<div className="flex justify-between"><span className="text-muted-foreground">Flow rate</span><span className="font-mono">{fmt(chiller.flow_gpm, 0)} GPM</span></div>
<div className="flex justify-between"><span className="text-muted-foreground">Comp load</span><span className="font-mono">{fmt(chiller.compressor_load_pct, 1)}%</span></div>
<div className="flex justify-between"><span className="text-muted-foreground">Cond press</span><span className="font-mono">{fmt(chiller.condenser_pressure_bar, 2)} bar</span></div>
<div className="flex justify-between"><span className="text-muted-foreground">Evap press</span><span className="font-mono">{fmt(chiller.evaporator_pressure_bar, 2)} bar</span></div>
<div className="flex justify-between"><span className="text-muted-foreground">CW supply</span><span className="font-mono">{fmt(chiller.cw_supply_c, 1)}°C</span></div>
<div className="flex justify-between"><span className="text-muted-foreground">CW return</span><span className="font-mono">{fmt(chiller.cw_return_c, 1)}°C</span></div>
</div>
<div className="text-[10px] text-muted-foreground border-t border-border/30 pt-2">
Run hours: <strong className="text-foreground">{chiller.run_hours != null ? chiller.run_hours.toFixed(0) : "—"} h</strong>
</div>
</>
)}
</CardContent>
</Card>
);
}
// ── Page ──────────────────────────────────────────────────────────────────────
export default function CoolingPage() {
const [cracs, setCracs] = useState<CracStatus[]>([]);
const [chillers, setChillers] = useState<ChillerStatus[]>([]);
const [loading, setLoading] = useState(true);
const [selectedCrac, setSelected] = useState<string | null>(null);
const load = useCallback(async () => {
try {
const [c, ch] = await Promise.all([
fetchCracStatus(SITE_ID),
fetchChillerStatus(SITE_ID).catch(() => []),
]);
setCracs(c);
setChillers(ch);
}
catch { toast.error("Failed to load cooling data"); }
finally { setLoading(false); }
}, []);
useEffect(() => {
load();
const id = setInterval(load, 30_000);
return () => clearInterval(id);
}, [load]);
const online = cracs.filter(c => c.state === "online");
const anyFaulted = cracs.some(c => c.state === "fault");
const totalCoolingKw = online.reduce((s, c) => s + (c.cooling_capacity_kw ?? 0), 0);
const totalRatedKw = cracs.reduce((s, c) => s + (c.rated_capacity_kw ?? 0), 0);
const copUnits = online.filter(c => c.cop != null);
const avgCop = copUnits.length > 0
? copUnits.reduce((s, c) => s + (c.cop ?? 0), 0) / copUnits.length
: null;
const totalUnitPower = online.reduce((s, c) => s + (c.total_unit_power_kw ?? 0), 0);
const totalAirflowCfm = online.reduce((s, c) => s + (c.airflow_cfm ?? 0), 0);
return (
<div className="p-6 space-y-6">
{/* ── Page header ───────────────────────────────────────────── */}
<div className="flex items-start justify-between">
<div>
<h1 className="text-xl font-semibold">Cooling Systems</h1>
<p className="text-sm text-muted-foreground">
Singapore DC01 · click a unit to drill down · refreshes every 30s
</p>
</div>
{!loading && (
<span className={cn(
"flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-full",
anyFaulted ? "bg-destructive/10 text-destructive" : "bg-green-500/10 text-green-400",
)}>
{anyFaulted
? <><AlertTriangle className="w-3.5 h-3.5" /> Cooling fault detected</>
: <><CheckCircle2 className="w-3.5 h-3.5" /> All {cracs.length} units operational</>}
</span>
)}
</div>
{/* ── Filter alert banner ───────────────────────────────────── */}
{!loading && (() => {
const urgent = cracs
.filter(c => c.state === "online" && c.filter_dp_pa != null)
.map(c => ({ id: c.crac_id, days: Math.max(0, Math.round((120 - c.filter_dp_pa!) / 1.2)) }))
.filter(c => c.days < 14)
.sort((a, b) => a.days - b.days);
if (urgent.length === 0) return null;
return (
<div className="flex items-center gap-3 rounded-lg border border-amber-500/30 bg-amber-500/5 px-4 py-3 text-sm">
<Filter className="w-4 h-4 text-amber-400 shrink-0" />
<span className="text-amber-400">
<strong>Filter replacement due:</strong>{" "}
{urgent.map(u => `${u.id.toUpperCase()} in ${u.days === 0 ? "now" : `~${u.days}d`}`).join(", ")}
</span>
</div>
);
})()}
{/* ── Fleet summary KPI cards ───────────────────────────────── */}
{loading && (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
{Array.from({ length: 5 }).map((_, i) => <Skeleton key={i} className="h-20" />)}
</div>
)}
{!loading && cracs.length > 0 && (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
<Card>
<CardContent className="p-4">
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">Cooling Load</p>
<p className="text-2xl font-bold tabular-nums">{totalCoolingKw.toFixed(1)} kW</p>
<p className="text-[10px] text-muted-foreground mt-0.5">of {totalRatedKw} kW rated</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">Avg COP</p>
<p className={cn("text-2xl font-bold tabular-nums", avgCop != null && avgCop < 1.5 && "text-amber-400")}>
{avgCop != null ? avgCop.toFixed(2) : "—"}
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">Unit Power Draw</p>
<p className="text-2xl font-bold tabular-nums">{totalUnitPower.toFixed(1)} kW</p>
<p className="text-[10px] text-muted-foreground mt-0.5">total electrical input</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">Units Online</p>
<p className={cn("text-2xl font-bold tabular-nums", anyFaulted && "text-amber-400")}>
{online.length} / {cracs.length}
</p>
</CardContent>
</Card>
{totalAirflowCfm > 0 && (
<Card>
<CardContent className="p-4">
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">Total Airflow</p>
<p className="text-2xl font-bold tabular-nums">{Math.round(totalAirflowCfm).toLocaleString()}</p>
<p className="text-[10px] text-muted-foreground mt-0.5">CFM combined output</p>
</CardContent>
</Card>
)}
</div>
)}
{/* ── Chiller plant ─────────────────────────────────────────── */}
{(loading || chillers.length > 0) && (
<>
<h2 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">Chiller Plant</h2>
{loading ? (
<Skeleton className="h-56" />
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{chillers.map(ch => <ChillerCard key={ch.chiller_id} chiller={ch} />)}
</div>
)}
</>
)}
{/* ── Filter health (moved before CRAC cards) ───────────────── */}
{!loading && <FilterEstimate cracs={cracs} />}
{/* ── CRAC cards ────────────────────────────────────────────── */}
<h2 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">CRAC / CRAH Units</h2>
{loading ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Skeleton className="h-72" />
<Skeleton className="h-72" />
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{cracs.map(crac => (
<CracCard key={crac.crac_id} crac={crac} onOpen={() => setSelected(crac.crac_id)} />
))}
</div>
)}
<CracDetailSheet
siteId={SITE_ID}
cracId={selectedCrac}
onClose={() => setSelected(null)}
/>
</div>
);
}