Skip to content

Audit 2026-11-12: UI and accessibility fixes - #13

Merged
Blackwellcode merged 31 commits into
masterfrom
audit/ui-fixes
Nov 12, 2026
Merged

Blackwellcode merged 31 commits into
masterfrom
audit/ui-fixes

Conversation

@Blackwellcode

@Blackwellcode Blackwellcode commented Nov 12, 2026 •

Copy link
Copy Markdown
Owner

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.

 request
   render template
+    csp_nonce()            -> nonce="..." on every inline <script>
+    |css_url               -> banner/hero url() only for /path or https:// (is_allowed_url)
   set_security_headers
-    Content-Security-Policy-Report-Only: script-src 'unsafe-inline' ...
+    Content-Security-Policy: script-src 'self' 'nonce-…' 'sha256-…' cdn; script-src-attr 'none'
+                             img-src https: ; connect-src … BTTV FFZ 7TV ivr.fi
 WEBUI/static/
 ├── css/
 │   ├── tokens.css          # the only token layer (was: 16 :root blocks in 3 files)
 │   ├── a11y.css            # linked last: focus ring, dialogs, targets, buttons
-│   └── ui-polish.css       # dead, never linked
 ├── style.css               # dead selectors + shadowed declarations removed
+├── sv_confirm.js           # data-confirm -> native <dialog>, strings via svT()
+├── overlay_audio.js        # header-authenticated audio as blob URL (TTS + alerts)
 ├── tts_overlay.js          # token from #token= (or ?token=), sent as X-TTS-Token
 └── alerts_overlay.js       # token from #token= (or ?token=), sent as X-Alert-Token
 inline onclick= / onsubmit= / confirm()   (≈180 handlers in 30 templates)
-  removed
+  addEventListener / data-* delegation
+  data-confirm (restart, maintenance, deletes, revoke, rename, logout everywhere)

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

Measure (local demo, same scripts before/after) Before (c5a142c) After
CSP violations, strict policy, 9 pages 2–11 per page (Report-Only) 0, enforced
Lighthouse a11y desktop: dash/channels/search/admin/users/vod/streamer/storage/traffic 92/100/100/95/97/97/89/88/95 100 on all 9
LCP mobile dashboard, desktop UA (375 px, Slow 4G, CPU 4×) 6639 ms 4046 / 4049 ms
LCP mobile dashboard, iPhone UA 4858 / 4850 ms (at a3a1372) 4112 / 4094 ms, load delay 3270 → 99 ms
CSS bytes / !important / colour literal occurrences 603161 / 4861 / 1967 401477 / 3532 / 1022
Overlay API without token / with ?token= only / with header – 401 / 401 / 200 (TTS + alerts)
POST /logout/all without / with CSRF; second session afterwards – 403 / 302 → /login; 302 → /login

ui_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 legacy body:is(.page-*) button rule had kept it blue), white button text → #f3f5fc, field edge #66789c from 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 from inject_mobile_app_head() are exempt; img-src https:).

Changed assertion, ClipEditorShareXssTests (CSP blocks inline handlers, so the Share button has no onclick any more):

-    def _share_onclick(self, clip_name):
+    def _share_clip_name(self, clip_name):
 ...
-                value = dict(attrs).get("onclick") or ""
-                if value.startswith("shareClip("):
-                    handlers.append(value)
+                attr_map = dict(attrs)
+                inline_handlers.extend(name for name in attr_map if name.startswith("on"))
+                if attr_map.get("id") == "btnShareClip":
+                    share_values.append(attr_map.get("data-clip-name"))
 ...
-        self.assertEqual(len(handlers), 1)
+        self.assertEqual(inline_handlers, [])
+        self.assertEqual(len(share_values), 1)
 ...
-                self.assertEqual(json.loads(match.group(1)), payload)
+                self.assertEqual(self._share_clip_name(payload), payload)

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=false switches the policy back to Report-Only without a revert.

Blast Radius: UI-wide

  • Custom or forked templates with inline scripts or onclick= stop working under the enforced CSP until they use nonce="{{ csp_nonce() }}" / listeners.
  • Visual restyle across all pages (palette, type and radius scales, removed dead CSS); checked at 375/768/1280/1920 on dashboard, VOD, admin, channels only.
  • OBS overlays: existing ?token= URLs keep working client-side; the server only sees the header.
  • Open: z-index scale and further !important reduction need cascade work; the backend PWA install banner overlaps the bottom navigation on phones.

🤖 Generated with Claude Code

Blackwellcode and others added 30 commits November 12, 2026 08:51
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
Blackwellcode merged commit 016090b into master Nov 12, 2026
4 checks passed
@Blackwellcode
Blackwellcode deleted the audit/ui-fixes branch November 12, 2026 14:48
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant