Server: permanent battery history log + graph in the web UI
Build and push server image / build-and-push (push) Successful in 35s

battery_history stays cycle-scoped (reset on recharge, feeds the "on
battery for"/estimate numbers), but nothing kept a permanent record --
added battery_log, appended on every report and never reset, capped at
~2 years of hourly reports as a sanity bound rather than a real limit.
New GET /api/battery-log serves it; the web UI draws it as a plain
canvas line chart (no chart library) under a new "Battery history"
section, loaded once on page load.

Also caught up server/README.md, which never documented the OTA
firmware endpoints or the /api/queue response's current "device" shape
from the earlier status-panel work.
This commit is contained in:
2026-07-20 23:21:20 -04:00
parent 594b0bd513
commit a7b6c6d77a
4 changed files with 128 additions and 8 deletions
+81
View File
@@ -117,6 +117,9 @@
<h2 class="section">Device</h2>
<div id="device-status"><p class="sub">Loading...</p></div>
<h2 class="section">Battery history</h2>
<div id="battery-chart-wrap"><p class="sub">Loading...</p></div>
<h2 class="section">Firmware update</h2>
<p class="sub" id="firmware-available">
{% if cfg.firmware_available_version %}Uploaded: v{{ cfg.firmware_available_version }} -- the frame
@@ -527,7 +530,85 @@
}
}
function drawBatteryChart(log) {
const wrap = document.getElementById('battery-chart-wrap');
if (!log || log.length < 2) {
wrap.innerHTML = '<p class="sub">Not enough data yet.</p>';
return;
}
wrap.innerHTML = '';
const width = wrap.clientWidth || 440;
const height = 180;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.style.border = '1px solid #e5e7eb';
canvas.style.borderRadius = '4px';
wrap.appendChild(canvas);
const ctx = canvas.getContext('2d');
const pad = { left: 28, right: 8, top: 10, bottom: 20 };
const plotW = width - pad.left - pad.right;
const plotH = height - pad.top - pad.bottom;
const times = log.map((p) => p[0]);
const minT = Math.min(...times);
const maxT = Math.max(...times);
const spanT = Math.max(1, maxT - minT);
const x = (t) => pad.left + ((t - minT) / spanT) * plotW;
const y = (pct) => pad.top + (1 - pct / 100) * plotH;
ctx.strokeStyle = '#e5e7eb';
ctx.fillStyle = '#999';
ctx.font = '10px system-ui, sans-serif';
ctx.lineWidth = 1;
ctx.textAlign = 'left';
[0, 25, 50, 75, 100].forEach((pct) => {
const yy = y(pct);
ctx.beginPath();
ctx.moveTo(pad.left, yy);
ctx.lineTo(width - pad.right, yy);
ctx.stroke();
ctx.fillText(String(pct), 2, yy + 3);
});
ctx.strokeStyle = '#2563eb';
ctx.lineWidth = 1.5;
ctx.beginPath();
log.forEach((p, i) => {
const px = x(p[0]);
const py = y(p[1]);
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
});
ctx.stroke();
const fmt = (t) => new Date(t * 1000).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
ctx.fillStyle = '#666';
ctx.textAlign = 'left';
ctx.fillText(fmt(minT), pad.left, height - 4);
ctx.textAlign = 'right';
ctx.fillText(fmt(maxT), width - pad.right, height - 4);
}
async function loadBatteryLog() {
const wrap = document.getElementById('battery-chart-wrap');
try {
const resp = await fetch('/api/battery-log');
if (!resp.ok) {
wrap.innerHTML = '<p class="sub">Could not load.</p>';
return;
}
const data = await resp.json();
drawBatteryChart(data.log);
} catch (e) {
wrap.innerHTML = '<p class="sub">Could not load.</p>';
}
}
loadQueue();
loadBatteryLog();
// Fast tick: re-renders "Last seen"/"On battery for" etc. from the
// already-fetched device data every second, so they count up smoothly