mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 14:55:08 +00:00
fix(web): harden, polish and optimize the web UI per the Sept 2026 audit (#568)
* fix(web): harden, polish and optimize the web UI per the September 2026 audit Works through docs/archive/WEB_UI_AUDIT_2026-09.md (health 8/20). Implementation integrity (P0) - app.css now defines every utility class the templates and JS use, including .hidden, so the ~145 JS show/hide toggles work. Button reset, and base component rules (.btn, .form-control) wrapped in :where() so utility classes on the same element win. New static-audit test fails when a used utility class has no rule. Accessibility - Focus rings render (the old ring rule referenced undefined variables); one :focus-visible outline everywhere; skip link; labelled nav landmarks. - Shared dialog helper (js/utils/dialog.js): role/aria-modal, focus trap, Escape, focus return, applied to every modal. - Named icon-only buttons and labelled ~70 form fields. - Toasts announced once; errors persist >= 10s; one showNotification. - Captive WiFi page: live region, timeouts, dark mode, 16px inputs. Performance (Pi Zero 2 W) - SSE streams and tab timers pause when hidden or off-tab; the display stream only runs while a preview is visible. app-shell.js deferred. - Widget scripts served as one versioned bundle (/assets/widgets.js): 52 -> 21 script tags, 66 -> 35 requests on first load. - Stdlib gzip fallback when flask-compress is missing: first-load JS/CSS 1358 KB -> 291 KB on the wire. SSE untouched. Theming and responsive - File managers, form fields and Fonts upload on theme tokens; bare inputs themed in dark mode; no more white surfaces. - No horizontal overflow at 375px on any tab; 44px touch targets on coarse pointers; reduced-motion respected; header title truncates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web): clear Codacy findings on #568 - json-file-manager: focus-trap releases kept in a Map (no dynamic property access or delete; no value-returning forEach callback) - notification / schedule-picker: style and day-label lookups via Map - app.js: move the pending-queue assignment out of the expression - diff_viewer / error_handler: named function declarations instead of arrow consts No behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: check the OAuth widget ships in the widget bundle base.html no longer tags widget scripts one by one; they load through /assets/widgets.js. Assert the page requests the bundle and the bundle contains google-oauth.js, which is what the test was protecting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web): address review feedback on #568 - widget bundle version fingerprints every file (name, mtime_ns, size) - gzip fallback appends Accept-Encoding to an existing Vary header - dialog helper: releasing a non-top dialog no longer moves focus out of the dialog the user is in - labels: file-upload targets its file input; fallback config fields get label for/id pairs; native color input has a fallback name - utility audit also reads class names inside bound :class expressions Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web): give the native color-picker input an accessible name CodeRabbit flagged this on PR #568 as an outside-diff finding (never posted inline, so it was missed in the round of fixes that addressed the other 6 review comments). The <input type="color"> only carried a title attribute; screen readers don't reliably announce title, and there's no other label naming the control when showHexInput is false. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web): clear Codacy findings in app-shell.js - drop the unused catch binding on the SSE JSON parse - move the pending-notification queue assignment out of the expression No behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web): contain plugin widgets/ dir and bound style-editor retries From CodeRabbit review on #568 (code that arrived with the main merge): - serve_plugin_widget resolves widgets/ with resolve_under before resolving the manifest script under it, so a symlinked widgets directory can't become the containment base (CWE-22). New test. - style-editor init stops polling after ~10s when the widget never registers and leaves the plain fallback fields in place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,7 @@
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Switch branch -->
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900">Branch</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Choose which branch this pi follows. Switching attaches tracking, so Pull Latest works afterwards.</p>
|
||||
@@ -50,7 +50,7 @@
|
||||
<div id="result-checkout-branch" class="hidden"></div>
|
||||
|
||||
<!-- Pull latest -->
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Pull latest (rebase)</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Stashes any local changes, then runs <code class="bg-gray-100 px-1 rounded">git pull --rebase</code>. The stash is preserved but not re-applied.</p>
|
||||
@@ -63,7 +63,7 @@
|
||||
<div id="result-git-pull" class="hidden"></div>
|
||||
|
||||
<!-- Force reset -->
|
||||
<div class="flex items-start justify-between gap-4 pt-4 border-t border-gray-100">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-start justify-between gap-4 pt-4 border-t border-gray-100">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Force reset to <code class="bg-gray-100 px-1 rounded">origin/main</code></p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Runs <code class="bg-gray-100 px-1 rounded">git fetch origin</code> then <code class="bg-gray-100 px-1 rounded">git reset --hard origin/main</code>. Discards all local changes.</p>
|
||||
@@ -99,7 +99,7 @@
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Base requirements -->
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Reinstall base requirements</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Installs from <code class="bg-gray-100 px-1 rounded">requirements.txt</code> in the project root.</p>
|
||||
@@ -112,7 +112,7 @@
|
||||
<div id="result-base-reqs" class="hidden"></div>
|
||||
|
||||
<!-- Plugin requirements -->
|
||||
<div class="flex items-start justify-between gap-4 pt-4 border-t border-gray-100">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-start justify-between gap-4 pt-4 border-t border-gray-100">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Reinstall plugin requirements</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Runs <code class="bg-gray-100 px-1 rounded">pip install</code> for every installed plugin that has a <code class="bg-gray-100 px-1 rounded">requirements.txt</code>.</p>
|
||||
@@ -134,7 +134,7 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Clear Python cache</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Deletes all <code class="bg-gray-100 px-1 rounded">__pycache__</code> directories in the project. Useful after switching branches or debugging import issues.</p>
|
||||
@@ -166,14 +166,15 @@
|
||||
<p class="mt-1 text-sm text-gray-600">Turn the WiFi radio on or off. Full WiFi setup (scan, connect, hotspot) lives on the <span class="font-medium">WiFi</span> tab.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">WiFi radio</p>
|
||||
<p id="wifi-radio-label" class="text-sm font-medium text-gray-900">WiFi radio</p>
|
||||
<p id="wifi-radio-note" class="text-xs text-gray-500 mt-0.5">Checking current state…</p>
|
||||
</div>
|
||||
<div class="shrink-0 flex flex-col items-end gap-2">
|
||||
<!-- Toggle switch -->
|
||||
<button id="wifi-radio-toggle" type="button" role="switch" aria-checked="false"
|
||||
aria-labelledby="wifi-radio-label" aria-describedby="wifi-radio-note"
|
||||
onclick="onWifiRadioToggleClick()" disabled
|
||||
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-gray-200 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 opacity-60">
|
||||
<span id="wifi-radio-knob" aria-hidden="true"
|
||||
@@ -207,7 +208,7 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Restart display service</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Restarts <code class="bg-gray-100 px-1 rounded">ledmatrix.service</code>.</p>
|
||||
@@ -219,7 +220,7 @@
|
||||
</div>
|
||||
<div id="result-restart-display" class="hidden"></div>
|
||||
|
||||
<div class="flex items-start justify-between gap-4 pt-4 border-t border-gray-100">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-start justify-between gap-4 pt-4 border-t border-gray-100">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Restart web interface</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Restarts <code class="bg-gray-100 px-1 rounded">ledmatrix-web.service</code>. The page will go offline briefly.</p>
|
||||
@@ -282,7 +283,7 @@
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Reboot -->
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Reboot</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Runs <code class="bg-gray-100 px-1 rounded">sudo reboot</code>. The Pi will restart and come back online in a minute or two.</p>
|
||||
@@ -308,7 +309,7 @@
|
||||
<div id="result-reboot" class="hidden"></div>
|
||||
|
||||
<!-- Shutdown -->
|
||||
<div class="flex items-start justify-between gap-4 pt-4 border-t border-gray-100">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-start justify-between gap-4 pt-4 border-t border-gray-100">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Shut down</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Runs <code class="bg-gray-100 px-1 rounded">sudo poweroff</code>. The Pi will power off and must be unplugged/replugged (or power-cycled) to turn back on.</p>
|
||||
@@ -979,26 +980,31 @@
|
||||
loadGitInfo();
|
||||
loadBranches();
|
||||
|
||||
// Plugin health: initial load + periodic refresh. Guard against duplicate
|
||||
// timers if this partial is re-swapped in by HTMX; the handler re-resolves
|
||||
// DOM nodes by id each tick.
|
||||
refreshPluginHealth(false);
|
||||
if (!window._pluginHealthTimer) {
|
||||
window._pluginHealthTimer = setInterval(function () { refreshPluginHealth(true); }, 15000);
|
||||
// Plugin health (15s) and system diagnostics (10s): the partial stays in
|
||||
// the DOM (hidden via x-show) when another tab is active, so both polls
|
||||
// run only while the Tools tab is active and the page is visible — each
|
||||
// restarts with an immediate refresh. LEDVisibility keys the registration,
|
||||
// so a partial re-swapped in by HTMX replaces (not stacks) the timers.
|
||||
function stopToolsPolling() {
|
||||
if (window._pluginHealthTimer) { clearInterval(window._pluginHealthTimer); window._pluginHealthTimer = null; }
|
||||
if (window._diagPollInterval) { clearInterval(window._diagPollInterval); window._diagPollInterval = null; }
|
||||
}
|
||||
|
||||
// System diagnostics: load now, then refresh every 10s. Clear any prior
|
||||
// interval so re-swapping the partial doesn't stack. The recurring poll is
|
||||
// gated on visibility — the partial stays in the DOM (hidden via x-show)
|
||||
// when another tab is active, so without this it would keep hitting
|
||||
// /api/v3/system/status every 10s and churn the Pi while off-screen.
|
||||
if (window._diagPollInterval) clearInterval(window._diagPollInterval);
|
||||
window.loadSystemDiagnostics();
|
||||
window._diagPollInterval = setInterval(function () {
|
||||
const panel = document.getElementById('diag-panel');
|
||||
if (!panel || document.hidden || panel.offsetParent === null) return;
|
||||
function startToolsPolling() {
|
||||
stopToolsPolling();
|
||||
refreshPluginHealth(false);
|
||||
window._pluginHealthTimer = setInterval(function () { refreshPluginHealth(true); }, 15000);
|
||||
window.loadSystemDiagnostics();
|
||||
}, 10000);
|
||||
window._diagPollInterval = setInterval(function () {
|
||||
const panel = document.getElementById('diag-panel');
|
||||
if (!panel || document.hidden || panel.offsetParent === null) return;
|
||||
window.loadSystemDiagnostics();
|
||||
}, 10000);
|
||||
}
|
||||
if (window.LEDVisibility) {
|
||||
window.LEDVisibility.onActive('tools', startToolsPolling, stopToolsPolling);
|
||||
} else {
|
||||
startToolsPolling();
|
||||
}
|
||||
|
||||
// WiFi radio current state.
|
||||
window.loadWifiRadio();
|
||||
@@ -1223,7 +1229,7 @@
|
||||
|
||||
const active = status.running ? `
|
||||
<div class="rounded-md border border-amber-300 bg-amber-50 p-3">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-amber-900">
|
||||
<i class="fas fa-circle-notch fa-spin mr-1"></i>
|
||||
|
||||
Reference in New Issue
Block a user