fix(tour, csp): media-src für Tour-Audio + Tour global außer Administration

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.
This commit is contained in:
Dotty Dotter 2026-05-09 08:43:35 +02:00
parent 57434485ea
commit 4c989ea443
5 changed files with 45 additions and 10 deletions

View File

@ -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

View File

@ -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 ───────────────────────────── #}
<script>
(function () {
@ -330,14 +340,14 @@
</script>
<script>
/* Tour-Link in der Topbar nur einblenden, wenn die aktuelle Page eine
Tour definiert hat (window.GWOE_TOUR_STEPS). Synchron — das Skript
steht nach dem body_scripts-Block, dort werden die STEPS gesetzt.
addEventListener('DOMContentLoaded') hatte einen Race weil
DOMContentLoaded an manchen Stellen schon gefeuert hat. */
/* Tour-Link in der Topbar einblenden, wenn die Tour-Engine geladen ist —
das ist auf allen Pages außer Administration der Fall (siehe
{% include "v3/components/tour.html" %} im body unten). Pages ohne
eigene window.GWOE_TOUR_STEPS bekommen eine Fallback-Tour mit
Topbar + Sidebar + Logo-Erklärung. */
(function () {
var link = document.getElementById('v2-topbar-tour');
if (link && Array.isArray(window.GWOE_TOUR_STEPS) && window.GWOE_TOUR_STEPS.length > 0) {
if (link && typeof window.gwoeTourStart === 'function') {
link.hidden = false;
}
})();

View File

@ -111,7 +111,6 @@ window.GWOE_TOUR_STEPS = [
{% endif %}
];
</script>
{% include "v3/components/tour.html" %}
{% endblock %}

View File

@ -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;

View File

@ -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.' },
];
</script>
{% include "v3/components/tour.html" %}
{# Tour-Engine wird global in v2/base.html eingebunden #}
</div>{# .v3-page #}
{% endif %}{# antrag #}