BMS/frontend/app/(dashboard)/dashboard/page.tsx
2026-03-19 11:32:17 +00:00

246 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import { Zap, Thermometer, Wind, AlertTriangle, Wifi, WifiOff, Fuel, Droplets } from "lucide-react";
import { KpiCard } from "@/components/dashboard/kpi-card";
import { PowerTrendChart } from "@/components/dashboard/power-trend-chart";
import { TemperatureTrendChart } from "@/components/dashboard/temperature-trend-chart";
import { AlarmFeed } from "@/components/dashboard/alarm-feed";
import { MiniFloorMap } from "@/components/dashboard/mini-floor-map";
import { RackDetailSheet } from "@/components/dashboard/rack-detail-sheet";
import {
fetchKpis, fetchPowerHistory, fetchTempHistory,
fetchAlarms, fetchGeneratorStatus, fetchLeakStatus,
fetchCapacitySummary, fetchFloorLayout,
type KpiData, type PowerBucket, type TempBucket,
type Alarm, type GeneratorStatus, type LeakSensorStatus,
type RackCapacity,
} from "@/lib/api";
import { TimeRangePicker } from "@/components/ui/time-range-picker";
import Link from "next/link";
import { PageShell } from "@/components/layout/page-shell";
const SITE_ID = "sg-01";
const KPI_INTERVAL = 15_000;
const CHART_INTERVAL = 30_000;
// Fallback static data shown when the API is unreachable
const FALLBACK_KPIS: KpiData = {
total_power_kw: 0, pue: 0, avg_temperature: 0, active_alarms: 0,
};
export default function DashboardPage() {
const router = useRouter();
const [kpis, setKpis] = useState<KpiData>(FALLBACK_KPIS);
const [prevKpis, setPrevKpis] = useState<KpiData | null>(null);
const [powerHistory, setPowerHistory] = useState<PowerBucket[]>([]);
const [tempHistory, setTempHistory] = useState<TempBucket[]>([]);
const [alarms, setAlarms] = useState<Alarm[]>([]);
const [generators, setGenerators] = useState<GeneratorStatus[]>([]);
const [leakSensors, setLeakSensors] = useState<LeakSensorStatus[]>([]);
const [mapRacks, setMapRacks] = useState<RackCapacity[]>([]);
const [mapLayout, setMapLayout] = useState<Record<string, { label: string; crac_id: string; rows: { label: string; racks: string[] }[] }> | null>(null);
const [chartHours, setChartHours] = useState(1);
const [loading, setLoading] = useState(true);
const [liveError, setLiveError] = useState(false);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [selectedRack, setSelectedRack] = useState<string | null>(null);
const refreshKpis = useCallback(async () => {
try {
const [k, a, g, l, cap] = await Promise.all([
fetchKpis(SITE_ID),
fetchAlarms(SITE_ID),
fetchGeneratorStatus(SITE_ID).catch(() => []),
fetchLeakStatus(SITE_ID).catch(() => []),
fetchCapacitySummary(SITE_ID).catch(() => null),
]);
setKpis((current) => {
if (current !== FALLBACK_KPIS) setPrevKpis(current);
return k;
});
setAlarms(a);
setGenerators(g);
setLeakSensors(l);
if (cap) setMapRacks(cap.racks);
setLiveError(false);
setLastUpdated(new Date());
} catch {
setLiveError(true);
}
}, []);
const refreshCharts = useCallback(async () => {
try {
const [p, t] = await Promise.all([
fetchPowerHistory(SITE_ID, chartHours),
fetchTempHistory(SITE_ID, chartHours),
]);
setPowerHistory(p);
setTempHistory(t);
} catch {
// keep previous chart data on failure
}
}, []);
// Initial load
useEffect(() => {
Promise.all([refreshKpis(), refreshCharts()]).finally(() => setLoading(false));
fetchFloorLayout(SITE_ID)
.then(l => setMapLayout(l as typeof mapLayout))
.catch(() => {});
}, [refreshKpis, refreshCharts]);
// Re-fetch charts when time range changes
useEffect(() => { refreshCharts(); }, [chartHours, refreshCharts]);
// Polling
useEffect(() => {
const kpiTimer = setInterval(refreshKpis, KPI_INTERVAL);
const chartTimer = setInterval(refreshCharts, CHART_INTERVAL);
return () => { clearInterval(kpiTimer); clearInterval(chartTimer); };
}, [refreshKpis, refreshCharts]);
function handleAlarmClick(alarm: Alarm) {
if (alarm.rack_id) {
setSelectedRack(alarm.rack_id);
} else if (alarm.room_id) {
router.push("/environmental");
} else {
router.push("/alarms");
}
}
// Derived KPI display values
const alarmStatus = kpis.active_alarms === 0 ? "ok"
: kpis.active_alarms <= 2 ? "warning" : "critical";
const tempStatus = kpis.avg_temperature === 0 ? "ok"
: kpis.avg_temperature >= 28 ? "critical"
: kpis.avg_temperature >= 25 ? "warning" : "ok";
// Trends vs previous poll
const powerTrend = prevKpis ? Math.round((kpis.total_power_kw - prevKpis.total_power_kw) * 10) / 10 : null;
const tempTrend = prevKpis ? Math.round((kpis.avg_temperature - prevKpis.avg_temperature) * 10) / 10 : null;
const alarmTrend = prevKpis ? kpis.active_alarms - prevKpis.active_alarms : null;
// Generator derived
const gen = generators[0] ?? null;
const genFuel = gen?.fuel_pct ?? null;
const genState = gen?.state ?? "unknown";
const genStatus: "ok" | "warning" | "critical" =
genState === "fault" ? "critical" :
genState === "running" ? "warning" :
genFuel !== null && genFuel < 25 ? "warning" : "ok";
// Leak derived
const activeLeaks = leakSensors.filter(s => s.state === "detected").length;
const leakStatus: "ok" | "warning" | "critical" = activeLeaks > 0 ? "critical" : "ok";
return (
<PageShell>
<RackDetailSheet siteId="sg-01" rackId={selectedRack} onClose={() => setSelectedRack(null)} />
{/* Live status bar */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{liveError ? (
<><WifiOff className="w-3 h-3 text-destructive" /> Live data unavailable</>
) : (
<><Wifi className="w-3 h-3 text-green-400" /> Live · updates every 15s</>
)}
</div>
{lastUpdated && (
<span className="text-xs text-muted-foreground">
Last updated {lastUpdated.toLocaleTimeString()}
</span>
)}
</div>
{/* Unified KPI grid — 3×2 on desktop */}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-3">
<KpiCard
title="Total Power"
value={loading ? "—" : `${kpis.total_power_kw} kW`}
icon={Zap}
iconColor="text-amber-400"
status="ok"
loading={loading}
trend={powerTrend}
trendLabel={powerTrend !== null ? `${powerTrend > 0 ? "+" : ""}${powerTrend} kW` : undefined}
href="/power"
/>
<KpiCard
title="PUE"
value={loading ? "—" : kpis.pue.toFixed(2)}
hint="Lower is better"
icon={Wind}
iconColor="text-primary"
status="ok"
loading={loading}
href="/capacity"
/>
<KpiCard
title="Avg Temperature"
value={loading ? "—" : `${kpis.avg_temperature}°C`}
icon={Thermometer}
iconColor="text-green-400"
status={loading ? "ok" : tempStatus}
loading={loading}
trend={tempTrend}
trendLabel={tempTrend !== null ? `${tempTrend > 0 ? "+" : ""}${tempTrend}°C` : undefined}
trendInvert
href="/environmental"
/>
<KpiCard
title="Active Alarms"
value={loading ? "—" : String(kpis.active_alarms)}
icon={AlertTriangle}
iconColor="text-destructive"
status={loading ? "ok" : alarmStatus}
loading={loading}
trend={alarmTrend}
trendLabel={alarmTrend !== null ? `${alarmTrend > 0 ? "+" : ""}${alarmTrend}` : undefined}
trendInvert
href="/alarms"
/>
<KpiCard
title="Generator"
value={loading ? "—" : genFuel !== null ? `${genFuel.toFixed(1)}% fuel` : "—"}
hint={genState === "standby" ? "Standby — ready" : genState === "running" ? "Running under load" : genState === "test" ? "Test run" : genState === "fault" ? "FAULT — check generator" : "—"}
icon={Fuel}
iconColor={genStatus === "critical" ? "text-destructive" : genStatus === "warning" ? "text-amber-400" : "text-green-400"}
status={loading ? "ok" : genStatus}
loading={loading}
href="/power"
/>
<KpiCard
title="Leak Detection"
value={loading ? "—" : activeLeaks > 0 ? `${activeLeaks} active` : "All clear"}
hint={activeLeaks > 0 ? "Water detected — investigate immediately" : `${leakSensors.length} sensors monitoring`}
icon={Droplets}
iconColor={leakStatus === "critical" ? "text-destructive" : "text-blue-400"}
status={loading ? "ok" : leakStatus}
loading={loading}
href="/environmental"
/>
</div>
{/* Charts row */}
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wider">Trends</p>
<TimeRangePicker value={chartHours} onChange={setChartHours} />
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<PowerTrendChart data={powerHistory} loading={loading} />
<TemperatureTrendChart data={tempHistory} loading={loading} />
</div>
{/* Bottom row — 50/50 */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<MiniFloorMap layout={mapLayout} racks={mapRacks} loading={loading} />
<AlarmFeed alarms={alarms} loading={loading} onAcknowledge={refreshKpis} onAlarmClick={handleAlarmClick} />
</div>
</PageShell>
);
}