fix: added energy-management
This commit is contained in:
136
energy-management/frontend/src/Dashboard.tsx
Normal file
136
energy-management/frontend/src/Dashboard.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user