Server: lifetime stats counters in a collapsed web UI section
Build and push server image / build-and-push (push) Successful in 34s

New FrameStats (first_seen, device_wakes, photos_displayed,
photos_removed, battery_reports, recharge_cycles, ota_updates_applied,
config_saves), persisted alongside everything else in config.json.
Incremented at the existing route/photo_queue.py call sites that already
own each event -- no new instrumentation plumbing, no behavior depends
on these, purely informational. GET /api/stats serves them; the web UI
renders them into a native <details> "Stats" card (collapsed by default,
no JS needed for the expand/collapse), fetched once on page load like
the battery-history chart.

Verified: TestClient run through /frame/config (wakes + first_seen +
OTA-applied detection), /frame/battery (reports + recharge detection),
/api/config (saves); direct photo_queue.py unit checks for
advance/back/remove covering the "did the current photo actually
change" distinction (removing a queued-but-not-current photo bumps
photos_removed but not photos_displayed).
This commit is contained in:
2026-07-21 00:00:37 -04:00
parent 1b11067da7
commit 475888306e
6 changed files with 86 additions and 1 deletions
+13
View File
@@ -198,7 +198,12 @@ def frame_config(request: Request):
with config.locked():
cfg = config.load()
cfg.last_seen = time.time()
if cfg.stats.first_seen == 0:
cfg.stats.first_seen = cfg.last_seen
cfg.stats.device_wakes += 1
if reported_version:
if cfg.device_firmware_version and reported_version != cfg.device_firmware_version:
cfg.stats.ota_updates_applied += 1
cfg.device_firmware_version = reported_version
config.save(cfg)
return {
@@ -283,10 +288,16 @@ def api_config_save(
cfg.quiet_hours_end = quiet_hours_end
if timezone in ALL_TIMEZONES:
cfg.timezone = timezone
cfg.stats.config_saves += 1
config.save(cfg)
return {"status": "saved"}
@app.get("/api/stats", dependencies=[Depends(require_access_token)])
def api_stats():
return config.load().stats.model_dump()
def _require_configured(cfg: config.FrameConfig) -> None:
if not cfg.immich_url or not cfg.immich_api_key:
raise HTTPException(400, "Immich URL/API key not configured yet")
@@ -404,11 +415,13 @@ def frame_battery(body: BatteryReport):
now = time.time()
with config.locked():
cfg = config.load()
cfg.stats.battery_reports += 1
if cfg.battery_history and body.percent >= cfg.battery_history[-1][1] + RECHARGE_JUMP_PCT:
# Percent jumped up meaningfully -- the battery was recharged
# (or swapped). Start a fresh discharge cycle so runtime and
# discharge-rate estimates never span a charge.
cfg.battery_history = []
cfg.stats.recharge_cycles += 1
cfg.battery_history.append([now, body.percent])
cfg.battery_history = cfg.battery_history[-BATTERY_HISTORY_MAX:]
cfg.battery_log.append([now, body.percent])