Server: lifetime stats counters in a collapsed web UI section
Build and push server image / build-and-push (push) Successful in 34s
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:
@@ -145,7 +145,7 @@
|
||||
.icon-btn:hover { background: var(--surface-alt); }
|
||||
.icon-btn:active { transform: scale(0.94); }
|
||||
|
||||
h2.card-title {
|
||||
h2.card-title, summary.card-title {
|
||||
font-size: 14.5px;
|
||||
font-weight: 650;
|
||||
margin: 0 0 14px;
|
||||
@@ -153,6 +153,9 @@
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
summary.card-title { cursor: pointer; margin-bottom: 0; }
|
||||
details.card[open] summary.card-title { margin-bottom: 14px; }
|
||||
details.card .sub { margin-top: 8px; }
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
|
||||
@@ -105,6 +105,11 @@
|
||||
<input type="file" id="firmware-file" accept=".bin">
|
||||
<button type="button" class="secondary" id="firmware-upload">Upload firmware</button>
|
||||
</section>
|
||||
|
||||
<details class="card">
|
||||
<summary class="card-title">Stats</summary>
|
||||
<div id="stats-box"><p class="sub">Loading...</p></div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -609,8 +614,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderStats(stats) {
|
||||
const el = document.getElementById('stats-box');
|
||||
el.innerHTML = '';
|
||||
const now = Date.now() / 1000;
|
||||
const rows = [
|
||||
['Running since', stats.first_seen ? formatDuration(Math.max(0, now - stats.first_seen)) + ' ago' : 'never checked in'],
|
||||
['Wake cycles', stats.device_wakes],
|
||||
['Photos displayed', stats.photos_displayed],
|
||||
['Photos removed from rotation', stats.photos_removed],
|
||||
['Battery reports received', stats.battery_reports],
|
||||
['Battery recharge cycles', stats.recharge_cycles],
|
||||
['OTA updates applied', stats.ota_updates_applied],
|
||||
['Settings saved', stats.config_saves],
|
||||
];
|
||||
for (const [label, value] of rows) {
|
||||
const p = document.createElement('p');
|
||||
p.className = 'sub';
|
||||
p.textContent = `${label}: ${value}`;
|
||||
el.appendChild(p);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
const el = document.getElementById('stats-box');
|
||||
try {
|
||||
const resp = await fetch('/api/stats');
|
||||
if (!resp.ok) {
|
||||
el.innerHTML = '<p class="sub">Could not load.</p>';
|
||||
return;
|
||||
}
|
||||
renderStats(await resp.json());
|
||||
} catch (e) {
|
||||
el.innerHTML = '<p class="sub">Could not load.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
loadQueue();
|
||||
loadBatteryLog();
|
||||
loadStats();
|
||||
|
||||
// Redraw the canvas chart (and the "Last seen" alert color) with the
|
||||
// new theme's colors as soon as the toggle in the header is used --
|
||||
|
||||
Reference in New Issue
Block a user