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
|
// Stats tab: lifetime counters + battery history chart (chart logic in
|
||||||
// (chart logic in battery_chart.js). window.FRAME_API set by template.
|
// battery_chart.js). Device status now lives in the always-visible bar
|
||||||
|
// (device_status_bar.js), not here. 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 */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderStats(stats) {
|
function renderStats(stats) {
|
||||||
const el = document.getElementById('stats-box');
|
const el = document.getElementById('stats-box');
|
||||||
@@ -90,29 +38,14 @@ async function loadStats() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadDevice();
|
|
||||||
loadStats();
|
loadStats();
|
||||||
loadBatteryLog();
|
loadBatteryLog();
|
||||||
|
|
||||||
// Redraw the canvas chart (and the "Last seen" alert color) with the
|
// Redraw the canvas chart with the new theme's colors as soon as the
|
||||||
// new theme's colors as soon as the toggle is used -- canvas pixels
|
// toggle is used -- canvas pixels don't repaint themselves the way CSS
|
||||||
// don't repaint themselves the way CSS does.
|
// does.
|
||||||
document.addEventListener('themechange', () => {
|
document.addEventListener('themechange', () => {
|
||||||
if (lastBatteryLog) {
|
if (lastBatteryLog) {
|
||||||
drawBatteryChart(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; }
|
.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: sidebar off-canvas, hamburger in a slim top bar. */
|
||||||
.mobile-bar { display: none; }
|
.mobile-bar { display: none; }
|
||||||
.sidebar-backdrop { 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>
|
<button type="button" id="theme-toggle" class="icon-btn" title="Toggle dark mode" aria-label="Toggle dark mode">🌓</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% block device_status %}{% endblock %}
|
||||||
{% block tabs %}{% endblock %}
|
{% block tabs %}{% endblock %}
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% block title %}{{ frame.name or "Frame" }} · Configuration{% endblock %}
|
{% block title %}{{ frame.name or "Frame" }} · Configuration{% endblock %}
|
||||||
{% block page_title %}{{ frame.name or "Frame " ~ frame.id }}{% 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 tabs %}{% include "_frame_tabs.html" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -203,5 +204,6 @@
|
|||||||
window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }};
|
window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }};
|
||||||
window.DEFAULT_PALETTE_HEX = {{ palette_to_hex(default_palette_rgb) | tojson }};
|
window.DEFAULT_PALETTE_HEX = {{ palette_to_hex(default_palette_rgb) | tojson }};
|
||||||
</script>
|
</script>
|
||||||
|
<script src="/static/device_status_bar.js"></script>
|
||||||
<script src="/static/frame_config.js"></script>
|
<script src="/static/frame_config.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% block title %}{{ frame.name or "Frame" }} · Photos{% endblock %}
|
{% block title %}{{ frame.name or "Frame" }} · Photos{% endblock %}
|
||||||
{% block page_title %}{{ frame.name or "Frame " ~ frame.id }}{% 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 tabs %}{% include "_frame_tabs.html" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }};</script>
|
<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/queue.js"></script>
|
||||||
<script src="/static/frame_photos.js"></script>
|
<script src="/static/frame_photos.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,11 +3,10 @@
|
|||||||
{% block title %}{{ frame.name or "Frame" }} · Stats{% endblock %}
|
{% block title %}{{ frame.name or "Frame" }} · Stats{% endblock %}
|
||||||
{% block page_title %}{{ frame.name or "Frame " ~ frame.id }}{% 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 tabs %}{% include "_frame_tabs.html" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="layout">
|
|
||||||
<div class="main-col">
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2 class="card-title">Battery history</h2>
|
<h2 class="card-title">Battery history</h2>
|
||||||
<div id="battery-chart-wrap"><p class="sub">Loading...</p></div>
|
<div id="battery-chart-wrap"><p class="sub">Loading...</p></div>
|
||||||
@@ -17,15 +16,6 @@
|
|||||||
<h2 class="card-title">Lifetime stats</h2>
|
<h2 class="card-title">Lifetime stats</h2>
|
||||||
<div id="stats-box"><p class="sub">Loading...</p></div>
|
<div id="stats-box"><p class="sub">Loading...</p></div>
|
||||||
</section>
|
</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>
|
<div id="result"></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -33,5 +23,6 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }};</script>
|
<script>window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }};</script>
|
||||||
<script src="/static/battery_chart.js"></script>
|
<script src="/static/battery_chart.js"></script>
|
||||||
|
<script src="/static/device_status_bar.js"></script>
|
||||||
<script src="/static/frame_stats.js"></script>
|
<script src="/static/frame_stats.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user