Repository navigation
Audit 2026-11-12: UI and accessibility fixes - #13
Merged
Merged
Conversation
script-src drops 'unsafe-inline': inline <script> blocks carry
nonce="{{ csp_nonce() }}", the two static scripts injected by
inject_mobile_app_head() are allowed by sha256 hashes computed from
mobile_app_head_tags() (never from the response body), and
script-src-attr 'none' blocks inline event handlers.
Allow the hosts the UI really talks to: BTTV/FFZ/7TV/ivr.fi for chat
emotes (connect-src, img-src) and cdnjs for the clip editor slider.
VOD_CSP_ENFORCE now defaults to true; false keeps Report-Only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and banner output Inline on*= attributes and javascript: URLs replaced by addEventListener and data-action delegation, inline scripts carry the CSP nonce (storage, files, live, hls, streamer, clip editor, twitch watch pages, mobile player, language switcher and legal pages). Security (audit S8): chat fallbacks in mobile_player and twitch_watch_chat escape message text and user names when chat_emotes.js is missing; the streamer banner/hero URL is only emitted into CSS url() for same-origin paths or https:// URLs without quotes, parentheses, backslashes, angle brackets or whitespace. Also: clip editor strings go through gettext, decorative SVG icons are aria-hidden, icon-only buttons and unlabeled streamer fields get names, and stylesheets load via the components/styles.html partial. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…chain style.css pulled tokens/reduced-motion/a11y/mobile.css via @import and tube_design_system.css re-imported tokens.css, so the browser discovered them only after style.css (465 KB) had downloaded. All 19 templates now include components/styles.html, which links the same files in the same cascade order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kens base.html: skip link to #main-content, visually hidden label and role=search for the global search, the change-password modal is a <dialog> opened with showModal() (focus moves in, Escape closes, focus returns to the opener) with for/id label pairs, a hidden username field for password managers and a role=alert status box. Inline scripts carry the CSP nonce; the font stylesheet switches media without onload=. tokens.css: muted text #8A97B0 (>= 4.71:1 on every surface, was 3.62:1 on active), accent-as-text #8494FF, single focus ring colour, field edge #66789C (>= 3:1), danger button background #D42A40 (white 4.99:1), live text #F2667A, solid primary instead of the blue-violet gradient (white 4.62:1, gradient end was 3.79:1), defines the missing --ns-warm tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e admin actions New static/sv_confirm.js: forms or submit buttons with data-confirm open a native <dialog> (focus moves in, Escape cancels, focus returns) with an action-labelled confirm button instead of window.confirm(). admin.html: "Restart services" and "Enable maintenance mode" ask first and explain the consequence; restart uses the secondary style, enable maintenance a danger outline. Share revoke and backup delete use the same dialog. Tabs bind via data attributes (no onclick), keep roving tabindex, Home/End keys, aria-labelledby on panels and visually hidden h2s (no h1->h3 jump). Status/error boxes are live regions, the allowlist field has a label, and the content is a <main> landmark with skip link. The 1024px breakpoint that let the 68px rail overlap content (769-1024px) now matches the rail breakpoint (768px). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bels The six modals in admin_users.html are native <dialog>s opened with showModal(): focus moves to the first field, Escape and backdrop click close, focus returns to the button that opened them. 12 label/for pairs, group headings no longer use <label> without a control, close buttons have names, row icon buttons name their user/group, tabs expose tab/tabpanel roles with arrow keys. All inline handlers replaced by data attributes + listeners; toasts use textContent instead of innerHTML (audit S8) and role=status/alert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…yles Below 768px the rail is hidden (and removed by the mobile parity script), which left desktop-UA users without navigation or logout. sidebar.html now renders a bottom bar (Home, Search, Options, Log out) using the existing .mobile-global-nav styles; the rail badge is no longer a div with onclick and the language switcher text is 12px. a11y.css is linked last: a single 2px outline focus ring in --nebula-focus-ring for every focusable element including the player controls, field edges >= 3:1, 20px checkboxes/radios with 24px spacing, 24px minimum height for channel links, skip link, dialog/button/alert components. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pages - index: first two VOD thumbnails eager, the first with fetchpriority=high (was loading=lazy on the LCP image); h1 > h2 > h3 heading order; named storage progressbar; labelled Twitch channel field; empty archive state with the next step; "Live now"/"Watch live" in sentence case. - channels: labelled inputs, fieldset/legend for the operating mode, emoji aria-hidden, 12px badges and 28px remove buttons, removal asks via the confirm dialog, empty state links to the add form. - vod: share link copies with navigator.clipboard and announces "Copied" (no execCommand), translated chat filter labels and a named clear button, player error overlay role=alert, transcript progressbar named, clear delete/rename confirmations, nonces instead of handlers. - mobile_index/mobile_streamer: zoom no longer locked (WCAG 1.4.4), bottom bar is a <nav> with Log out, page h1/h2/h3, named icon links, 44px back target, labelled search/sort, the broken deleteItem() call replaced by a confirmed POST form. - login/admin/search/archive filters: alert role, labels, nonces. tests: ClipEditorShareXssTests read data-clip-name instead of the removed onclick (CSP blocks inline handlers) and additionally assert that the page has no inline handler at all; new ContentSecurityPolicyTests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a tokens Restyle (layout kept, Nebula brand kept, blue-violet gradient dropped): - tokens.css is the single token layer: the 11 top-level :root blocks in style.css / tube_design_system.css / mobile.css are gone; their 66 still-referenced custom properties live in tokens.css as legacy aliases with the value that won the old cascade, 19 unused ones are dropped. The second tokens.css link is no longer needed. - static/css/ui-polish.css deleted (not linked by any template). - 1320 selectors whose classes/ids occur in no template, script or Python file removed (1030 whole rules); 873 declarations removed that a later rule with the same selector and media context overrides. - Lime, yellow and zinc literals of the three foreign palettes and the Nebula literals map to --nebula-* tokens (94 in CSS, 117 in template inline styles); var(--token, literal) fallbacks for defined tokens dropped (155). - Type and radius on scales: 252 px font sizes onto --sv-text-*, 240 radii onto --nebula-radius-*; 83 sizes below 12px raised to --sv-min-text (12px), 60 inline template sizes as well. - transition: all replaced by explicit properties (16), outline:none removed (33) so the a11y.css focus ring shows everywhere. Metrics (static CSS, same script before/after): bytes 603161 -> 399691, !important 4861 -> 3508, colour literal occurrences 1967 -> 1021 (distinct 767 -> 493), top-level :root blocks 16 -> 3 (media-scoped), font-size values 39 -> 31, radius values 48 -> 34. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…for CSP tts_overlay and alerts_overlay read the token from their own URL (OBS browser source) but no longer forward it as ?token=: API calls send X-TTS-Token / X-Alert-Token, and alert/TTS audio is fetched with the header and played from a blob URL (an <audio src> cannot send headers), revoked after playback. Prepares for OVERLAY_ALLOW_QUERY_TOKEN=false (backend PR #12). The inline scripts move to static/*_overlay.js because the enforced CSP blocks inline scripts on these static pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- body.theme-silo/theme-vault (the default theme class) redefined the palette with lime/zinc literals and #7a7a7a muted text (4.05:1); its variables now point at the Nebula tokens (one palette). - Danger buttons use --nebula-status-live-text (>= 4.5:1), the active users tab uses --nebula-accent-text. - Bottom nav is a div role=navigation (legacy page CSS forces every <nav> to display:flex), dialog/confirm button variants and the bottom-nav logout keep their style against legacy !important rules. - Brand glyph: visible SV plus sr-only name instead of a mismatching aria-label; storage settings fields labelled; streamer thumbnail links named; hidden h2 on storage and traffic pages (heading order). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…talogs rebuilt - 18 all-caps msgids (LIVE, WATCH LIVE, OPERATING MODE:, OPTION A: ...) become sentence case; capitals belong in CSS, screen readers spell capitals out. Existing translations were moved to the new ids. - Template msgids end in a real ellipsis; the 32 renamed ids keep their translations, every msgstr in all 11 catalogs uses the ellipsis. - German: 33 new strings translated, the four du-forms switched to the majority Sie form (0 untranslated). - Catalogs re-extracted with the repository's format (--no-location --sort-output --no-wrap) via uv run --no-project --with babel and recompiled. The other 10 languages show the 33 new strings in English until translated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ict nonce test - css_url (Jinja filter, WEBUI.webui.css_url) replaces the inline guard in streamer.html: same-origin paths or https URLs accepted by is_allowed_url(), nothing that could close url() (quotes, parentheses, backslash, angle brackets, whitespace). - img-src allows https: so admin-entered banner/avatar URLs on arbitrary hosts load; images cannot run script. - ContentSecurityPolicyTests now require the response nonce on every executable inline script on six pages (only the hashed scripts from inject_mobile_app_head() are exempt) instead of checking only scripts that already had a nonce attribute; new img-src test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…AGES - streamer.html (stop/empty/delete folder, banner/avatar reset) and storage.html (provider/object deletions) drop their own confirm() handlers (data-streamer-confirm, data-storage-confirm) and use the shared data-confirm dialog with action-labelled buttons. The streamer page also uses the new css_url filter for banner and hero images. - sv_confirm.js takes "Please confirm", "Confirm" and "Cancel" from svT(); the strings are registered in JS_MESSAGES instead of <meta> tags with English fallbacks; admin.html and mobile_streamer.html now include components/js_i18n.html. Storage settings and provider fields get label/for pairs (Lighthouse a11y 95 -> 100). - tests/test_webui_frontend.py: css_url cases and filter registration, chat-fallback escaping (escapeChatText run in node when available), data-confirm rendering, no parallel confirm attributes or inline handlers in any template, dialog strings in JS_MESSAGES. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
34 strings from the audit fixes (confirmation texts, empty states, labels) translated for es, fr, it, ja, ko, nl, pl, pt_BR, tr and zh_Hans in each catalog's existing register, one new German string; 0 untranslated in every catalog. Catalogs refreshed with --no-location --sort-output --no-wrap and compiled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…empty catch The blob-URL audio loader existed twice (tts_overlay.js, alerts_overlay.js); it now lives in static/overlay_audio.js (SVOverlayAudio.create). Empty catch blocks and silent play().catch() log with console.error and context (item id, step). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dashboard_live.js renders cards with --nebula-* colours and --sv-text-* sizes (h3 like the server-rendered cards); new tokens --nebula-scrim and --nebula-shadow-brand replace the last literals in the card badge and the sidebar glyph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hero avatar is no longer lazy; on the mobile dashboard the first three VOD thumbnails load eagerly (first with fetchpriority=high), the rest stays lazy. Mobile dashboard LCP (375px, Slow 4G, CPU 4x, iPhone UA): 4858/4850 ms -> 4112/4094 ms, resource load delay 3270 -> 99 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict only in de/LC_MESSAGES/messages.mo: catalogs re-extracted and all 11 recompiled (pybabel compile); the new backend string "Your IP has been blocked..." and the overlay/logout-everywhere strings of the next commits are translated in every catalog. The app is now built by create_app(); csp_nonce and css_url register via app.add_app_template_global / add_app_template_filter on the core blueprint instead of app.jinja_env. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OBS browser sources cannot send headers and the overlay pages are now served without a token (PR #12). tts_overlay.js and alerts_overlay.js read the token from #token= (never sent to the server) or, for existing URLs, from ?token=, and pass it only as X-TTS-Token / X-Alert-Token. Options > Settings shows the recommended browser-source URLs with #token=TOKEN. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e pages POST /logout/all (logout_everywhere, CSRF token like every other POST form) behind the confirmation dialog: rail icon button on desktop, an "Everywhere" item in the bottom navigation below 768px and in both mobile templates. tests: overlay scripts read #token= first and only send headers; the logout-everywhere forms render with CSRF and data-confirm (desktop 2, mobile 1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…PR screenshots Legacy body:is(.page-*) button rules with !important overrode the danger and primary buttons inside the confirmation dialog; the variants now use a higher-specificity selector (danger = #D42A40, primary = #4D63FF verified in the browser). docs/screenshots/audit-2026-11-12: desktop dashboard and confirmation dialog at 1280px, phone dashboard (mobile template) and bottom navigation at 375px, taken from the local demo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to the fragment static/overlay_common.js (replaces overlay_audio.js) holds the shared logic: parseToken/readToken prefer #token=, move a legacy ?token= into the fragment with history.replaceState and warn once that query tokens reach server logs; createPoller stops on 401 and backs off exponentially (up to 30 s) on errors instead of polling every 0.9-1.2 s; createAudio is the former blob-URL loader. Without a token the overlays show "Token fehlt" and never call the API; a rejected token shows "Token ungültig – Abfrage angehalten". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
files.html: the delete button carries data-confirm and calls svConfirm() instead of the native confirm(); vod.html drops its window.confirm() fallback. No template calls confirm() any more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of literals; PWA banner above the bottom nav - The legacy page-wide button selectors (49 in style.css and tube_design_system.css) now read button:where(:not(.sv-btn)); :where() keeps their specificity. The out-specifying html body .sv-btn.sv-btn... block in a11y.css is gone, the dialog buttons use plain .sv-btn--* rules. Browser check: primary #4D63FF, danger #D42A40, white text, restart button transparent with field edge #66789C. - New tokens --nebula-text-on-accent, --nebula-accent-primary-hover, --nebula-danger-bg-hover, --sv-bottom-nav-h; a11y.css has no hex literal left (41 var() fallbacks dropped). - Below 768px the injected PWA install banner sits above the bottom navigation (375px: banner bottom 740px, nav top 751px). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both modals rendered the provider fields with id="cfg_<key>"; ids are now prefixed with the modal container, labels point at them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
components/logout_everywhere.html (variants rail and nav) replaces the four copies in the sidebar, the bottom navigation and both mobile templates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Too many login attempts... %(num)d minute(s)" translated with each catalog's plural forms (pl 3, ja/ko/tr/zh_Hans 1, others 2); scripts/check_translations.py: 1106/1106 in all 11 catalogs, 0 problems. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d OBS panel - tests/webui_test_helpers.admin_test_client replaces three copies of the admin session setup. - node-backed tests fail in CI when node is missing instead of being skipped (CI=true on GitHub runners; node is present there). - Overlay: parseToken (fragment first, query token moved, params kept) and createPoller (backoff 2/4/8/8/8/8 s capped, reset after success, stop and one callback on 401) run in node; scripts return before polling without a token. - Logout everywhere: rendered on the desktop dashboard (2), the mobile dashboard (1) and the mobile streamer page (1) with CSRF and data-confirm; templates use the include. - OBS panel URLs end in #token=TOKEN and contain no ?token=. - No template calls confirm(); storage field ids are per modal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blackwellcode
added a commit
that referenced
this pull request
Nov 12, 2026
…elity Conflicts: - WEBUI/static/style.css: master reformatted the neighbouring chat rules; the Twitch chat block (--sv-* tokens) is kept unchanged. The tokens it uses (--tube-text, --tube-text-2) still exist on master. - Catalogs (.po/.pot/.mo, 11 locales): merged per message. Master's catalogs are the base (new UI strings from PR #13); entries missing there come from this branch (Twitch chat notices). Then extract, update --ignore-obsolete (master keeps no obsolete entries) and compile. check_translations: 1132/1132 in every locale, 0 problems. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
WebUI part of the 2026-11-12 audit (01 runtime UI, 03 app design, 04 S2/S8), the approved restyle, the code-review fixes and the follow-ups to #12. Layout and wiring are kept; one CSP, one token layer, one confirmation dialog.
Also:
<dialog>modals with focus in/out and Escape, labels on every flagged field, skip link, heading order, bottom navigation with Log out / Log out everywhere below 768 px, zoom unlocked on the mobile templates, AA contrast tokens, 12 px minimum text, eager LCP images, sentence-case msgids, Sie-form German, 0 untranslated strings in all 11 catalogs, CHANGELOG and.env.example(VOD_CSP_ENFORCE=true).Evidence
Screenshots (local demo): desktop dashboard · desktop confirm dialog · phone dashboard · phone bottom navigation
!important/ colour literal occurrences?token=only / with headerPOST /logout/allwithout / with CSRF; second session afterwardsui_baseline.py compare(dembrandt, baseline a3a1372 → after review fixes): login 0, index 0, channels 0 (2 changes), search 0 (2 changes), admin 1 (4 changes) → overall exit 1. The admin drift is intended: "Restart services" is now really the secondary style (the legacybody:is(.page-*) buttonrule had kept it blue), white button text →#f3f5fc, field edge#66789cfrom the new OBS URL fields.Tests:
python -m unittest discover -s tests -p "test_*.py"→Ran 323 tests … OK (skipped=20);uv run --no-project --with ruff==0.17.3 ruff check .→All checks passed!. New:tests/test_webui_frontend.py(css_url, chat-fallback escaping incl. node run, data-confirm rendering, no inline handlers or parallel confirm attributes, JS_MESSAGES, overlay token transport, logout-everywhere forms),ContentSecurityPolicyTests(every executable inline script carries the response nonce on six pages; only the hashed scripts frominject_mobile_app_head()are exempt;img-src https:).Changed assertion,
ClipEditorShareXssTests(CSP blocks inline handlers, so the Share button has noonclickany more):Same five payloads, same exact round-trip requirement; additionally the page must contain no inline handler at all.
Merge Danger
Door: two-way
Revert restores the old templates and CSS;
VOD_CSP_ENFORCE=falseswitches the policy back to Report-Only without a revert.Blast Radius: UI-wide
onclick=stop working under the enforced CSP until they usenonce="{{ csp_nonce() }}"/ listeners.?token=URLs keep working client-side; the server only sees the header.!importantreduction need cascade work; the backend PWA install banner overlaps the bottom navigation on phones.🤖 Generated with Claude Code