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
+17
View File
@@ -19,6 +19,21 @@ CONFIG_PATH = Path(os.environ.get("CONFIG_PATH", "/data/config.json"))
_lock = RLock()
class FrameStats(BaseModel):
"""Cumulative, lifetime counters -- purely informational, never read
back to drive any behavior, so there's no harm in them being a little
approximate at the edges. Shown in a collapsed "Stats" section in the
web UI (GET /api/stats). Never reset except by deleting config.json."""
first_seen: float = 0.0 # first time this frame ever checked in
device_wakes: int = 0 # wake cycles, counted once each via GET /frame/config
photos_displayed: int = 0 # times the current photo actually changed (any cause)
photos_removed: int = 0 # times a photo was permanently excluded from rotation
battery_reports: int = 0 # POST /frame/battery calls
recharge_cycles: int = 0 # times a battery recharge was detected
ota_updates_applied: int = 0 # times the device's reported firmware version changed
config_saves: int = 0 # POST /api/config calls
class FrameConfig(BaseModel):
immich_url: str = ""
immich_api_key: str = ""
@@ -84,6 +99,8 @@ class FrameConfig(BaseModel):
# (POST /api/firmware); "" = none uploaded yet.
firmware_available_version: str = ""
stats: FrameStats = FrameStats()
def load() -> FrameConfig:
with _lock: