Files
homeAutomation/energy-management/frontend/src/Dashboard.tsx
2026-08-19 18:35:47 +02:00

137 lines
4.4 KiB
TypeScript

import { useEffect, useState } from "react";
import { api, type BlockHistoryPoint, type ControlAction, type DeviceStatus, type QuarterHourStatus } from "./api";
import { PeakHistoryChart } from "./PeakHistoryChart";
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
export function Dashboard() {
const [quarterHour, setQuarterHour] = useState<QuarterHourStatus | null>(null);
const [devices, setDevices] = useState<DeviceStatus[]>([]);
const [history, setHistory] = useState<BlockHistoryPoint[]>([]);
const [actions, setActions] = useState<ControlAction[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
async function refresh() {
try {
const [status, hist, acts] = await Promise.all([
api.getStatus(),
api.getHistory(Date.now() - THIRTY_DAYS_MS),
api.getActions(10),
]);
if (cancelled) return;
setQuarterHour(status.quarterHour);
setDevices(status.devices);
setHistory(hist);
setActions(acts);
setError(null);
} catch (err) {
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load status");
}
}
refresh();
const interval = setInterval(refresh, 10_000);
return () => {
cancelled = true;
clearInterval(interval);
};
}, []);
if (error) {
return (
<div className="card">
<h2>Error</h2>
<p>{error}</p>
</div>
);
}
if (!quarterHour) {
return <div className="card">Loading</div>;
}
const overTarget = quarterHour.overTarget;
const shedCount = devices.filter((d) => d.shed).length;
return (
<>
<div className="card">
<h2>Current quarter-hour block</h2>
<div className="hero-figure">{(quarterHour.projectedAverageW / 1000).toFixed(2)} kW</div>
<div className="hero-sub">
projected average · target {(quarterHour.targetW / 1000).toFixed(2)} kW · running average{" "}
{(quarterHour.runningAverageW / 1000).toFixed(2)} kW ·{" "}
{Math.round(quarterHour.elapsedFractionOfBlock * 100)}% through block
</div>
<div style={{ marginTop: 10 }}>
<span className={`status-pill ${overTarget ? "critical" : "good"}`}>
{overTarget ? `Over target · ${shedCount} device(s) shed` : "Under target"}
</span>
</div>
</div>
<div className="card">
<h2>Devices</h2>
{devices.length === 0 ? (
<p style={{ color: "var(--text-muted)", fontSize: 13 }}>No devices configured yet add them in the Devices tab.</p>
) : (
devices
.slice()
.sort((a, b) => a.priority - b.priority)
.map((d) => (
<div className="device-row" key={d.id}>
<div>
<div className="device-name">{d.name}</div>
<div className="device-meta">
{d.entityId} · priority {d.priority}
{!d.available ? " · unavailable" : ""}
</div>
</div>
<div style={{ display: "flex", gap: 6 }}>
{d.manualOverride && <span className="badge override">Manual</span>}
<span className={`badge ${d.shed ? "shed" : "active"}`}>{d.shed ? "Shed" : "Active"}</span>
</div>
</div>
))
)}
</div>
<div className="card">
<h2>Daily peak (last 30 days)</h2>
<PeakHistoryChart points={history} targetW={quarterHour.targetW} />
</div>
<div className="card">
<h2>Recent control actions</h2>
{actions.length === 0 ? (
<p style={{ color: "var(--text-muted)", fontSize: 13 }}>No actions yet.</p>
) : (
<table>
<thead>
<tr>
<th>Time</th>
<th>Device</th>
<th>Action</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
{actions.map((a, i) => (
<tr key={i}>
<td>{new Date(a.timestampMs).toLocaleTimeString()}</td>
<td>{a.entityId}</td>
<td>{a.action}</td>
<td style={{ color: "var(--text-secondary)" }}>{a.reason}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</>
);
}