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,330 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { toast } from "sonner";
import {
fetchEnergyReport, fetchUtilityPower,
type EnergyReport, type UtilityPower,
} from "@/lib/api";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import {
AreaChart, Area, LineChart, Line,
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine,
} from "recharts";
import { Zap, Leaf, RefreshCw, TrendingDown, DollarSign, Activity } from "lucide-react";
import { cn } from "@/lib/utils";
const SITE_ID = "sg-01";
// Singapore grid emission factor (kgCO2e/kWh) — Energy Market Authority 2023
const GRID_EF_KG_CO2_KWH = 0.4168;
// Approximate WUE for air-cooled DC in Singapore climate
const WUE_EST = 1.4;
function KpiTile({
label, value, sub, icon: Icon, iconClass, warn,
}: {
label: string; value: string; sub?: string;
icon?: React.ElementType; iconClass?: string; warn?: boolean;
}) {
return (
<div className="rounded-xl border border-border bg-muted/10 px-4 py-4 space-y-1">
<div className="flex items-center gap-2">
{Icon && <Icon className={cn("w-4 h-4 shrink-0", iconClass ?? "text-primary")} />}
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">{label}</p>
</div>
<p className={cn("text-2xl font-bold tabular-nums leading-none", warn && "text-amber-400")}>{value}</p>
{sub && <p className="text-[10px] text-muted-foreground">{sub}</p>}
</div>
);
}
function SectionHeader({ children }: { children: React.ReactNode }) {
return (
<h2 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">{children}</h2>
);
}
export default function EnergyPage() {
const [energy, setEnergy] = useState<EnergyReport | null>(null);
const [utility, setUtility] = useState<UtilityPower | null>(null);
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
try {
const [e, u] = await Promise.all([
fetchEnergyReport(SITE_ID, 30),
fetchUtilityPower(SITE_ID).catch(() => null),
]);
setEnergy(e);
setUtility(u);
} catch { toast.error("Failed to load energy data"); }
finally { setLoading(false); }
}, []);
useEffect(() => {
load();
const id = setInterval(load, 60_000);
return () => clearInterval(id);
}, [load]);
const co2e_kg = energy ? Math.round(energy.kwh_total * GRID_EF_KG_CO2_KWH) : null;
const co2e_t = co2e_kg ? (co2e_kg / 1000).toFixed(2) : null;
const wue_water = energy ? (energy.kwh_total * (WUE_EST - 1)).toFixed(0) : null;
const itKwChart = (energy?.pue_trend ?? []).map((d) => ({
day: new Date(d.day).toLocaleDateString("en-GB", { month: "short", day: "numeric" }),
kw: d.avg_it_kw,
pue: d.pue_est,
}));
const avgPue30 = energy?.pue_estimated ?? null;
const pueWarn = avgPue30 != null && avgPue30 > 1.5;
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-start justify-between gap-4 flex-wrap">
<div>
<h1 className="text-xl font-semibold">Energy &amp; Sustainability</h1>
<p className="text-sm text-muted-foreground">Singapore DC01 30-day energy analysis · refreshes every 60s</p>
</div>
<div className="flex items-center gap-3">
{!loading && (
<div className="flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-full bg-green-500/10 text-green-400 font-semibold">
<Leaf className="w-3.5 h-3.5" /> {co2e_t ? `${co2e_t} tCO₂e this month` : "—"}
</div>
)}
<button
onClick={load}
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<RefreshCw className="w-3.5 h-3.5" /> Refresh
</button>
</div>
</div>
{/* Site energy banner */}
{!loading && utility && (
<div className="rounded-xl border border-border bg-muted/10 px-5 py-3 flex items-center gap-8 text-sm flex-wrap">
<div>
<span className="text-muted-foreground">Current IT load: </span>
<strong>{utility.total_kw.toFixed(1)} kW</strong>
</div>
<div>
<span className="text-muted-foreground">Tariff: </span>
<strong>SGD {utility.tariff_sgd_kwh.toFixed(3)}/kWh</strong>
</div>
<div>
<span className="text-muted-foreground">Month-to-date: </span>
<strong>{utility.kwh_month_to_date.toFixed(0)} kWh</strong>
<span className="text-muted-foreground ml-1">
(SGD {utility.cost_sgd_mtd.toFixed(0)})
</span>
</div>
<div className="ml-auto text-xs text-muted-foreground">
Singapore · SP Group grid
</div>
</div>
)}
{/* 30-day KPIs */}
<SectionHeader>30-Day Energy Summary</SectionHeader>
{loading ? (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-24" />)}
</div>
) : (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<KpiTile
label="Total Consumption"
value={energy ? `${energy.kwh_total.toFixed(0)} kWh` : "—"}
sub="last 30 days"
icon={Zap}
iconClass="text-amber-400"
/>
<KpiTile
label="Energy Cost"
value={energy ? `SGD ${energy.cost_sgd.toFixed(0)}` : "—"}
sub={`@ SGD ${energy?.tariff_sgd_kwh.toFixed(3) ?? "—"}/kWh`}
icon={DollarSign}
iconClass="text-green-400"
/>
<KpiTile
label="Avg PUE"
value={avgPue30 != null ? avgPue30.toFixed(3) : "—"}
sub={avgPue30 != null && avgPue30 < 1.4 ? "Excellent" : avgPue30 != null && avgPue30 < 1.6 ? "Good" : "Room to improve"}
icon={Activity}
iconClass={pueWarn ? "text-amber-400" : "text-primary"}
warn={pueWarn}
/>
<KpiTile
label="Annual Estimate"
value={utility ? `SGD ${(utility.cost_sgd_annual_est).toFixed(0)}` : "—"}
sub={utility ? `${utility.kwh_annual_est.toFixed(0)} kWh/yr` : undefined}
icon={TrendingDown}
iconClass="text-muted-foreground"
/>
</div>
)}
{/* IT Load trend */}
{(loading || itKwChart.length > 0) && (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<Zap className="w-4 h-4 text-amber-400" />
Daily IT Load 30 Days
</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<Skeleton className="h-48" />
) : (
<ResponsiveContainer width="100%" height={200}>
<AreaChart data={itKwChart} margin={{ top: 4, right: 16, left: -10, bottom: 0 }}>
<defs>
<linearGradient id="itKwGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="oklch(0.78 0.17 84)" stopOpacity={0.3} />
<stop offset="95%" stopColor="oklch(0.78 0.17 84)" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="oklch(1 0 0 / 8%)" vertical={false} />
<XAxis
dataKey="day"
tick={{ fontSize: 9, fill: "oklch(0.65 0.04 257)" }}
tickLine={false} axisLine={false}
interval={4}
/>
<YAxis
tick={{ fontSize: 9, fill: "oklch(0.65 0.04 257)" }}
tickLine={false} axisLine={false}
tickFormatter={(v) => `${v} kW`}
domain={["auto", "auto"]}
/>
<Tooltip
contentStyle={{ backgroundColor: "oklch(0.16 0.04 265)", border: "1px solid oklch(1 0 0/9%)", borderRadius: "6px", fontSize: "12px" }}
formatter={(v) => [`${Number(v).toFixed(1)} kW`, "IT Load"]}
/>
<Area type="monotone" dataKey="kw" stroke="oklch(0.78 0.17 84)" fill="url(#itKwGrad)" strokeWidth={2} dot={false} />
</AreaChart>
</ResponsiveContainer>
)}
</CardContent>
</Card>
)}
{/* PUE trend */}
{(loading || itKwChart.length > 0) && (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<Activity className="w-4 h-4 text-primary" />
PUE Trend 30 Days
<span className="text-[10px] font-normal text-muted-foreground ml-1">(target: &lt; 1.4)</span>
</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<Skeleton className="h-48" />
) : (
<ResponsiveContainer width="100%" height={200}>
<LineChart data={itKwChart} margin={{ top: 4, right: 16, left: -10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="oklch(1 0 0 / 8%)" vertical={false} />
<XAxis
dataKey="day"
tick={{ fontSize: 9, fill: "oklch(0.65 0.04 257)" }}
tickLine={false} axisLine={false}
interval={4}
/>
<YAxis
tick={{ fontSize: 9, fill: "oklch(0.65 0.04 257)" }}
tickLine={false} axisLine={false}
domain={[1.0, "auto"]}
/>
<Tooltip
contentStyle={{ backgroundColor: "oklch(0.16 0.04 265)", border: "1px solid oklch(1 0 0/9%)", borderRadius: "6px", fontSize: "12px" }}
formatter={(v) => [Number(v).toFixed(3), "PUE"]}
/>
<ReferenceLine y={1.4} stroke="oklch(0.68 0.14 162)" strokeDasharray="4 4" strokeWidth={1}
label={{ value: "Target 1.4", fontSize: 9, fill: "oklch(0.68 0.14 162)", position: "insideTopRight" }} />
<ReferenceLine y={1.6} stroke="oklch(0.65 0.20 45)" strokeDasharray="4 4" strokeWidth={1}
label={{ value: "Warn 1.6", fontSize: 9, fill: "oklch(0.65 0.20 45)", position: "insideTopRight" }} />
<Line type="monotone" dataKey="pue" stroke="oklch(0.62 0.17 212)" strokeWidth={2} dot={false} activeDot={{ r: 3 }} />
</LineChart>
</ResponsiveContainer>
)}
</CardContent>
</Card>
)}
{/* Sustainability */}
<SectionHeader>Sustainability Metrics</SectionHeader>
{loading ? (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{Array.from({ length: 3 }).map((_, i) => <Skeleton key={i} className="h-24" />)}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="rounded-xl border border-green-500/20 bg-green-500/5 px-4 py-4 space-y-2">
<div className="flex items-center gap-2">
<Leaf className="w-4 h-4 text-green-400" />
<p className="text-xs font-semibold text-green-400 uppercase tracking-wider">Carbon Footprint</p>
</div>
<p className="text-2xl font-bold tabular-nums">{co2e_t ?? "—"} tCOe</p>
<p className="text-[10px] text-muted-foreground">
30-day estimate · {energy?.kwh_total.toFixed(0) ?? "—"} kWh × {GRID_EF_KG_CO2_KWH} kgCOe/kWh
</p>
<p className="text-[10px] text-muted-foreground">
Singapore grid emission factor (EMA 2023)
</p>
</div>
<div className="rounded-xl border border-blue-500/20 bg-blue-500/5 px-4 py-4 space-y-2">
<div className="flex items-center gap-2">
<Activity className="w-4 h-4 text-blue-400" />
<p className="text-xs font-semibold text-blue-400 uppercase tracking-wider">Water Usage (WUE)</p>
</div>
<p className="text-2xl font-bold tabular-nums">{WUE_EST.toFixed(1)}</p>
<p className="text-[10px] text-muted-foreground">
Estimated WUE (L/kWh) · air-cooled DC
</p>
<p className="text-[10px] text-muted-foreground">
Est. {wue_water ? `${Number(wue_water).toLocaleString()} L` : "—"} consumed (30d)
</p>
</div>
<div className="rounded-xl border border-primary/20 bg-primary/5 px-4 py-4 space-y-2">
<div className="flex items-center gap-2">
<TrendingDown className="w-4 h-4 text-primary" />
<p className="text-xs font-semibold text-primary uppercase tracking-wider">Efficiency</p>
</div>
<p className={cn("text-2xl font-bold tabular-nums", pueWarn ? "text-amber-400" : "text-green-400")}>
{avgPue30?.toFixed(3) ?? "—"}
</p>
<p className="text-[10px] text-muted-foreground">
Avg PUE · {avgPue30 != null && avgPue30 < 1.4 ? "Excellent — Tier IV class" :
avgPue30 != null && avgPue30 < 1.6 ? "Good — industry average" :
"Above average — optimise cooling"}
</p>
<p className="text-[10px] text-muted-foreground">
IT energy efficiency: {avgPue30 != null ? `${(1 / avgPue30 * 100).toFixed(1)}%` : "—"} of total power to IT
</p>
</div>
</div>
)}
{/* Reference info */}
<div className="rounded-xl border border-border bg-muted/5 px-5 py-4 text-xs text-muted-foreground space-y-1.5">
<p className="font-semibold text-foreground/80">Singapore Energy Context</p>
<p>Grid emission factor: {GRID_EF_KG_CO2_KWH} kgCOe/kWh (EMA 2023, predominantly natural gas + growing solar)</p>
<p>Electricity tariff: SGD {utility?.tariff_sgd_kwh.toFixed(3) ?? "0.298"}/kWh (SP Group commercial rate)</p>
<p>BCA Green Mark: Targeting GoldPLUS certification · PUE target &lt; 1.4</p>
<p className="text-muted-foreground/50 text-[10px] pt-1">
COe and WUE estimates are indicative. Actual values depend on metered chilled water and cooling tower data.
</p>
</div>
</div>
);
}