Make the device status card always visible, not just on Stats
Build and push server image / build-and-push (push) Successful in 39s
Build and push server image / build-and-push (push) Successful in 39s
Moved out of the Stats tab's side column into a new horizontal bar shared by every frame page (Photos/Configuration/Stats), sitting between the page title and the tabs so it's on screen regardless of which tab is active. _device_status_bar.html is a new partial included via a device_status block in app_base.html; device_status_bar.js is the fetch/render/poll logic extracted from frame_stats.js and adapted to a wrapping row of label/value pairs instead of a stacked list. frame_stats.html's Device card and now-single-card .side-col are gone -- Battery history and Lifetime stats just stack directly.
This commit is contained in:
@@ -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 = '<p class="sub">The frame hasn\'t checked in yet.</p>';
|
||||
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);
|
||||
@@ -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 = '<p class="sub">The frame hasn\'t checked in yet.</p>';
|
||||
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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<section class="card device-status-bar" id="device-status-bar">
|
||||
<div id="device-status" class="device-status-row"><p class="sub">Loading...</p></div>
|
||||
</section>
|
||||
@@ -72,6 +72,7 @@
|
||||
<button type="button" id="theme-toggle" class="icon-btn" title="Toggle dark mode" aria-label="Toggle dark mode">🌓</button>
|
||||
</div>
|
||||
</div>
|
||||
{% block device_status %}{% endblock %}
|
||||
{% block tabs %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
@@ -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 }};
|
||||
</script>
|
||||
<script src="/static/device_status_bar.js"></script>
|
||||
<script src="/static/frame_config.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 %}
|
||||
<script>window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }};</script>
|
||||
<script src="/static/device_status_bar.js"></script>
|
||||
<script src="/static/queue.js"></script>
|
||||
<script src="/static/frame_photos.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
{% 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 %}
|
||||
<div class="layout">
|
||||
<div class="main-col">
|
||||
<section class="card">
|
||||
<h2 class="card-title">Battery history</h2>
|
||||
<div id="battery-chart-wrap"><p class="sub">Loading...</p></div>
|
||||
@@ -17,15 +16,6 @@
|
||||
<h2 class="card-title">Lifetime stats</h2>
|
||||
<div id="stats-box"><p class="sub">Loading...</p></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="side-col">
|
||||
<section class="card">
|
||||
<h2 class="card-title">Device</h2>
|
||||
<div id="device-status"><p class="sub">Loading...</p></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="result"></div>
|
||||
{% endblock %}
|
||||
@@ -33,5 +23,6 @@
|
||||
{% block scripts %}
|
||||
<script>window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }};</script>
|
||||
<script src="/static/battery_chart.js"></script>
|
||||
<script src="/static/device_status_bar.js"></script>
|
||||
<script src="/static/frame_stats.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user