Battery history
-Loading...
Battery history
+Loading...
Lifetime stats
-Loading...
diff --git a/server/app/static/device_status_bar.js b/server/app/static/device_status_bar.js new file mode 100644 index 0000000..382ac0e --- /dev/null +++ b/server/app/static/device_status_bar.js @@ -0,0 +1,77 @@ +// Device status bar: always-visible strip (below the page title, above +// the tabs -- see _device_status_bar.html) showing last-seen/firmware/ +// battery, so it's not tucked away on just the Stats tab. Shared by +// every frame page; each sets window.FRAME_API before this loads. + +let lastDeviceStatus = null; + +function renderDeviceStatusBar(device) { + const el = document.getElementById('device-status'); + if (!el) return; + 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 stat = document.createElement('span'); + stat.className = 'device-stat' + (alert ? ' alert' : ''); + const labelPart = document.createTextNode(label + ': '); + const valuePart = document.createElement('strong'); + valuePart.textContent = value; + stat.appendChild(labelPart); + stat.appendChild(valuePart); + el.appendChild(stat); + } +} + +async function loadDeviceStatusBar() { + try { + const resp = await fetch(`${window.FRAME_API}/queue`); + if (!resp.ok) { + return; + } + const data = await resp.json(); + lastDeviceStatus = data.device; + renderDeviceStatusBar(data.device); + } catch (e) { /* retried on the next poll */ } +} + +loadDeviceStatusBar(); + +document.addEventListener('themechange', () => { + if (lastDeviceStatus) { + renderDeviceStatusBar(lastDeviceStatus); + } +}); + +// 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 (lastDeviceStatus) { + renderDeviceStatusBar(lastDeviceStatus); + } +}, 1000); + +setInterval(loadDeviceStatusBar, 10000); diff --git a/server/app/static/frame_stats.js b/server/app/static/frame_stats.js index 0a3db08..b6f61f5 100644 --- a/server/app/static/frame_stats.js +++ b/server/app/static/frame_stats.js @@ -1,58 +1,6 @@ -// 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 */ } -} +// Stats tab: lifetime counters + battery history chart (chart logic in +// battery_chart.js). Device status now lives in the always-visible bar +// (device_status_bar.js), not here. window.FRAME_API set by template. function renderStats(stats) { const el = document.getElementById('stats-box'); @@ -90,29 +38,14 @@ async function loadStats() { } } -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. +// Redraw the canvas chart 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); diff --git a/server/app/static/theme.css b/server/app/static/theme.css index 4210dbe..8960e05 100644 --- a/server/app/static/theme.css +++ b/server/app/static/theme.css @@ -484,6 +484,33 @@ code { } .control-banner button { margin: 0; } +/* Always-visible device summary, sitting between the page title and the + tabs (see _device_status_bar.html) -- a compact horizontal row rather + than a full .card, since it has to fit above the tabs on every frame + page without pushing content down. */ +.device-status-bar { + padding: 10px 16px; + margin-bottom: 18px; +} +.device-status-row { + display: flex; + flex-wrap: wrap; + align-items: baseline; + column-gap: 26px; + row-gap: 6px; +} +.device-status-row .sub { margin: 0; } +.device-stat { + font-size: 13px; + color: var(--text-muted); + white-space: nowrap; +} +.device-stat strong { color: var(--text); font-weight: 600; } +.device-stat.alert, .device-stat.alert strong { color: var(--danger-text); } +@media (max-width: 860px) { + .device-status-row { column-gap: 16px; } +} + /* Mobile: sidebar off-canvas, hamburger in a slim top bar. */ .mobile-bar { display: none; } .sidebar-backdrop { display: none; } diff --git a/server/app/templates/_device_status_bar.html b/server/app/templates/_device_status_bar.html new file mode 100644 index 0000000..1144dcf --- /dev/null +++ b/server/app/templates/_device_status_bar.html @@ -0,0 +1,3 @@ + diff --git a/server/app/templates/app_base.html b/server/app/templates/app_base.html index 728a022..48b9793 100644 --- a/server/app/templates/app_base.html +++ b/server/app/templates/app_base.html @@ -72,6 +72,7 @@ + {% block device_status %}{% endblock %} {% block tabs %}{% endblock %} {% block content %}{% endblock %} diff --git a/server/app/templates/frame_config.html b/server/app/templates/frame_config.html index 66a9a2f..acee31b 100644 --- a/server/app/templates/frame_config.html +++ b/server/app/templates/frame_config.html @@ -3,6 +3,7 @@ {% block title %}{{ frame.name or "Frame" }} · Configuration{% endblock %} {% block page_title %}{{ frame.name or "Frame " ~ frame.id }}{% endblock %} +{% block device_status %}{% include "_device_status_bar.html" %}{% endblock %} {% block tabs %}{% include "_frame_tabs.html" %}{% endblock %} {% block content %} @@ -203,5 +204,6 @@ window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }}; window.DEFAULT_PALETTE_HEX = {{ palette_to_hex(default_palette_rgb) | tojson }}; + {% endblock %} diff --git a/server/app/templates/frame_photos.html b/server/app/templates/frame_photos.html index 4ad3b47..96fa794 100644 --- a/server/app/templates/frame_photos.html +++ b/server/app/templates/frame_photos.html @@ -3,6 +3,7 @@ {% block title %}{{ frame.name or "Frame" }} · Photos{% endblock %} {% block page_title %}{{ frame.name or "Frame " ~ frame.id }}{% endblock %} +{% block device_status %}{% include "_device_status_bar.html" %}{% endblock %} {% block tabs %}{% include "_frame_tabs.html" %}{% endblock %} {% block content %} @@ -55,6 +56,7 @@ {% block scripts %} + {% endblock %} diff --git a/server/app/templates/frame_stats.html b/server/app/templates/frame_stats.html index 7d336c4..cc89918 100644 --- a/server/app/templates/frame_stats.html +++ b/server/app/templates/frame_stats.html @@ -3,29 +3,19 @@ {% block title %}{{ frame.name or "Frame" }} · Stats{% endblock %} {% block page_title %}{{ frame.name or "Frame " ~ frame.id }}{% endblock %} +{% block device_status %}{% include "_device_status_bar.html" %}{% endblock %} {% block tabs %}{% include "_frame_tabs.html" %}{% endblock %} {% block content %} -Loading...
Loading...
Loading...
Loading...
Loading...