diff --git a/cockpit/vanrouter/index.html b/cockpit/vanrouter/index.html
index 1994314..5f14918 100644
--- a/cockpit/vanrouter/index.html
+++ b/cockpit/vanrouter/index.html
@@ -28,6 +28,11 @@
WAN Failover
diff --git a/cockpit/vanrouter/vanrouter.css b/cockpit/vanrouter/vanrouter.css
index 04a733c..0143e9c 100644
--- a/cockpit/vanrouter/vanrouter.css
+++ b/cockpit/vanrouter/vanrouter.css
@@ -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; }
diff --git a/cockpit/vanrouter/vanrouter.js b/cockpit/vanrouter/vanrouter.js
index 4bc07fe..35bebb5 100644
--- a/cockpit/vanrouter/vanrouter.js
+++ b/cockpit/vanrouter/vanrouter.js
@@ -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 = `
van-battery daemon not running (no state file).
`;
+ 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
+ ? `
on battery`
+ : `
on mains`;
+
+ let html = `
Power: ${src} Charge: ${esc(cap)}%
`;
+ html += `
`;
+ if (b.on_battery && b.alerted_below != null)
+ html += `
Low-battery alert sent at ${esc(b.alerted_below)}%.
`;
+ html += `
On battery: alerts at ${esc(warns.join("/"))}%, auto-shutdown at ${esc(shut)}%.` +
+ ` Alerts: journalctl -u van-battery
`;
+ 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();
diff --git a/power/van-battery b/power/van-battery
index b6d0944..06fc39b 100644
--- a/power/van-battery
+++ b/power/van-battery
@@ -150,13 +150,15 @@ def poweroff(cfg):
log(f"poweroff failed: {e}", "crit")
-def write_state(on_batt, cap, armed):
+def write_state(on_batt, cap, armed, warn_levels, shutdown_level):
STATE_DIR.mkdir(parents=True, exist_ok=True)
payload = {
"updated": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"on_battery": on_batt,
"capacity": cap,
"alerted_below": armed, # lowest level alerted this discharge episode, or null
+ "warn_levels": warn_levels,
+ "shutdown_level": shutdown_level,
}
tmp = STATE_PATH.with_suffix(".tmp")
tmp.write_text(json.dumps(payload))
@@ -216,7 +218,7 @@ def main():
prev_on_batt = on_batt
try:
- write_state(on_batt, cap, last_alerted)
+ write_state(on_batt, cap, last_alerted, cfg["warn_levels"], shutdown_level)
except OSError as e:
log(f"state write failed: {e}", "warn")
time.sleep(cfg["poll_interval"])