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,246 @@
"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>
);
}