Replaces the hand-written Tailwind subset in app.css with a real, purged
Tailwind build: scripts/build_css.py runs the pinned, SHA-256-checked
standalone Tailwind CLI (no Node), the generated tailwind.css and
plugin-frame.css are committed, and CI fails when they are stale. The Pi
never builds anything. The login page (#683) now links tailwind.css too,
and the load-order test covers every template that links app.css.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 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>