The floating preview opened empty and stayed empty until the display next
CHANGED - the SSE stream only pushes frames on change, and the panel only
consumed frames while already open, so the connection's initial frame
(sent while the panel was closed) was dropped. Reported from mobile
testing as "the button doesn't work".
- updateDisplayPreview now caches the latest frame globally regardless of
panel state; opening the panel populates the image from that cache
instantly, then live frames take over.
- Resizable: a size button cycles 192/256/384/512px presets (persisted per
browser; works on touch), and desktop additionally gets a native drag
handle (CSS resize: both). The image is fluid within the panel; on
phones the panel is capped to the viewport width. The size icon
(fa-up-right-and-down-left-from-center) is verified present in the
vendored FA 6.0.0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
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