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
Preview-while-configuring:
- Floating mini preview (fixed, bottom-right) available on every tab except
Overview, fed from the same SSE display stream by updateDisplayPreview -
no new connections. Collapses to a round toggle button; open/closed state
persists in localStorage; hides on Overview where the full preview lives.
- "Preview on display" button on every plugin config page header: runs that
plugin on the real display for 60 seconds via the existing
/display/on-demand/start API and opens the floating preview, closing the
configure -> see-the-result loop.
Drawer/nav accessibility:
- aria-current="page" tracks the active tab (system + dynamic plugin tabs,
matched via their Alpine @click expression), updated from the activeTab
watcher so search deep-links and checklist navigation are covered too.
- Escape closes the mobile drawer and returns focus to the hamburger;
opening the drawer moves focus to its first tab.
Validation: all 40 web tests pass; Jinja parse + div balance on both
touched templates.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
- Add debugLog to the /* global */ headers of the six JS files that call it
(defined in base.html's first inline script) — resolves the wall of
"'debugLog' is not defined" ESLint errors failing the Codacy check.
- Fix the two js/double-escaping CodeQL alerts in app-shell.js: the
entity-unescape chains decoded & before </>, so a value
containing a pre-escaped "&lt;" wrongly double-decoded to "<".
& now decodes last (standard order). Pre-existing bug, made visible
when the inline scripts moved into scannable .js files.
- Page title / header drop the "- v3" suffix, matching the de-versioned
user-facing URL.
The remaining 7 CodeQL alerts are pre-existing patterns newly visible to
scanning (CodeQL doesn't see inline template JS): 4 github.com/htmx.org
URL-substring checks (the htmx ones match error-message text, not URLs —
false positives in context) and 1 innerHTML XSS-through-DOM in the GitHub
install flow. Triage/fix deferred to a focused follow-up rather than
expanding this PR.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
The initial SSE render sized the preview image (and both overlay canvases)
from the server-reported config dimensions (cols x chain_length,
rows x parallel), while the scale slider's re-render path sized from
img.naturalWidth/naturalHeight. Whenever the snapshot PNG's actual size
disagrees with the config (stale config, display service not restarted
after a hardware change), the initial render stretched the image at a
fractional ratio - blurry despite image-rendering: pixelated - and
touching the scale slider "fixed" it. Reported live on the devpi test rig.
Both paths now size from the loaded image's natural dimensions inside
img.onload (which also removes a transient wrong-size flash between
src assignment and load). The meta label now reports the true snapshot
size. The preview card also gets overflow-x-auto so on narrow screens a
wide preview scrolls at its exact pixel-perfect size instead of being
squeezed into the viewport (fractional downscaling of pixel art also
reads as blur).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
base.html shipped ~4,200 lines of inline JavaScript inside the HTML
document, re-downloaded and re-parsed on every page load (gzip helps the
transfer, but inline scripts can never be browser-cached). The four
largest blocks - none containing any Jinja syntax, verified by scanning
every inline block for {{ }} / {% %} - now live as static files served
with the app's existing mtime-versioned immutable caching:
- js/htmx-config.js (246 lines): HTMX swap/script-execution config,
toggleSection helpers
- js/app-early.js (346 lines): early helpers + the app() stub that must
precede Alpine init
- js/app-shell.js (2,997 lines): SSE wiring + the full Alpine app()
implementation and tab logic
- js/custom-feeds-helpers.js (262 lines): custom-feeds table helpers
Each replacement <script src> is CLASSIC (no defer/async) at the exact
position of the inline block it replaces - identical execution timing and
DOM visibility to inline scripts, so relative ordering with the deferred
scripts and with each other is unchanged. base.html drops from ~4,940 to
1,079 lines.
Validation: extraction proven lossless by programmatically reassembling
the four files back into the template and comparing against git HEAD -
byte-for-byte identical. Jinja parse passes; script open/close tags
balanced (53/53, after excluding a literal "<script>" inside an HTML
comment).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ