cockpit: battery/power card; van-battery publishes thresholds in state

New "Battery / Power" card shows mains vs battery, charge % with a severity-
colored bar (only bites while on battery), the last alert level, and the
configured warn/shutdown thresholds. van-battery now includes warn_levels +
shutdown_level in /run/van-battery/state.json so the card stays self-describing
instead of hardcoding the numbers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Andreas Wrede
2026-06-28 21:56:20 -04:00
co-authored by Claude Opus 4.8
parent 52e948654e
commit 473a75c729
4 changed files with 55 additions and 3 deletions
+5
View File
@@ -28,6 +28,11 @@
<div id="thermal"></div>
</div>
<div class="card">
<h3>Battery / Power</h3>
<div id="battery"></div>
</div>
<div class="card">
<h3>WAN Failover</h3>
<div id="failover"></div>
+5
View File
@@ -13,6 +13,11 @@ th { color: #6a6e73; font-weight: 600; }
.ok { background: #bde5b8; color: #1e4f18; }
.bad { background: #f0b8b8; color: #5f1414; }
.warn { background: #f5d9a8; color: #5f4414; }
.bar { height: 10px; border-radius: 5px; background: #e5e5e5; overflow: hidden; max-width: 320px; margin: 4px 0; }
.bar-fill { height: 100%; transition: width 0.3s; }
.bar-fill.ok { background: #3e8635; }
.bar-fill.warn { background: #e08a00; }
.bar-fill.crit { background: #c9190b; }
.muted { color: #6a6e73; font-size: 12px; }
.active-wan { font-weight: 700; color: #0066cc; }
.btn { margin-left: 6px; padding: 3px 10px; cursor: pointer; }
+41 -1
View File
@@ -97,6 +97,45 @@ function renderThermal(th) {
el.innerHTML = html;
}
/* ---------- Battery / power source (van-battery daemon state) ---------- */
async function readBattery() {
try {
return JSON.parse(await run(["cat", "/run/van-battery/state.json"]));
} catch (e) {
return null;
}
}
function renderBattery(b) {
const el = document.getElementById("battery");
if (!b || b.capacity == null) {
el.innerHTML = `<p class="muted">van-battery daemon not running (no state file).</p>`;
return;
}
const cap = b.capacity;
const shut = b.shutdown_level != null ? b.shutdown_level : 10;
const warns = b.warn_levels || [];
const lowest = warns.length ? Math.min.apply(null, warns) : 25;
// Charge severity only bites while on battery; on mains it's just charging/full.
let lvl = "ok";
if (b.on_battery) lvl = cap <= shut ? "crit" : cap <= lowest ? "warn" : "ok";
const pillClass = { ok: "ok", warn: "warn", crit: "bad" }[lvl];
const src = b.on_battery
? `<span class="pill warn">on battery</span>`
: `<span class="pill ok">on mains</span>`;
let html = `<p>Power: ${src} &nbsp; Charge: <span class="pill ${pillClass}">${esc(cap)}%</span></p>`;
html += `<div class="bar"><div class="bar-fill ${lvl}" style="width:${Math.max(0, Math.min(100, cap))}%"></div></div>`;
if (b.on_battery && b.alerted_below != null)
html += `<p class="muted">Low-battery alert sent at ${esc(b.alerted_below)}%.</p>`;
html += `<p class="muted">On battery: alerts at ${esc(warns.join("/"))}%, auto-shutdown at ${esc(shut)}%.` +
` Alerts: <code>journalctl -u van-battery</code></p>`;
el.innerHTML = html;
}
/* ---------- WAN failover (van-failover daemon state) ---------- */
async function readFailover() {
@@ -225,9 +264,10 @@ async function restartAP() {
async function refresh() {
try {
const [ap, th, fo, wan] = await Promise.all([readAP(), readThermal(), readFailover(), readWAN()]);
const [ap, th, bat, fo, wan] = await Promise.all([readAP(), readThermal(), readBattery(), readFailover(), readWAN()]);
renderAP(ap);
renderThermal(th);
renderBattery(bat);
renderFailover(fo);
renderWAN(wan);
document.getElementById("updated").textContent = "updated " + new Date().toLocaleTimeString();