power-compare/app/web/templates/dashboard.html
dlawler489 84c4922bbc Initial commit: solar usage vs AER plan comparison app
FastAPI app that pulls interval usage from Sungrow iSolarCloud (OAuth2),
caches AER Consumer Data Right electricity plan data, and compares actual
metered usage against every available plan to estimate real annual cost
(TOU, tiered feed-in, and flat tariffs).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 11:07:01 +10:00

79 lines
3.1 KiB
HTML

{% extends "base.html" %}
{% block content %}
<h1>Usage Dashboard</h1>
<div class="card" id="summary-card">
<div class="muted">Loading summary&hellip;</div>
</div>
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center;">
<h3 style="margin:0;">Generation / load / import / export</h3>
<select id="range-select">
<option value="7">Last 7 days</option>
<option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option>
<option value="365">Last 365 days</option>
<option value="all">All time</option>
</select>
</div>
<div class="chart-container">
<canvas id="usage-chart"></canvas>
</div>
</div>
<script>
async function loadDashboard(days) {
const [summary, daily] = await Promise.all([
fetch(`/api/usage/summary?days=${days}`).then(r => r.json()),
fetch(`/api/usage/daily?days=${days}`).then(r => r.json()),
]);
const summaryEl = document.getElementById('summary-card');
if (!summary.days_with_data) {
summaryEl.innerHTML = '<div class="muted">No usage data yet. If you\'re running on mock data, it should appear within a few minutes of startup.</div>';
} else {
summaryEl.innerHTML = `
<div class="muted">${summary.first_day} &rarr; ${summary.last_day} (${summary.days_with_data} days with data)</div>
<div style="display:flex; gap:2rem; margin-top:0.75rem; flex-wrap:wrap;">
<div><div class="muted">Generated</div><strong>${summary.generation_kwh} kWh</strong></div>
<div><div class="muted">Consumed</div><strong>${summary.load_kwh} kWh</strong></div>
<div><div class="muted">Imported</div><strong>${summary.import_kwh} kWh</strong></div>
<div><div class="muted">Exported</div><strong>${summary.export_kwh} kWh</strong></div>
</div>
`;
}
const ctx = document.getElementById('usage-chart');
if (window.usageChart) window.usageChart.destroy();
// Chart.js can measure a stale (pre-layout) container size when created
// synchronously during initial page parse. Wait a couple of frames so the
// browser has actually settled the flex/grid layout first.
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
window.usageChart = new Chart(ctx, {
type: 'bar',
data: {
labels: daily.map(d => d.date),
datasets: [
{ label: 'Generation', data: daily.map(d => d.generation_kwh), backgroundColor: '#facc15' },
{ label: 'Load', data: daily.map(d => d.load_kwh), backgroundColor: '#60a5fa' },
{ label: 'Import', data: daily.map(d => d.import_kwh), backgroundColor: '#f87171' },
{ label: 'Export', data: daily.map(d => d.export_kwh), backgroundColor: '#4ade80' },
],
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,
scales: { x: { stacked: false }, y: { title: { display: true, text: 'kWh' } } },
},
});
}
const rangeSelect = document.getElementById('range-select');
rangeSelect.addEventListener('change', () => loadDashboard(rangeSelect.value));
loadDashboard(rangeSelect.value);
</script>
{% endblock %}