// Stats tab: device status, lifetime counters, battery history chart // (chart logic in battery_chart.js). window.FRAME_API set by template. let lastDevice = null; function renderDeviceStatus(device) { const el = document.getElementById('device-status'); el.innerHTML = ''; if (!device || !device.last_seen) { el.innerHTML = '

The frame hasn\'t checked in yet.

'; return; } const now = Date.now() / 1000; const rows = []; const ago = formatDuration(Math.max(0, now - device.last_seen)); rows.push(['Last seen', `${ago} ago`, device.overdue]); if (device.firmware_version) { let fw = `v${device.firmware_version}`; if (device.firmware_available && device.firmware_available !== device.firmware_version) { fw += ` (v${device.firmware_available} waiting)`; } rows.push(['Firmware', fw, false]); } if (device.battery) { rows.push(['Battery', `${device.battery.percent}%`, false]); } if (device.on_battery_since) { rows.push(['On battery for', formatDuration(now - device.on_battery_since), false]); } if (device.battery_estimate_s !== null && device.battery_estimate_s !== undefined) { rows.push(['Est. remaining', `~${formatDuration(device.battery_estimate_s)}`, false]); } for (const [label, value, alert] of rows) { const p = document.createElement('p'); p.className = 'sub'; if (alert) { p.style.color = 'var(--danger-text)'; p.style.fontWeight = '600'; } p.textContent = `${label}: ${value}`; el.appendChild(p); } } async function loadDevice() { try { const resp = await fetch(`${window.FRAME_API}/queue`); if (!resp.ok) { return; } const data = await resp.json(); lastDevice = data.device; renderDeviceStatus(data.device); } catch (e) { /* retried on the next poll */ } } 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(`${window.FRAME_API}/stats`); if (!resp.ok) { el.innerHTML = '

Could not load.

'; return; } renderStats(await resp.json()); } catch (e) { el.innerHTML = '

Could not load.

'; } } loadDevice(); loadStats(); loadBatteryLog(); // Redraw the canvas chart (and the "Last seen" alert color) with the // new theme's colors as soon as the toggle is used -- canvas pixels // don't repaint themselves the way CSS does. document.addEventListener('themechange', () => { if (lastBatteryLog) { drawBatteryChart(lastBatteryLog); } if (lastDevice) { renderDeviceStatus(lastDevice); } }); // Fast tick: re-renders "Last seen"/"On battery for" from already- // fetched data every second so they count up smoothly without hitting // the server that often. setInterval(() => { if (lastDevice) { renderDeviceStatus(lastDevice); } }, 1000); setInterval(loadDevice, 10000);