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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
ChuckBuilds
2026-07-16 12:19:23 -04:00
co-authored by Claude Sonnet 5
parent 05aee74147
commit 69863f70cd
6 changed files with 165 additions and 0 deletions
+36
View File
@@ -5,6 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Matrix Control Panel</title>
<!-- Installable web app (Add to Home Screen) -->
<link rel="manifest" href="{{ url_for('static', filename='v3/manifest.json') }}">
<meta name="theme-color" content="#111827">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='v3/icons/apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="192x192" href="{{ url_for('static', filename='v3/icons/icon-192.png') }}">
<!-- Debug logging gate: verbose console output is suppressed unless
localStorage.pluginDebug === 'true' (the same switch the plugin
scripts already use). console.error/warn are never gated. -->
@@ -396,6 +402,36 @@
</div>
</div>
<!-- Restart-pending banner: shown after a main-config save (display
hardware, rotation order, durations, general settings) — those only
take effect after the display service restarts. Plugin config saves
apply live and don't trigger this. Wired in app.js. -->
<div id="restart-pending-banner" style="display:none"
class="update-banner border-b transition-all duration-300 ease-in-out">
<div class="mx-auto px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-2" style="max-width:100%">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<i class="fas fa-rotate text-lg"></i>
<span class="text-sm font-medium" aria-live="polite">
Configuration saved &mdash; restart the display to apply the changes
</span>
</div>
<div class="flex items-center space-x-3">
<button onclick="window.restartPendingNow()" id="restart-pending-btn"
class="inline-flex items-center px-3 py-1 text-xs font-semibold rounded-md
update-banner-action transition-colors duration-150">
<i class="fas fa-power-off mr-1"></i> Restart Now
</button>
<button type="button" onclick="window.dismissRestartPending()"
class="update-banner-dismiss rounded p-1 transition-colors duration-150"
title="Dismiss" aria-label="Dismiss restart reminder">
<i class="fas fa-times text-sm"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Under-voltage / throttling warning banner -->
<div id="power-warning-banner" style="display:none"
class="power-warning-banner border-b transition-all duration-300 ease-in-out">