From 4c989ea443abb3b0b0169939074307fe96cc85bf Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Sat, 9 May 2026 08:43:35 +0200 Subject: [PATCH] =?UTF-8?q?fix(tour,=20csp):=20media-src=20f=C3=BCr=20Tour?= =?UTF-8?q?-Audio=20+=20Tour=20global=20au=C3=9Fer=20Administration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei Bugs: 1) Audio kam nicht durch — die Content-Security-Policy hatte kein media-src und fiel auf default-src 'self' zurück. data:- (silent-WAV zum Element-Unlock) und blob:-URLs (ElevenLabs-MP3-Cache) wurden geblockt. Browser-Fehlermeldung im Console: „Loading media from ‚data:audio/wav;base64,…' violates the following Content Security Policy directive". Fix: ``media-src 'self' data: blob:;`` ergänzt. 2) Tour war nur auf Startseite + Antrag-Detail eingebunden. User-Wunsch: auf jeder Page außer Administration. Lösung: Tour-Engine-Include in v2/base.html, mit ``{% if v2_active_nav not in [admin_*] %}``-Guard. Pages ohne eigene ``window.GWOE_TOUR_STEPS`` bekommen einen Fallback mit drei Stationen (Logo+Konzept, Topbar, Sidebar). Topbar-Tour-Link sichtbar wenn ``window.gwoeTourStart`` existiert (Engine geladen) — nicht mehr abhängig von Page-eigenen Steps. Aufräumen: redundante Tour-Includes aus durchsuchen.html und antrag_detail.html entfernt — die Engine kommt jetzt nur einmal aus base.html. --- app/main.py | 9 ++++++++- app/templates/v2/base.html | 22 +++++++++++++++------ app/templates/v2/screens/durchsuchen.html | 1 - app/templates/v3/components/tour.html | 21 +++++++++++++++++++- app/templates/v3/screens/antrag_detail.html | 2 +- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/app/main.py b/app/main.py index 3ceeac3..dbc2ce6 100644 --- a/app/main.py +++ b/app/main.py @@ -118,12 +118,19 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): response.headers["X-XSS-Protection"] = "1; mode=block" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()" - # CSP: Allow self, inline styles (for templates), and PDF viewer + # CSP: Allow self, inline styles (for templates), and PDF viewer. + # media-src: 'self' für die Eigen-Hosting-MP3s (Tour-Audio aus + # ElevenLabs-Cache via /api/tour/voice), data: für das 1×1 + # silent-WAV zum Audio-Element-Unlock im Click-Handler, blob: + # für die im Browser zwischengespeicherten ElevenLabs-Blobs + # (Object URLs via URL.createObjectURL). Ohne media-src + # blockt der Browser jeden Audio-play-Versuch (#185 Phase 2). response.headers["Content-Security-Policy"] = ( "default-src 'self'; " "style-src 'self' 'unsafe-inline'; " "script-src 'self' 'unsafe-inline'; " "img-src 'self' data:; " + "media-src 'self' data: blob:; " "frame-ancestors 'none';" ) return response diff --git a/app/templates/v2/base.html b/app/templates/v2/base.html index 1294b12..7ff9e51 100644 --- a/app/templates/v2/base.html +++ b/app/templates/v2/base.html @@ -226,6 +226,16 @@ {% block body_scripts %}{% endblock %} +{# ── Tour-Engine — auf jeder Seite eingehängt (#185). + Pages, die eigene Stationen wollen, setzen ``window.GWOE_TOUR_STEPS`` + im body_scripts-Block oben. Pages ohne Stationen lassen den Topbar- + Tour-Link automatisch ausgeblendet. Administration ist explizit + ausgenommen. #} +{% if v2_active_nav not in ['admin_stand', 'admin_queue', 'admin_abos', + 'freischaltungen', 'queue', 'abos'] %} +{% include "v3/components/tour.html" ignore missing %} +{% endif %} + {# ── Globaler BL-Selector — Persistenz + Event ───────────────────────────── #} -{% include "v3/components/tour.html" %} {% endblock %} diff --git a/app/templates/v3/components/tour.html b/app/templates/v3/components/tour.html index 4181d8a..1090b40 100644 --- a/app/templates/v3/components/tour.html +++ b/app/templates/v3/components/tour.html @@ -141,8 +141,27 @@ // Jeder Eintrag: {selector, title, text}. Selectoren werden zur Laufzeit // aufgelöst — fehlt das Element (z.B. keine Plenum-Votes auf einer // bestimmten Drucksache), überspringt die Tour den Schritt automatisch. + // + // Fallback: pages ohne eigene Stationen bekommen eine kurze + // Orientierungs-Tour, damit die Tour-Schaltfläche überall einen + // sinnvollen Inhalt zeigt. + const FALLBACK_STEPS = [ + { selector: '.v2-brand-link, .v2-brand', + title: 'Willkommen beim GWÖ-Antragsprüfer', + text: 'Diese Seite bewertet Anträge aus deutschen Parlamenten nach der Gemeinwohl-Matrix. Was sie auszeichnet: Sie schaut nicht nur, ob ein Antrag „gut klingt", sondern wie sehr er fünf Werten dient — Würde, Solidarität, Nachhaltigkeit, Gerechtigkeit und Demokratie. Klick auf das Logo bringt dich jederzeit zur Übersicht zurück.' }, + { selector: '.v2-topbar', + title: 'Topbar', + text: 'Oben findest du Methodik (wie wir bewerten), Quellen (alle Wahl- und Grundsatzprogramme, semantisch durchsuchbar) und den Bundesland-Filter. Rechts ist der Theme-Toggle und — falls eingeloggt — dein Profil.' }, + { selector: '#v2-sidebar nav', + title: 'Navigation links', + text: 'Links findest du die Hauptbereiche. Die Liste passt sich an: angemeldete Nutzer:innen sehen außerdem Auswertungen, Stimmverhalten und persönliche Funktionen wie eine Merkliste.' }, + ]; + function getSteps() { - return Array.isArray(window.GWOE_TOUR_STEPS) ? window.GWOE_TOUR_STEPS : []; + if (Array.isArray(window.GWOE_TOUR_STEPS) && window.GWOE_TOUR_STEPS.length > 0) { + return window.GWOE_TOUR_STEPS; + } + return FALLBACK_STEPS; } let _tourIdx = 0; diff --git a/app/templates/v3/screens/antrag_detail.html b/app/templates/v3/screens/antrag_detail.html index 6d77e80..467ed50 100644 --- a/app/templates/v3/screens/antrag_detail.html +++ b/app/templates/v3/screens/antrag_detail.html @@ -565,7 +565,7 @@ text: 'Hier sehen Sie, wie die Fraktionen tatsächlich abgestimmt haben. Das Warnschild neben einer Fraktion bedeutet Heuchelei: Sie stimmt mit Nein, obwohl der Antrag exakt zu ihrem eigenen Wahlprogramm passt. Das Ausrufezeichen markiert Opportunismus: Ja-Stimme bei einem Antrag, der dem eigenen Programm widerspricht.' }, ]; - {% include "v3/components/tour.html" %} + {# Tour-Engine wird global in v2/base.html eingebunden #} {# .v3-page #} {% endif %}{# antrag #}