From 69863f70cd41f7fd4063be6dfee37e3994b642f9 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Thu, 16 Jul 2026 12:19:23 -0400 Subject: [PATCH] feat(web): restart-pending banner, unsaved-changes guard, installable web app Three usability improvements from live testing feedback: - Restart-pending banner: every successful POST to /api/v3/config/main (display hardware, rotation/durations, general settings) now raises a persistent banner - "Configuration saved, restart the display to apply" - with a Restart Now button that posts restart_display_service directly. Backed by sessionStorage so it survives tab switches and reloads until restarted or dismissed. Plugin config saves are deliberately excluded: they apply live via the display process's config watcher. - Unsaved-changes guard: plugin config panels are Alpine x-if templates, so navigating away destroys the panel and revisiting re-fetches it - edits were silently discarded. Forms now mark themselves dirty on input (cleared on successful submit), a capture-phase click handler confirms before a lossy tab switch, and beforeunload guards full page unloads. System tabs (x-show, persistent DOM) are exempt - no false prompts. - Installable web app: manifest.json (standalone display, dark theme) + generated LED-grid icons (192/512 maskable + 180 apple-touch), linked from base.html. "Add to Home Screen" now yields an app-like fullscreen experience; no service worker, so zero behavioral risk. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ --- web_interface/static/v3/app.js | 105 ++++++++++++++++++ .../static/v3/icons/apple-touch-icon.png | Bin 0 -> 5562 bytes web_interface/static/v3/icons/icon-192.png | Bin 0 -> 1961 bytes web_interface/static/v3/icons/icon-512.png | Bin 0 -> 7621 bytes web_interface/static/v3/manifest.json | 24 ++++ web_interface/templates/v3/base.html | 36 ++++++ 6 files changed, 165 insertions(+) create mode 100644 web_interface/static/v3/icons/apple-touch-icon.png create mode 100644 web_interface/static/v3/icons/icon-192.png create mode 100644 web_interface/static/v3/icons/icon-512.png create mode 100644 web_interface/static/v3/manifest.json diff --git a/web_interface/static/v3/app.js b/web_interface/static/v3/app.js index dd17fc7d..f2739ddc 100644 --- a/web_interface/static/v3/app.js +++ b/web_interface/static/v3/app.js @@ -49,6 +49,111 @@ document.body.addEventListener('htmx:afterRequest', function(event) { // Not JSON, ignore } } + + // Main-config saves (display hardware, rotation/durations, general) only + // take effect after a display-service restart — surface the reminder + // banner. Plugin config saves apply live and are deliberately excluded. + try { + const cfg = event.detail.requestConfig; + if (cfg && cfg.verb === 'post' && + (cfg.path || '').includes('/api/v3/config/main') && + response && response.status >= 200 && response.status < 300) { + window.showRestartPending(); + } + } catch { /* banner is best-effort */ } +}); + +// ===== Unsaved-changes guard ===== +// Plugin config panels are Alpine x-if templates: navigating away DESTROYS +// the panel and revisiting re-fetches it, silently discarding any edits. +// (System tabs use x-show + data-loaded and persist, so they're exempt.) +// Track dirty forms and confirm before a lossy navigation. +(function() { + function markDirty(e) { + const form = e.target && e.target.closest ? e.target.closest('form') : null; + if (form) form.setAttribute('data-dirty', ''); + } + document.body.addEventListener('input', markDirty); + document.body.addEventListener('change', markDirty); + + // A successful submit makes the form clean again + document.body.addEventListener('htmx:afterRequest', function(event) { + const xhr = event.detail.xhr; + const form = event.detail.elt && event.detail.elt.closest ? event.detail.elt.closest('form') : null; + if (form && xhr && xhr.status >= 200 && xhr.status < 300) { + form.removeAttribute('data-dirty'); + } + }); + + // Capture phase so this runs before Alpine's bubbling @click switches tabs + document.addEventListener('click', function(e) { + const tabBtn = e.target && e.target.closest ? e.target.closest('.nav-tab') : null; + if (!tabBtn) return; + const lossy = Array.prototype.filter.call( + document.querySelectorAll('.plugin-config-tab form[data-dirty]'), + function(f) { return f.offsetParent !== null; } + ); + if (lossy.length === 0) return; + if (!window.confirm('You have unsaved plugin settings — leaving this page will discard them. Leave anyway?')) { + e.stopPropagation(); + e.preventDefault(); + } + }, true); + + // Full page unload loses every panel's edits + window.addEventListener('beforeunload', function(e) { + const dirty = Array.prototype.some.call( + document.querySelectorAll('form[data-dirty]'), + function(f) { return f.offsetParent !== null; } + ); + if (dirty) { + e.preventDefault(); + e.returnValue = ''; + } + }); +})(); + +// ===== Restart-pending banner ===== +// Shown after restart-requiring saves; persists across tab switches (and +// reloads, via sessionStorage) until the display restarts or it's dismissed. +window.showRestartPending = function() { + try { sessionStorage.setItem('ledmatrix-restart-pending', '1'); } catch { /* private browsing */ } + const banner = document.getElementById('restart-pending-banner'); + if (banner) banner.style.display = 'block'; +}; + +window.dismissRestartPending = function() { + try { sessionStorage.removeItem('ledmatrix-restart-pending'); } catch { /* no-op */ } + const banner = document.getElementById('restart-pending-banner'); + if (banner) banner.style.display = 'none'; +}; + +window.restartPendingNow = function() { + const btn = document.getElementById('restart-pending-btn'); + if (btn) btn.disabled = true; + fetch('/api/v3/system/action', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'restart_display_service' }) + }) + .then(r => r.json()) + .then(data => { + showNotification(data.message || 'Display restarting…', data.status || 'success'); + window.dismissRestartPending(); + }) + .catch(err => { + showNotification('Error restarting display: ' + err.message, 'error'); + }) + .finally(() => { if (btn) btn.disabled = false; }); +}; + +document.addEventListener('DOMContentLoaded', function() { + try { + if (sessionStorage.getItem('ledmatrix-restart-pending') === '1') { + const banner = document.getElementById('restart-pending-banner'); + if (banner) banner.style.display = 'block'; + } + } catch { /* no-op */ } }); // SSE reconnection helper — closes and reopens both SSE streams, diff --git a/web_interface/static/v3/icons/apple-touch-icon.png b/web_interface/static/v3/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1099cf8a0d9835d249ecb2bc4ec836a748ed33a9 GIT binary patch literal 5562 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 E1^ZPtyZ`_I literal 0 HcmV?d00001 diff --git a/web_interface/static/v3/icons/icon-512.png b/web_interface/static/v3/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..3bed56722b61b34ab8990d94a0c14972ea4498ae GIT binary patch literal 7621 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 GpZ*8BmIL7c literal 0 HcmV?d00001 diff --git a/web_interface/static/v3/manifest.json b/web_interface/static/v3/manifest.json new file mode 100644 index 00000000..1922cf76 --- /dev/null +++ b/web_interface/static/v3/manifest.json @@ -0,0 +1,24 @@ +{ + "name": "LED Matrix Control", + "short_name": "LEDMatrix", + "description": "Control panel for the LEDMatrix display", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#111827", + "theme_color": "#111827", + "icons": [ + { + "src": "/static/v3/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/static/v3/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/web_interface/templates/v3/base.html b/web_interface/templates/v3/base.html index 09000568..89e8b095 100644 --- a/web_interface/templates/v3/base.html +++ b/web_interface/templates/v3/base.html @@ -5,6 +5,12 @@ LED Matrix Control Panel + + + + + + @@ -396,6 +402,36 @@ + + +