fix(web): address CodeRabbit review — validation, a11y, perf, and privacy fixes

Verified each finding against current code. Fixed:

- api_v3: plugin_rotation_order is now strictly validated (JSON list of
  strings, 400 with a descriptive message otherwise) and popped from the
  payload before any further handling.
- display_controller: _apply_plugin_rotation_order defensively ignores a
  non-list value (keeps the existing rotation, logs a warning) and drops
  non-string entries; new logs carry the [DisplayController] prefix.
  Unit-tested both defensive paths.
- app.py: snapshot-read handler narrowed to OSError with debug logging;
  flask-compress ImportError now emits one structured warning with the
  install remedy.
- htmx-config: the response-error logger prints form FIELD NAMES only -
  values (API keys, passwords) never reach the console.
- plugin-order-list: saved order/exclusions normalized with Array.isArray
  (a saved "null" previously crashed .forEach); each row gained
  keyboard/touch-accessible move-up/move-down buttons (HTML5 drag events
  don't fire on most mobile browsers) that reorder and syncInputs()
  immediately alongside native drag.
- app-shell: window.installedPlugins setter always takes the new list
  (same-ID metadata/enabled updates were silently dropped); tab rebuild
  stays gated on ID changes. LED dot renderer reads the frame with ONE
  getImageData call instead of one per pixel (~9,200/frame at 192x48).
- plugins_manager: togglePlugin returns its request promise resolving the
  API outcome; the install flow now shows the "installed and enabled"
  toast (with Restart Now) only after enablement succeeds, and a warning
  without a restart offer when it fails.
- a11y: hamburger aria-label flips Open/Close with drawer state; both
  Advanced-section toggle buttons declare aria-controls/aria-expanded and
  the shared toggleSection() keeps aria-expanded in sync; move buttons
  have per-plugin aria-labels.
- Rotation/Vegas order-list bootstraps cap their retries (~5s) and show a
  reload hint instead of spinning forever; Alpine app-state lookups prefer
  [x-data="app()"] with a generic fallback.

Skipped, with reasons:
- executePluginAction arg order: caller (plugin_config.html) already
  passes (actionId, index, pluginId) matching the signature exactly.
- generateFieldHtml XSS, entity-unescape blocks, dotToNested pollution,
  and "app.loadInstalledPlugins" in app-shell: all inside the legacy
  client-side config cluster whose entry points are shadowed by
  plugins_manager.js / replaced by server-rendered forms (zero live
  callers, verified) - queued for wholesale deletion in the follow-up
  rather than patching dead code.
- custom-feeds-helpers.js findings (3): file was deleted in a prior commit.
- console.error/warn override removal and afterSwap script re-execution
  removal: deliberate pre-existing workarounds every partial's inline
  init currently depends on; reworking them safely needs isolated testing
  (follow-up), and the error suppression is already double-gated
  (insertBefore AND htmx match).
- "move durations bootstrap into a bundle": inline partial-scoped init is
  the established pattern for HTMX partials in this codebase.

Validation: all 40 web tests pass; py_compile on all touched Python; all
touched templates parse; rotation-order defensive paths unit-tested.

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 14:17:44 -04:00
co-authored by Claude Sonnet 5
parent aec1368d63
commit 8044084280
13 changed files with 148 additions and 51 deletions
+24 -13
View File
@@ -317,7 +317,7 @@ window.togglePlugin = window.togglePlugin || function(pluginId, enabled) {
showNotification(`${action.charAt(0).toUpperCase() + action.slice(1)} ${pluginName}...`, 'info');
}
fetch('/api/v3/plugins/toggle', {
return fetch('/api/v3/plugins/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plugin_id: pluginId, enabled: enabled })
@@ -361,6 +361,9 @@ window.togglePlugin = window.togglePlugin || function(pluginId, enabled) {
if (wrapperDiv) {
wrapperDiv.classList.remove('opacity-50', 'pointer-events-none');
}
// Resolve the outcome so callers (e.g. the install flow) can chain
// on whether enabling actually succeeded.
return data;
})
.catch(error => {
// Verify this error is for the latest request (prevent race conditions)
@@ -3889,19 +3892,27 @@ window.installPlugin = function(pluginId, branch = null) {
.then(data => {
showNotification(data.message, data.status);
if (data.status === 'success') {
// Enable immediately so install -> enable is one step, then nudge
// for the display restart that's needed before it shows on the
// matrix (persistent toast; duration 0 = stays until dismissed).
window.togglePlugin(pluginId, true);
showNotification(
`${pluginId} installed and enabled — restart the display to show it`,
{
type: 'success',
duration: 0,
actionLabel: 'Restart Now',
onAction: () => restartDisplay()
// Enable immediately so install -> enable is one step; only nudge
// for a restart once enablement actually succeeded (persistent
// toast; duration 0 = stays until dismissed).
Promise.resolve(window.togglePlugin(pluginId, true)).then(toggleResult => {
if (toggleResult && toggleResult.status === 'success') {
showNotification(
`${pluginId} installed and enabled — restart the display to show it`,
{
type: 'success',
duration: 0,
actionLabel: 'Restart Now',
onAction: () => restartDisplay()
}
);
} else {
showNotification(
`${pluginId} installed, but enabling it failed — use its toggle in the plugin list`,
'warning'
);
}
);
});
// Refresh installed plugins list, then re-render store to update badges
loadInstalledPlugins();
setTimeout(() => applyStoreFiltersAndSort(true), 500);