* chore(scripts): delete unreferenced helper scripts None of these is referenced by an installer, systemd unit, CI workflow, test, the web UI or src/: - utils/cleanup_venv.sh removes venv_web_v2, which nothing creates - utils/clear_python_cache.sh hardcodes ~/LEDMatrix and a .webassets-cache nothing uses - install/migrate_config.sh only copies the template, which the installer and ConfigManager already do - install/debug_install.sh, debug/debug_web_manual.py - diagnose_web_ui.sh and verify_web_ui.sh overlap diagnose_web_interface.sh, which the docs point to - fix_internet_connectivity.sh is iptables-only (stale on nftables) - diagnose_plugin_permissions.sh, dev/validate_python.py - download_nba_logos.py + README_NBA_LOGOS.md: logo_downloader fetches logos on demand - setup_plugin_repos.py linked into the production plugin-repos/ dir; the dev workflow is scripts/dev/dev_plugin_setup.sh, and MULTI_ROOT_WORKSPACE_SETUP.md now uses it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(config): drop unused plugin_system flags and a dead unit comment - config.template.json: remove plugin_system.auto_discover, auto_load_enabled and development_mode. Nothing reads them; the web UI only stores them when a client sends them. ConfigManager's migration only adds template keys, so existing configs keep theirs unchanged. - config.template.json: re-indent vegas_scroll's live_* keys. - systemd/ledmatrix.service: remove the comment documenting LEDMATRIX_ON_DEMAND_PLUGIN / on_demand_env.conf; nothing reads either. - CONFIG_REFERENCE.md: say the legacy keys are no longer in the template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: delete docs/archive and PLUGIN_IMPLEMENTATION_SUMMARY.md - docs/archive/: superseded guides; the repository history keeps them and no live doc links into the directory. The one open document in it, WEB_UI_AUDIT_2026-09.md, moves to docs/audits/ and is linked from the docs index. - PLUGIN_IMPLEMENTATION_SUMMARY.md invented usage statistics, called v2.0.0 current, listed shipped auto-updates as future work and documented a BasePlugin.get_config() that does not exist. - docs/README.md: drop both, and stop telling contributors to archive obsolete pages instead of deleting them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(plugin-api): fix extra_small_font size, cache metric key and scroll pacing example - PLUGIN_API_REFERENCE: extra_small_font loads at 7, not 6 (crisp_size snaps it, src/display_manager.py); get_cache_metrics() returns cache_hit_rate, not hit_rate (src/cache/cache_metrics.py). - ADVANCED_PLUGIN_DEVELOPMENT: the basic scrolling example slept in a loop and never passed frame_hold; use ScrollHelper + scroll_config.configure() and set_scrolling_state(True, frame_hold=...) as PLUGIN_API_REFERENCE does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(plugin-config): match the config tab, icon and web-action docs to the code - PLUGIN_CONFIG_QUICK_START / PLUGIN_CONFIGURATION_TABS / PLUGIN_CONFIGURATION_GUIDE: there is no "Reset to Defaults" button (the tab has Refresh, Update, Uninstall, Save Configuration); plugin config hot-reloads (ConfigService + on_config_change), so no restart; the schema is found by the fixed name config_schema.json, not a manifest config_schema field; the tab row is "Plugin Manager", not "Plugins"; forms are server-rendered from /v3/partials/plugin-config/<id>; the duration hook is get_display_duration()/display_duration; a class_name mismatch raises PluginError; the store requires id, name, class_name and display_modes (not version); plugin_system.debug/log_level do not exist (use run.py -d / LEDMATRIX_DEBUG). Drop "future" features that shipped. - PLUGIN_CONFIG_CORE_PROPERTIES: list all of CORE_PLUGIN_PROPERTIES, including skin, skin_options and the vegas_* tuning keys. - PLUGIN_CUSTOM_ICONS: icon is only a Font Awesome class (fallback fa-puzzle-piece); emoji/URL icons and getPluginIcon() never existed in v3. Note that /api/v3/plugins/installed currently omits icon. - PLUGIN_WEB_UI_ACTIONS (+ example JSON): success_message, error_message and step1_message are never read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(store): describe the monorepo registry and the store UI as they are - PLUGIN_STORE_GUIDE: the Plugin Store is a section of the Plugin Manager tab; URL installs are "Install from GitHub" -> "Install Single Plugin"; bulk update exists (Check & Update All) plus opt-in weekly auto-update; PluginStoreManager() defaults to plugins/, so the Python examples pass plugin-repos; registry plugins are downloaded (GitHub API, ZIP fallback), not cloned; updates compare version with latest_version. - PLUGIN_REGISTRY_SETUP_GUIDE: replace the per-plugin-repo + tag walkthrough with a short page on the monorepo registry (plugin_path, latest_version, update_registry.py) that points at the monorepo's own SUBMISSION.md. Drops the reference to the deleted PLUGIN_IMPLEMENTATION_SUMMARY.md and setup_plugin_repos.py. - plugin_registry_template.json: use the real entry shape. - PLUGIN_QUICK_REFERENCE: automatic background updates exist (opt-in); registry example and publishing steps use the monorepo, not tags. - PLUGIN_DEVELOPMENT_GUIDE: tags/releases are not read by the store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(readme): fix the Triple Bonnet mapping, install prerequisites and backup names - README: the Adafruit Triple Bonnet uses `regular` (3 outputs), not `regular-pi1` (1 output) -- src/matrix_support.py MAPPING_OUTPUTS, and the README's own hardware_mapping section; the template default mapping is adafruit-hat, the PWM mod switches it to adafruit-hat-pwm; manual install only needs git up front (first_time_install.sh installs python-dev-is-python3, cmake, ninja-build etc.; cython3/scons are not used); the Pi Zero 2 W is a supported low-memory board, consistent with PRODUCT.md, LOW_MEMORY_BOARDS.md and the installer's low-memory build; fix the "First_time_install.sh" spelling, an orphan "2." list item and the hello-world starter link (it lives in the plugins monorepo). - CONFIG_DEBUGGING: automatic backups are config/backups/config.json.backup.<YYYYMMDD_HHMMSS_ffffff> (five kept), not config_YYYYMMDD_HHMMSS.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(dev): correct the test-running and rgbmatrix build instructions - HOW_TO_RUN_TESTS: coverage is not collected by a plain pytest run and pytest.ini has no threshold; the only one is --cov-fail-under=52 in the core unit-test job of .github/workflows/test.yml, which runs the whole test/ tree (not an allowlist). Almost no tests carry markers, so -m integration / -m slow select nothing; drop them and -m unit as the quick check. Replace the hardcoded /home/chuck path. - DEVELOPMENT: the rgbmatrix package is built with pip install . from the submodule root (scikit-build-core + CMake + Ninja), as first_time_install.sh does; there is no make build-python / bindings/python step, and the build deps are python-dev-is-python3, cmake and ninja-build, not cython3/scons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(wifi): the setup AP is open; auto-enable can be turned off without code changes - WIFI_NETWORK_SETUP / SSH_UNAVAILABLE_AFTER_INSTALL: both AP paths in src/wifi_manager.py create an open network and nothing reads ap_password, so drop the "ledmatrix123" password and the ap_password key/advice. - SSH_UNAVAILABLE_AFTER_INSTALL: disabling automatic AP mode does not need code changes -- auto_enable_ap_mode is a WiFi-tab toggle and POST /api/v3/wifi/ap/auto-enable; note the monitor daemon reads wifi_config.json at start, so restart it after changing the setting. Use the ledpi username and a relative install path like the other docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(reference): add auto_update, drop drifted line numbers, fix UI and service details - CONFIG_REFERENCE: document the top-level auto_update.enabled key (read by web_interface/auto_update.py and src/auto_update_setup.py); replace drifted file:line references with function names; the template's dim_schedule mode is "global". - ADVANCED_FEATURES: core does not read a per-plugin background_service block (the sports plugins read their own), and priority is "higher number = higher priority" on FetchRequest but not used for ordering. - WEB_INTERFACE_GUIDE: the General tab toggle is "Web Display Autostart" (web interface service), brightness is 1-100, and config paths are relative to the LEDMatrix folder, not /config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: drop references to code removed in #608 get_installed_plugin_info, WiFiManager's saved_networks and the six always-skipping plugin test files are deleted there. NetworkManager already remembers joined networks; LEDMatrix no longer stores WiFi passwords. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: don't link SKIN_SYSTEM.md from the core-properties page #615 deletes SKIN_SYSTEM.md; with this link, whichever of the two merged second would break test_doc_links. The skin/skin_options entries go when #615 removes the keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
8.6 KiB
Web UI Technical Audit — September 2026
Scope: web_interface/ (Flask + HTMX + Alpine, templates/v3/, static/v3/).
Method: Impeccable design detector, code review (accessibility; performance,
theming, responsive), and a live pass on the running app at desktop and
375px mobile widths in light and dark themes. Severe claims were verified
against the live page; one was rejected (see below). No code was changed.
Product context: see PRODUCT.md.
Health score: 8/20 (Poor)
| # | Dimension | Score | Key finding |
|---|---|---|---|
| 1 | Accessibility | 2 | Focus rings never render; modals have no dialog semantics or focus management |
| 2 | Performance | 2 | ~1.2 MB JS (≈250 KB gzip) on every page; SSE streams and polling never pause |
| 3 | Responsive | 2 | Mobile drawer works; header title wraps to 3 lines; many ~24px touch targets |
| 4 | Theming | 1 | Tokens exist but hex dominates; dark mode is a class-by-class patch with leaks |
| 5 | Implementation integrity | 1 | Templates use Tailwind classes that don't exist in the stylesheet |
Implementation integrity verdict: fail
There is no Tailwind build. static/v3/app.css is a hand-written subset of
Tailwind, while templates and JS are authored as if full Tailwind were loaded.
- 333 of 516 utility class names used have no CSS rule (2,582 uses),
confirmed against the live stylesheets. Top offenders:
border(250),text-gray-700(183),block(148),mr-1(127),hidden(79),py-1,text-blue-600,px-2,text-center,hover:bg-blue-700,divide-y,uppercase,font-mono. .hiddenhas never existed inapp.css, so the 145classList.add/remove/toggle('hidden')calls across 26 files do nothing. Visible proof: the header shows both the moon and sun theme icons.- 15 classes are defined only under
[data-theme="dark"](e.g.bg-blue-50,bg-red-50,bg-yellow-50,border-blue-200,text-red-700), so tinted notice boxes are unstyled in light mode. - Visible damage: the Getting Started checklist (
partials/overview.html:96-117) renders native gray outset buttons in both themes; 32 visible buttons on the Plugin Manager page render with default browser chrome; search icons overlap inputs; error/diff modal backdrops are transparent (bg-gray-500 bg-opacity-75undefined).
Other drift:
- Four competing
showNotificationdefinitions (app.js:6,app-shell.js:2464,widgets/notification.js:298,partials/fonts.html:249) — the winner depends on load order — plus 53alert()/confirm()calls. - At least four modal implementations (on-demand modal in
base.html:1032, Tailwind-UI style inerror_handler.js/diff_viewer.js,.jfm-*/.pfm-*with injected CSS, ad-hoc modals inplugins_manager.js). .btnmixed with ~90 hand-assembled color-utility button combos.- SSE wiring duplicated in
app-shell.js:5-60andapp.js:186-205.
Findings by severity
P0
Undefined utility layer (above). Every show/hide toggle and every layout
built from missing classes silently fails; root cause of most visual bugs.
Fix: replace the hand-rolled subset with a real, purged Tailwind build
(with dark-mode variants), or at minimum define the high-use missing classes
(hidden, border, block, spacing/text utilities) and a button reset.
→ /impeccable harden
P1
- Focus rings never render.
focus:ring-2(app.css:289-291) references--tw-ring-insetand--tw-ring-offset-width, which are never defined, so thebox-shadowis invalid.focus:outline-none(18 uses) does remove the outline.peer-focus:ring-4has no rule, so the plugin enable toggle (plugins_manager.js:1586-1593,sr-onlycheckbox) shows no focus. WCAG 2.4.7. - Modals lack dialog semantics. Only
json-file-manager.jshasrole="dialog"/aria-modal/Escape/initial focus; none trap focus or return it. On-demand (base.html:1032), error (error_handler.js:164-205), diff (diff_viewer.js:211-214), plugin file manager (plugin-file-manager.js:372-392, 576-599), array-table editor (array-table.js:460-467) have none of it. WCAG 2.1.2 / 4.1.2. - Unnamed controls. ~16 icon-only buttons with no accessible name, e.g.
base.html:1036,plugins.html:173,210,plugin_config.html:485,656,number-input.js:102,129,text-input.js:120,date-picker.js:95,time-picker.js:100,password-input.js:141. ~115 of 245 form fields have no label (hotspots:plugin_config.html19,starlark_config.html14,plugins.html11); confirmed live on the 11 store search/sort/filter inputs. - Captive WiFi setup page (first-run surface):
#msgstatus has no live region, andoutline:noneis replaced by a 15%-alpha shadow (captive_setup.html:16,48). - Background traffic never stops.
/stream/statsand/stream/displaySSE stay open on every tab (display frames push with no preview visible). Tab timers keep running after leaving the tab (display.html:10465s,logs.html:2225s,tools.html:98715s,plugins_manager.js:188215s, update checkbase.html:119630min). Onlytools.html:999checksvisibilitychange. Costly on a Pi Zero 2 W. - Page weight. 47 script tags on every page, including all 33 widgets
(
base.html:984-1018).app-shell.js(177 KB) is render-blocking (base.html:956);plugins_manager.jsis 277 KB. - Dark mode leaks.
plugin-file-manager.js(53 hex) andjson-file-manager.js(63 hex, e.g..jfm-modal-box{background:#fff}) inject CSS that ignoresdata-theme;.form-controlhard-codes#fff/#111827(app.css:668-671).app.csshas 186 hex + 46 rgb literals vs 94var(--…)uses.
P2
- Toasts:
role="alert"inside anaria-live="polite"container (notification.js:78,156) → double/assertive announcements; auto-dismiss 4s. prefers-reduced-motioncovers 3 animations; ~106animate-pulse/fa-spinuses,modalSlideIn, and toast slides ignore it.- Mobile: header title wraps to three lines and spills out of the header;
~33 plugin-card buttons are
text-xs px-2 py-1(~24px);#logs-containerforced to 400/350px with!important(app.css:399-411). - Logs panel contrast:
text-gray-400onbg-gray-900≈ 3.9:1 (logs.html:75,86). - Three unnamed nested
<nav>landmarks (base.html:474,477,548); no skip link. - Plugin lists fully rebuilt via
innerHTMLon every filter change (plugins_manager.js:1554, 3784, 3993, 4389, 5905);logs.html:225adds a reflow-forcing resize listener on every partial load.
P3
- No
loading="lazy"on images; Font Awesomefont-display:block. - Unpinned
alpinejs@3.x.xunpkg fallback (base.html:241). widgets/example-color-picker.jsis not loaded anywhere.- Detector: 3px accent stripe on
.plugin-card::before(app.css:721).
Verified and rejected
- "Static assets are never cache-busted" (raised as P0): false.
app.py:491(@app.url_defaults add_static_version) appends file mtime as?v=to every static URL; the live HTML confirms it. The manual?v=20260307on two script tags is merely redundant. - Light-mode gray text contrast is mostly fine:
app.cssremaps grays darker (4.8–10:1). - Detector
gray-on-colorhits atapp.css:84,285andbroken-imagehits (JS-populatedsrc) are not real rendered issues.
What works
- Theme set before first paint, follows OS preference,
data-theme+ tokens. - Mobile drawer: Escape closes it, focus returns to the hamburger, 44px rows.
aria-current="page"on nav tabs; real<header>and<main>.- Status colors always paired with text; nearly all images have alt text.
toggle-switch.jsusesrole="switch"; vendor assets self-hosted.
Open decisions (block the P0 fix approach)
Recorded as undecided in PRODUCT.md:
- Must the UI work fully offline (no CDN fallbacks)?
- Is a Node/CSS build step acceptable for contributors?
- Is WCAG 2.2 AA a formal requirement?
Recommended order
- [P0]
/impeccable harden— fix the utility layer (real Tailwind build or define missing classes + button reset). - [P1]
/impeccable harden— focus-ring variables andpeer-focus; one shared accessible modal helper; name icon buttons and label fields; live region on the captive page. - [P1]
/impeccable optimize— pause SSE/timers on hidden tab or page; load widget scripts on demand. - [P1]
/impeccable colorize— move file-manager CSS and.form-controlonto theme tokens. - [P2]
/impeccable adapt— header wrap, touch targets, log height. - [P2]
/impeccable animate— reduced-motion alternatives. /impeccable polish— final pass.