Files
LEDMatrix/docs/audits/WEB_UI_AUDIT_2026-09.md
ChuckandClaude Opus 5.5 a231d4dbc7 chore: delete unreferenced scripts and archived docs; fix stale doc claims (#607)
* 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>
2026-09-23 12:36:38 -04:00

8.6 KiB
Raw Permalink Blame History

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.
  • .hidden has never existed in app.css, so the 145 classList.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-75 undefined).

Other drift:

  • Four competing showNotification definitions (app.js:6, app-shell.js:2464, widgets/notification.js:298, partials/fonts.html:249) — the winner depends on load order — plus 53 alert()/confirm() calls.
  • At least four modal implementations (on-demand modal in base.html:1032, Tailwind-UI style in error_handler.js/diff_viewer.js, .jfm-*/.pfm-* with injected CSS, ad-hoc modals in plugins_manager.js).
  • .btn mixed with ~90 hand-assembled color-utility button combos.
  • SSE wiring duplicated in app-shell.js:5-60 and app.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-inset and --tw-ring-offset-width, which are never defined, so the box-shadow is invalid. focus:outline-none (18 uses) does remove the outline. peer-focus:ring-4 has no rule, so the plugin enable toggle (plugins_manager.js:1586-1593, sr-only checkbox) shows no focus. WCAG 2.4.7.
  • Modals lack dialog semantics. Only json-file-manager.js has role="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.html 19, starlark_config.html 14, plugins.html 11); confirmed live on the 11 store search/sort/filter inputs.
  • Captive WiFi setup page (first-run surface): #msg status has no live region, and outline:none is replaced by a 15%-alpha shadow (captive_setup.html:16,48).
  • Background traffic never stops. /stream/stats and /stream/display SSE stay open on every tab (display frames push with no preview visible). Tab timers keep running after leaving the tab (display.html:1046 5s, logs.html:222 5s, tools.html:987 15s, plugins_manager.js:1882 15s, update check base.html:1196 30min). Only tools.html:999 checks visibilitychange. 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.js is 277 KB.
  • Dark mode leaks. plugin-file-manager.js (53 hex) and json-file-manager.js (63 hex, e.g. .jfm-modal-box{background:#fff}) inject CSS that ignores data-theme; .form-control hard-codes #fff/#111827 (app.css:668-671). app.css has 186 hex + 46 rgb literals vs 94 var(--…) uses.

P2

  • Toasts: role="alert" inside an aria-live="polite" container (notification.js:78,156) → double/assertive announcements; auto-dismiss 4s.
  • prefers-reduced-motion covers 3 animations; ~106 animate-pulse/fa-spin uses, 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-container forced to 400/350px with !important (app.css:399-411).
  • Logs panel contrast: text-gray-400 on bg-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 innerHTML on every filter change (plugins_manager.js:1554, 3784, 3993, 4389, 5905); logs.html:225 adds a reflow-forcing resize listener on every partial load.

P3

  • No loading="lazy" on images; Font Awesome font-display:block.
  • Unpinned alpinejs@3.x.x unpkg fallback (base.html:241).
  • widgets/example-color-picker.js is 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=20260307 on two script tags is merely redundant.
  • Light-mode gray text contrast is mostly fine: app.css remaps grays darker (4.8–10:1).
  • Detector gray-on-color hits at app.css:84,285 and broken-image hits (JS-populated src) 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.js uses role="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?
  1. [P0] /impeccable harden — fix the utility layer (real Tailwind build or define missing classes + button reset).
  2. [P1] /impeccable harden — focus-ring variables and peer-focus; one shared accessible modal helper; name icon buttons and label fields; live region on the captive page.
  3. [P1] /impeccable optimize — pause SSE/timers on hidden tab or page; load widget scripts on demand.
  4. [P1] /impeccable colorize — move file-manager CSS and .form-control onto theme tokens.
  5. [P2] /impeccable adapt — header wrap, touch targets, log height.
  6. [P2] /impeccable animate — reduced-motion alternatives.
  7. /impeccable polish — final pass.