Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions web/public/config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"hide_miners": true,
"miner_names": {
"020100008c675349": "Miner 3",
"020100001a0f0bc8": "Miner 1",
Expand Down
70 changes: 41 additions & 29 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,33 +136,7 @@ function App() {
</header>

<main className="main">
<section className="card">
<h2>Scheduler Status</h2>
<div className="info-grid">
<InfoItem
label="Running:"
value={health?.scheduler.is_running ? "Yes" : "No"}
valueClassName={
health?.scheduler.is_running ? "value-success" : "value-error"
}
/>
<InfoItem label="Network:" value={health?.scheduler.network} />
<InfoItem label="Miners Count:" value={status?.miners.count || 0} />
<InfoItem
label="Market Data:"
value={
health?.scheduler.has_market_data
? "Available"
: "Not Available"
}
valueClassName={
health?.scheduler.has_market_data
? "value-success"
: "value-warning"
}
/>
</div>
</section>
<MetricsSummary />

{currentPrice !== undefined && priceLimit !== undefined && (
<section className="card">
Expand All @@ -180,6 +154,7 @@ function App() {
</section>
)}

{!config.hide_miners && (
<section className="card">
<h2>
Discovered Miners
Expand Down Expand Up @@ -257,6 +232,7 @@ function App() {
<p className="no-miners">No miners discovered yet.</p>
)}
</section>
)}

<section className="card devices-section">
<h2>Devices</h2>
Expand Down Expand Up @@ -360,7 +336,38 @@ function App() {

<MPCDecisions decisions={health?.scheduler.mpc_decisions} />

<MetricsSummary />
<section className="card">
<h2>Scheduler Status</h2>
<div className="info-grid">
<InfoItem
label="Running:"
value={health?.scheduler.is_running ? "Yes" : "No"}
valueClassName={
health?.scheduler.is_running ? "value-success" : "value-error"
}
/>
<InfoItem label="Network:" value={health?.scheduler.network} />
{!config.hide_miners && (
<InfoItem
label="Miners Count:"
value={status?.miners.count || 0}
/>
)}
<InfoItem
label="Market Data:"
value={
health?.scheduler.has_market_data
? "Available"
: "Not Available"
}
valueClassName={
health?.scheduler.has_market_data
? "value-success"
: "value-warning"
}
/>
</div>
</section>

<section className="card system-info">
<h2>System Information</h2>
Expand Down Expand Up @@ -390,7 +397,12 @@ function App() {
<DemoInfo onClose={() => setShowDemoInfo(false)} />
)}

{showConfig && <ConfigMenu onClose={() => setShowConfig(false)} />}
{showConfig && (
<ConfigMenu
onClose={() => setShowConfig(false)}
hideMiners={config.hide_miners}
/>
)}

{showMarketDataUpload && (
<MarketDataUpload onClose={() => setShowMarketDataUpload(false)} />
Expand Down
26 changes: 23 additions & 3 deletions web/src/components/ConfigMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import "./ConfigMenu.css";

interface ConfigMenuProps {
onClose: () => void;
/** Hide miner-related sections and fields. */
hideMiners?: boolean;
}

type FieldType = "text" | "number" | "boolean" | "select" | "duration" | "numericSelect";
Expand Down Expand Up @@ -618,7 +620,25 @@ function ConfigField({ def, value, onChange }: FieldProps) {

// ─── Main component ─────────────────────────────────────────────────────────

export function ConfigMenu({ onClose }: ConfigMenuProps) {
const MINER_SECTION_TITLES = new Set(["Miner Power Modes"]);
const MINER_FIELD_KEYS = new Set<keyof EmsConfig>([
"miners_state_check_interval",
"miner_discovery_interval",
"miner_max_consecutive_errors",
"miner_timeout",
]);

const MINERLESS_SECTIONS: Section[] = SECTIONS.filter(
(s) => !MINER_SECTION_TITLES.has(s.title),
)
.map((s) => ({
...s,
fields: s.fields.filter((f) => !MINER_FIELD_KEYS.has(f.key)),
}))
.filter((s) => s.fields.length > 0);

export function ConfigMenu({ onClose, hideMiners = false }: ConfigMenuProps) {
const sections = hideMiners ? MINERLESS_SECTIONS : SECTIONS;
const {
config,
loading,
Expand Down Expand Up @@ -680,7 +700,7 @@ export function ConfigMenu({ onClose }: ConfigMenuProps) {
}, [onClose]);

const currentSection =
SECTIONS.find((s) => s.title === activeSection) ?? SECTIONS[0];
sections.find((s) => s.title === activeSection) ?? sections[0];

return (
<div
Expand Down Expand Up @@ -734,7 +754,7 @@ export function ConfigMenu({ onClose }: ConfigMenuProps) {
<div className="config-modal-body">
{/* Sidebar navigation */}
<nav className="config-nav">
{SECTIONS.map((section) => (
{sections.map((section) => (
<button
key={section.title}
type="button"
Expand Down
170 changes: 120 additions & 50 deletions web/src/components/MetricsSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,96 @@ function generateMockMetricsSummary(date: Date): MetricsSummaryType {
};
}

// Generate mock monthly summary by summing mock daily data for the month
function generateMockMonthlySummary(date: Date): MetricsSummaryType {
const { start, end } = getMonthRange(date);
const today = new Date();
const result: MetricsSummaryType = {
total_import_cost: 0,
total_export_cost: 0,
total_import_kwh: 0,
total_export_kwh: 0,
start_time: start.toISOString(),
end_time: end.toISOString(),
};
for (
const d = new Date(start);
d <= end && d <= today;
d.setDate(d.getDate() + 1)
) {
const day = generateMockMetricsSummary(d);
result.total_import_cost += day.total_import_cost;
result.total_export_cost += day.total_export_cost;
result.total_import_kwh += day.total_import_kwh;
result.total_export_kwh += day.total_export_kwh;
}
return result;
}

function getMonthRange(date: Date): { start: Date; end: Date } {
const start = new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
const end = new Date(
date.getFullYear(),
date.getMonth() + 1,
0,
23,
59,
59,
999,
);
return { start, end };
}

async function fetchSummary(
start: Date,
end: Date,
): Promise<MetricsSummaryType> {
const url = `/api/metrics/summary?start_time=${encodeURIComponent(start.toISOString())}&end_time=${encodeURIComponent(end.toISOString())}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}

function SummaryRows({ summary }: { summary: MetricsSummaryType }) {
// Import cost is spending, export cost is revenue
const net = summary.total_import_cost - summary.total_export_cost;
return (
<>
<div className="mpc-summary-item">
<span className="mpc-summary-label">Import Cost:</span>
<span className="mpc-summary-value value-error">
€{summary.total_import_cost.toFixed(2)} (
{summary.total_import_kwh.toFixed(2)} kWh)
</span>
</div>
<div className="mpc-summary-item">
<span className="mpc-summary-label">Export Revenue:</span>
<span className="mpc-summary-value value-success">
€{summary.total_export_cost.toFixed(2)} (
{summary.total_export_kwh.toFixed(2)} kWh)
</span>
</div>
<div className="mpc-summary-item">
<span className="mpc-summary-label">
{net <= 0 ? "Net Revenue:" : "Net Cost:"}
</span>
<span
className={`mpc-summary-value ${net <= 0 ? "value-success" : "value-error"}`}
>
€{Math.abs(net).toFixed(2)}
</span>
</div>
</>
);
}

export function MetricsSummary() {
const [metricsSummary, setMetricsSummary] =
useState<MetricsSummaryType | null>(null);
const [monthlySummary, setMonthlySummary] =
useState<MetricsSummaryType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedDate, setSelectedDate] = useState<Date>(() => {
Expand All @@ -68,28 +155,26 @@ export function MetricsSummary() {
if (isDemoMode) {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 300));
const mockData = generateMockMetricsSummary(selectedDate);
setMetricsSummary(mockData);
setMetricsSummary(generateMockMetricsSummary(selectedDate));
setMonthlySummary(generateMockMonthlySummary(selectedDate));
setError(null);
} else {
// Calculate time range for selected date (calendar day - midnight to midnight)
const startTime = selectedDate.toISOString();
const endDate = new Date(selectedDate);
endDate.setHours(23, 59, 59, 999);
const endTime = endDate.toISOString();
// Selected calendar day (midnight to midnight) and its calendar month
const dayEnd = new Date(selectedDate);
dayEnd.setHours(23, 59, 59, 999);
const month = getMonthRange(selectedDate);

const url = `/api/metrics/summary?start_time=${encodeURIComponent(startTime)}&end_time=${encodeURIComponent(endTime)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: MetricsSummaryType = await response.json();
setMetricsSummary(data);
const [daily, monthly] = await Promise.all([
fetchSummary(selectedDate, dayEnd),
fetchSummary(month.start, month.end),
]);
setMetricsSummary(daily);
setMonthlySummary(monthly);
setError(null);
}
} catch (error) {
console.error("Failed to fetch metrics summary:", error);
setError("Failed to load actual costs data");
setError("Failed to load data");
} finally {
setLoading(false);
isFetchingRef.current = false;
Expand Down Expand Up @@ -129,11 +214,6 @@ export function MetricsSummary() {
return selectedDate.getTime() === today.getTime();
};

// Calculate net cost from metrics (import cost is negative, export cost is positive revenue)
const netActualCost = metricsSummary
? metricsSummary.total_import_cost - metricsSummary.total_export_cost
: null;

return (
<section className="card">
<div
Expand All @@ -145,7 +225,7 @@ export function MetricsSummary() {
}}
>
<h2 style={{ margin: 0 }}>
Actual Costs - {formatDateDisplay(selectedDate)}
Data - {formatDateDisplay(selectedDate)}
</h2>
<div style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}>
<button
Expand Down Expand Up @@ -205,7 +285,7 @@ export function MetricsSummary() {
</div>
</div>

<div className="mpc-summary">
<div className="mpc-summary" style={{ flexDirection: "column", gap: "1rem" }}>
{loading && (
<div className="mpc-summary-item">
<span className="mpc-summary-label">Loading...</span>
Expand All @@ -222,36 +302,26 @@ export function MetricsSummary() {
</div>
)}
{metricsSummary && !loading && (
<>
<div className="mpc-summary-item">
<span className="mpc-summary-label">Import Cost:</span>
<span className="mpc-summary-value value-error">
€{metricsSummary.total_import_cost.toFixed(2)} (
{metricsSummary.total_import_kwh.toFixed(2)} kWh)
</span>
</div>
<div className="mpc-summary-item">
<span className="mpc-summary-label">Export Revenue:</span>
<span className="mpc-summary-value value-success">
€{metricsSummary.total_export_cost.toFixed(2)} (
{metricsSummary.total_export_kwh.toFixed(2)} kWh)
</span>
<div>
<h3 style={{ margin: "0 0 0.5rem" }}>Day</h3>
<div style={{ display: "flex", gap: "2rem", flexWrap: "wrap" }}>
<SummaryRows summary={metricsSummary} />
</div>
<div className="mpc-summary-item">
<span className="mpc-summary-label">
{netActualCost !== null && netActualCost <= 0
? "Net Revenue:"
: "Net Cost:"}
</span>
<span
className={`mpc-summary-value ${netActualCost !== null && netActualCost <= 0 ? "value-success" : "value-error"}`}
>
{netActualCost !== null
? `€${Math.abs(netActualCost).toFixed(2)}`
: "N/A"}
</span>
</div>
)}
{monthlySummary && !loading && (
<div>
<h3 style={{ margin: "0 0 0.5rem" }}>
Month -{" "}
{selectedDate.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
})}
</h3>
<div style={{ display: "flex", gap: "2rem", flexWrap: "wrap" }}>
<SummaryRows summary={monthlySummary} />
</div>
</>
</div>
)}
</div>
</section>
Expand Down
Loading
Loading