Files
survey-bundle/AI_HANDOVER.md
T
Jürgen Mummert db7094fd1e Fix frontend CSRF, clean reader URLs, and review findings
CSRF / actions:
- List action forms now send both REQUEST_TOKEN (Contao gate) and _token
  (Symfony, validated via isCsrfTokenValid) — mirrors the editor; fixes
  the 400/500 "Ungültiger CSRF-Token" on publish/toggle/duplicate/delete

Public reader / URLs:
- Reader & results read auto_item via Input::get (marks the route param used)
  so clean path URLs /<page>/<alias> no longer 404
- List controller generates path URLs via ['parameters' => '/'.$alias]
- Allow re-participation in the same session after a 60s cooldown

Review fixes:
- Results: merge answer snapshots by questionId+type (no more split cards on
  label edits); seed "Vielleicht" when allowMaybe so screen matches Excel
- Reader: replace generic answer hint with per-option "weiter zu Frage X"
  only when a jump is configured; strip HTML tags from survey description
- PDF: render free-text frequency summary; drop duplicate "Minimum" label
- Drag-sort: bundle SortableJS locally, drop the duplicated inline sort CSS/JS
  from _survey_assets, throttle the backend MutationObserver
- Backend: move hardcoded tl_survey operation labels to TL_LANG (de+en)
- Remove dead code: SurveyEditorVoter, SurveyPermissionService,
  categoryFilter/categorySummary + helpers, orphaned publishSurvey lang key

Reader layout:
- max-width 1000px (other views keep 1400px); back/next buttons fill the
  width as equal, card-sized halves; sort handle icon offset tweak

Docs: update AI_HANDOVER (dual-token CSRF, clean URLs) and README

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:00:05 +02:00

18 KiB
Raw Permalink Blame History

Survey Bundle AI Handover

Diese Datei beschreibt den aktuell gewünschten und umgesetzten Stand des Bundles. Ziel ist, dass ein KI-Modell oder ein neuer Bearbeiter die Logik, die Zustände und die wichtigsten Einstiegspunkte schnell versteht und darauf weiterarbeiten kann.

Zweck des Bundles

Das Bundle stellt ein Contao-Umfragesystem bereit mit:

  • Frontend-Liste für Redakteure
  • Frontend-Frageneditor
  • Öffentlicher Umfrageansicht über Alias-Link
  • Ergebnisseite im Frontend
  • Excel-Export der Ergebnisse
  • PDF-Export der Ergebnisse über Gotenberg
  • Backend-Verwaltung in Contao-DCA-Listen und Editmasken

Zentrale Zustandsfelder

Die Umfrage arbeitet mit drei relevanten Zuständen in tl_survey:

  • published

    • Fachlich: Die Umfrage wurde veröffentlicht.
    • Frontend: Ab Veröffentlichung darf der Redakteur im Frontend weder Fragen noch Metadaten bearbeiten.
    • Backend: Reversibel erlaubt. Der Admin darf den Status im Backend ändern.
  • isActive

    • Fachlich: Öffentliche Sichtbarkeit an/aus.
    • Gilt unabhängig vom Veröffentlichungsstatus als technischer Sichtbarkeitszustand.
    • Der öffentliche Reader liefert nur Umfragen aus, die published = 1 und isActive = 1 haben.
  • isLocked

    • Fachlich: Struktur wurde bereits benutzt oder veröffentlicht.
    • Wird automatisch gesetzt, wenn die erste Antwort gespeichert wird.
    • Wird beim Veröffentlichen ebenfalls gesetzt.
    • Im Backend gibt es bewusst keine harte Sperre mehr. Das Feld dient vor allem der internen Logik und Historie.

Gewollte Produktentscheidung

  • Frontend ist streng:

    • Keine Fragenbearbeitung nach Veröffentlichung
    • Keine Metadatenbearbeitung nach Veröffentlichung
    • Keine Fragenbearbeitung nach ersten Teilnahmen
  • Die Frontend-Sperre für veröffentlichte Umfragen soll bewusst nicht tief im System verankert sein.

    • Für Redakteure genügt das Ausblenden bzw. Deaktivieren der relevanten Buttons und Formulare im Frontend.
    • Es ist ausdrücklich keine zusätzliche harte Systembarriere gewünscht, solange die Redakteursoberfläche den Bearbeitungspfad sauber schließt.
  • Backend ist bewusst toleranter:

    • Ein einzelner Admin darf im Notfall weiter arbeiten
    • Statt harter Sperre gibt es nur einen Warnhinweis in der Liste bei veröffentlichten Umfragen
    • published ist im Backend editierbar
    • isActive ist im Backend per Contao-Toggle schaltbar
  • Bewusster Trade-off zwischen Frontend und Backend:

    • Ein veröffentlichter Survey soll für Frontend-Redakteure praktisch nicht mehr bearbeitbar sein.
    • Im Backend darf der Admin trotz Veröffentlichung weiterhin Änderungen vornehmen, zum Beispiel für Rechtschreibfehler oder kleine Korrekturen.
    • Dadurch bleibt bewusst das Risiko bestehen, dass der Admin auch Fragen oder Strukturen ändert, obwohl bereits Antworten vorliegen.
    • Mögliche Folgen sind verwässerte oder fachlich falsche Ergebnisseiten sowie Inkonsistenzen, wenn bestehende Antworten nicht mehr sauber zur später geänderten Struktur passen.
    • Dieses Risiko wird bewusst akzeptiert und organisatorisch über Briefing/Disziplin des Admins bzw. der zuständigen Redakteure abgefangen, nicht über eine harte technische Sperre im Backend.

Diese Entscheidung ist absichtlich so getroffen und kein Bug.

Datenmodell und wichtige Tabellen

  • tl_survey
    • Umfrage-Stammdaten und Statusfelder
    • Enthält zusätzlich die backend-only gepflegte internalNote
  • tl_survey_content
    • Fragen der Umfrage
  • tl_survey_condition
    • Generische Sprungregeln
  • tl_survey_editor
    • Zugewiesene Frontend-Redakteure
  • tl_survey_submission
    • Umfrage-Durchläufe
  • tl_survey_answer
    • Gespeicherte Antworten inklusive Snapshot-Feldern zur Ergebnisstabilität

Wichtige Snapshot-Felder in tl_survey_answer:

  • questionType
  • questionLabel
  • questionDescription
  • questionSorting

Diese Felder sorgen dafür, dass Ergebnisse auch dann stabil bleiben, wenn Fragen später im Backend verändert oder gelöscht wurden.

Frontend-Module und Controller

1. Redakteursliste

Controller:

  • src/Controller/FrontendModule/MemberSurveyListController.php

Template:

  • contao/templates/frontend/member_survey_list.html.twig

Zweck:

  • Zeigt alle dem eingeloggten Frontend-Mitglied zugewiesenen Umfragen
  • Sortierung ist bewusst fest auf Titel ASC
  • Hier liegen die Redakteursaktionen zur Veröffentlichung und Aktivierung

Aktuelle Button- und Sichtbarkeitslogik pro Umfrage:

  • Bearbeiten
    • Nur klickbar, solange published = 0
    • Nach Veröffentlichung disabled
  • Öffentliche Ansicht
    • Nur sichtbar, wenn published = 1
  • Entwurfs-Ansicht
    • Nur sichtbar, wenn published = 0
  • Ergebnisse anzeigen
    • Immer verfügbar, sofern Results-Seite konfiguriert ist
  • Umfrage veröffentlichen
    • Eigene Aktion in der Liste
    • Nach Veröffentlichung disabled und textlich als veröffentlicht markiert
  • Duplizieren
    • Legt eine Kopie der Umfrage für den Redakteur an
    • Titelvergabe ist fortlaufend: Titel - Kopie, danach Titel - Kopie 2, Titel - Kopie 3 usw.
    • Bestehende - Kopie-Suffixe werden fachlich normalisiert, damit keine Titel wie ... - Kopie - Kopie mehr neu entstehen
  • Kreis grau/grün
    • Schaltet isActive
    • Ist bewusst kein Auge mehr

Wichtiger technischer Punkt (CSRF):

  • Jedes Frontend-Aktionsformular (Liste und Editor) sendet bewusst zwei Tokens:
    • REQUEST_TOKEN ({{ contao.request_token }}) nötig für Contaos globalen Frontend-POST-Gate; fehlt er, lehnt Contao den POST schon vor dem Controller mit HTTP 400 ab.
    • _token ({{ csrf_token('<aktion>-' ~ id) }}) wird im Controller mit isCsrfTokenValid('<aktion>-'.$id, $token) geprüft (session-basierter Symfony-Token).
  • Beide sind erforderlich. Frühere Stände hatten nur einen davon das führte zu „Ungültiger CSRF-Token" (500) bzw. Bad Request (400). Der Listen-Controller spiegelt jetzt exakt die Token-Mechanik des Editors.
  • Der Contao-REQUEST_TOKEN allein reicht nicht als Controller-Prüfung: Er ist cookie-gebunden und bei anonymen/fragmentierten Frontend-Requests nicht stabil (Contao setzt das CSRF-Cookie dort teils gar nicht). Deshalb die zusätzliche session-basierte _token-Prüfung.

2. Frontend-Editor

Controller:

  • src/Controller/FrontendModule/MemberSurveyEditController.php

Template:

  • contao/templates/frontend/member_survey_edit.html.twig

Zweck:

  • Anlegen neuer Umfragen
  • Bearbeiten der Metadaten vor Veröffentlichung
  • Bearbeiten der Fragenstruktur

Aktueller Aufbau:

  • Bei neuer Umfrage:
    • Titel und Beschreibung
  • Bei bestehender, nicht veröffentlichter Umfrage:
    • Metadatenformular oberhalb des Frageneditors
    • Frage-für-Frage-Editor im Akkordeon
    • Neue Frage anlegen
    • Sprungregelübersicht rechts/unten je nach Breakpoint
  • Bei veröffentlichter Umfrage oder bei isLocked = 1:
    • Fragen im Frontend nicht mehr änderbar
    • Metadaten im Frontend ebenfalls nicht mehr bearbeitbar

Wichtige Entscheidung:

  • Die Header-Badges im Fragen-Akkordeon wurden bewusst entfernt.

    • Typ, Pflicht usw. sollen nicht schon im Header gezeigt werden.
    • Die Details sieht man erst nach dem Aufklappen.
  • Die Sprungziel-Dropdowns im Frontend zeigen Positionsnummern der Umfrage (1. ..., 2. ...) statt globaler Datenbank-IDs.

    • Die aktuell bearbeitete Frage wird dort nicht als eigenes Sprungziel angeboten.

3. Öffentliche Umfrageansicht

Controller:

  • src/Controller/FrontendModule/ShowSurveyController.php

Services:

  • src/Service/SurveyFlowService.php
  • src/Service/SurveySubmissionService.php

Zweck:

  • Öffentliche Teilnahme an Umfragen
  • Zusätzlich Entwurfsansicht per ?preview=1

Wichtige Regeln:

  • Öffentliche Ansicht findet eine Umfrage nur, wenn:
    • published = 1
    • isActive = 1
  • Entwurfsansicht arbeitet ohne Speicherung
  • Öffentliche Ansicht speichert Antworten und erstellt/führt Submission fort
  • Rückwärtsnavigation ist implementiert
  • Nach Abschluss zeigt der gleiche Browser-Token für eine kurze Cooldown-Zeit (RESTART_COOLDOWN_SECONDS = 60) die Danke-Seite; danach darf dieselbe Session erneut teilnehmen (SurveySubmissionService::isRestartAllowed).

Saubere Link-URLs (Reader und Ergebnisseite):

  • Die Umfrage wird über den Alias als Pfad-Segment adressiert: /<reader-seite>/<alias> statt ?auto_item=<alias>.
  • URL-Erzeugung: per Route-Platzhalter ['parameters' => '/'.$alias] (Frontend-Liste MemberSurveyListController, Backend SurveyDcaListener::buildSurveyPageUrl), nicht ['auto_item' => $alias].
  • Auslesen im Controller: $this->getContaoAdapter(Input::class)->get('auto_item'). Wichtig: Input::get markiert den Route-Parameter als benutzt sonst wirft Contaos LegacyRouteParametersListener eine 404 wegen „unbenutzter Parameter". Ein reines $request->query->get('auto_item') reicht für Pfad-URLs nicht.

4. Ergebnisseite

Controller:

  • src/Controller/FrontendModule/ShowSurveyResultsController.php

Service:

  • src/Service/SurveyResultsViewService.php

Template:

  • contao/templates/frontend/show_survey_results.html.twig

Zweck:

  • Aggregierte Auswertung pro Frage
  • Anzeige von Anzahl Teilnahmen, Rücklauf und Frageergebnissen

Aktuelle Ergebnislogik:

  • yes_no_maybe
    • Optionsstatistik plus Kreisdiagramm
  • choice
    • Optionsstatistik plus Kreisdiagramm
  • range
    • Minimum, Maximum, Durchschnitt plus Verteilung und Linienchart
  • text
    • Kein Pie-Chart
    • Stattdessen Response-Summary und Liste der Freitextantworten

Wichtig:

  • Die Ergebnisse werden aus den Antwort-Snapshots aufgebaut, nicht nur aus den aktuellen Fragen.
  • Dadurch bleiben historische Ergebnisse stabil.

Sprunglogik

Relevante Services/Repos:

  • src/Service/SurveyFlowService.php
  • src/Repository/SurveyConditionRepository.php
  • src/Controller/FrontendModule/MemberSurveyEditController.php

Reihenfolge der Navigation:

  1. Direkter Ja/Nein-Sprung über jumpOnYes / jumpOnNo
  2. Generische Bedingung aus tl_survey_condition
  3. Nächste Frage nach Sortierung

Wichtiger Detailpunkt:

  • In der Sprungregelübersicht im Frontend-Editor werden bewusst die Positionsnummern innerhalb der Umfrage angezeigt, nicht die globalen Datenbank-IDs der Fragen.
  • Gleiches gilt für die Sprungziel-Dropdowns in Backend und Frontend.

Wichtige technische Eigenschaft:

  • jumpOnYes, jumpOnNo und generische Bedingungen referenzieren Ziel-Fragen intern immer über die Frage-ID, nicht über die Position.
  • Dadurch bleiben Sprünge bei einer reinen Umordnung der Fragen fachlich stabil an dieselbe Frage gebunden.
  • Wenn eine Ziel-Frage an eine andere Position verschoben wird, zeigt die Oberfläche danach automatisch die neue Positionsnummer an.

Excel-Export

Controller:

  • src/Controller/SurveyResultsExportController.php

Service:

  • src/Service/SurveyResultsExportService.php

Bibliothek:

  • phpoffice/phpspreadsheet

Zweck:

  • Export aller abgeschlossenen Teilnahmen in eine XLSX-Datei

Aktuelle Spaltenlogik:

  • Ja/Nein/Vielleicht:
    • pro Antwortoption eigene Spalte
  • Choice:
    • pro Antwortoption eigene Spalte
  • Range:
    • eine Wertespalte
  • Text:
    • eine Textspalte

Wichtige Eigenschaft:

  • Die Spalten orientieren sich an den aktuell vorhandenen Fragen im Survey.
  • Die Zeilen enthalten nur abgeschlossene Submissions.

PDF-Export

Controller:

  • src/Controller/SurveyResultsExportController.php

Service:

  • src/Service/SurveyResultsPdfService.php

Template:

  • src/Resources/views/pdf/survey_results.html.twig

Externe Abhängigkeit:

  • Gotenberg über surveyGotenbergUrl in den Contao-Einstellungen

Zweck:

  • Serverseitiger PDF-Export der Ergebnisse

Aktuelle Logik:

  • HTML wird serverseitig mit Twig gerendert
  • Diagramme werden als SVG/Data-URI vorbereitet
  • Gotenberg rendert daraus das PDF
  • Ist keine Gotenberg-URL gesetzt, wird im Frontend kein PDF-Download angeboten

Backend-Aufbau

tl_survey

Dateien:

  • contao/dca/tl_survey.php
  • contao/languages/de/tl_survey.php
  • contao/languages/en/tl_survey.php

Wichtige Listenoperationen:

  • Fragen bearbeiten
  • Umfrage bearbeiten
  • isActive als Contao-Toggle
  • Warnsymbol bei veröffentlichten Umfragen
  • Link kopieren
  • Ergebnisse anzeigen

Aktuelle Listen- und Filterlogik:

  • Die mehrspaltige Umfragenliste zeigt aktuell:
    • Titel
    • Zugewiesene Benutzer
    • Vorlage
    • Interne Notiz
  • Kategorien wurden bewusst aus der mehrspaltigen Liste und aus der rechten Filterspalte entfernt.
  • Die Spalte Zugewiesene Benutzer wird in der Listenmetadatenlogik gekürzt:
    • maximal 3 Namen direkt sichtbar
    • danach Zusatz wie ... und X weitere
  • internalNote
    • backend-only Feld in tl_survey
    • nur im Backend bearbeitbar
    • in der mehrspaltigen Liste sichtbar
    • bei Duplikaten wird der Wert mitkopiert
  • In der rechten Suchspalte ist Interne Notiz suchbar.
  • ID wurde für die Umfragenliste bewusst aus dem Suchfeld entfernt.
    • Das geschieht über einen kleinen backend-spezifischen JS-Workaround, weil Contao id im Core für DC_Table hart ergänzt.

Aktuelle Backend-Entscheidung:

  • Das Warnsymbol genügt als Hinweis bei veröffentlichten Umfragen
  • Es gibt bewusst keine harte Sperre für Fragen im Backend

Child-Tabellen

  • tl_survey_content
  • tl_survey_condition
  • tl_survey_editor

Hinweis:

  • isLocked wird in den Child-Headerfeldern nicht mehr angezeigt, weil der Backend-Lock bewusst keine führende Benutzerfunktion mehr ist.
  • In tl_survey_content ist der Fragenlisten-Header zusätzlich reduziert:
    • nur noch Titel und Veröffentlicht
    • kein öffentlicher Link-Schlüssel mehr
    • die rechte Header-Leiste mit Aktionen ist im Normalzustand ausgeblendet
    • im aktiven Verschiebe-/Clipboard-Modus wird sie wieder eingeblendet, damit Neu oben bzw. die Einfügepositionen erreichbar bleiben

Fragenliste tl_survey_content:

  • Direkt sichtbare Primäraktion pro Frage:
    • Bearbeiten-Stift
  • Verschieben:
    • erfolgt im Backend über den normalen Contao-Paste-/Move-Flow
    • die Aktion Verschieben liegt bewusst nur unter den drei Punkten
    • dort wird der Contao-Pfeil mover.svg verwendet, nicht die blaue Schere
    • nach Aktivierung des Clipboard-Zustands erscheinen die üblichen Einfügeziele wie Nach dem Element ... einfügen

Systemeinstellungen

Datei:

  • contao/dca/tl_settings.php

Wichtige Settings:

  • surveyReaderPage
  • surveyResultsPage
  • surveyGotenbergUrl

Aktuelle Produktregeln in Kurzform

  • Frontend-Liste:
    • Titel alphabetisch
    • Veröffentlichung und Aktivierung in der Liste
  • Frontend-Editor:
    • Metadaten nur vor Veröffentlichung
    • Fragenbearbeitung nur solange nicht veröffentlicht und keine Teilnahmen vorliegen
  • Öffentliche Ansicht:
    • Nur published = 1 und isActive = 1
  • Backend:
    • published reversibel erlaubt
    • isActive per Standard-Contao-Toggle
    • nur Warnsymbol statt Backend-Sperre
    • Admin darf Fragen auch nach Veröffentlichung oder nach vorhandenen Antworten umsortieren

Wichtige Dateien nach Themengebiet

Frontend-Liste:

  • src/Controller/FrontendModule/MemberSurveyListController.php
  • contao/templates/frontend/member_survey_list.html.twig

Frontend-Editor:

  • src/Controller/FrontendModule/MemberSurveyEditController.php
  • contao/templates/frontend/member_survey_edit.html.twig

Öffentliche Umfrage:

  • src/Controller/FrontendModule/ShowSurveyController.php
  • src/Service/SurveyFlowService.php
  • src/Service/SurveySubmissionService.php

Ergebnisse:

  • src/Controller/FrontendModule/ShowSurveyResultsController.php
  • src/Service/SurveyResultsViewService.php
  • contao/templates/frontend/show_survey_results.html.twig

Exporte:

  • src/Controller/SurveyResultsExportController.php
  • src/Service/SurveyResultsExportService.php
  • src/Service/SurveyResultsPdfService.php

Backend:

  • contao/dca/tl_survey.php
  • src/EventListener/SurveyDcaListener.php

Technische Validierung, die in diesem Projekt gut funktioniert

  • php -l auf die geänderten PHP-Dateien
  • ddev exec php bin/console lint:yaml ...
  • ddev exec php bin/console cache:clear

Offene technische Punkte / Deployment-Hinweise

  • Neue DB-Spalte in tl_survey:

    • internalNote
    • Typ aktuell: varchar(255) NOT NULL default ''
    • Für andere Umgebungen muss diese Spalte per Migration/SQL vorhanden sein, sonst fehlen Backend-Feld, Listenanzeige und Suchfunktion fachlich bzw. führen zu Inkonsistenzen.
  • Nach Änderungen an DCA, Backend-CSS oder Backend-JS ist in dieser Installation typischerweise nötig:

    • php vendor/bin/contao-console assets:install public --symlink --relative
    • php vendor/bin/contao-console cache:clear --env=prod
  • Es gibt aktuell zwei bewusst eingebaute backend-spezifische UI-Workarounds über Bundle-Assets:

    • In der Umfragenliste wird ID aus dem Suchfeld per JS entfernt, weil Contao das Feld im Core von DC_Table hart ergänzt.
    • In der Fragenliste (table=tl_survey_content) wird die rechte Parent-Header-Leiste per CSS/JS im Normalzustand ausgeblendet und nur im aktiven Clipboard-/Verschiebe-Modus wieder eingeblendet.
  • Lokale DDEV-Besonderheit, die bei Fehleranalyse relevant sein kann:

    • Das Bundle läuft hier als Path-Repository/Symlink.
    • Wenn Backend-Übersetzungen oder Assets trotz korrektem Code unerwartet fehlen, zuerst Bundle-Pfad, veröffentlichte Assets und Prod-Cache prüfen.

Bekannte bewusste Trade-offs

  • Backend kann veröffentlichte Umfragen technisch weiterhin verändern.
    • Das ist bewusst akzeptiert, weil nur ein Admin arbeitet.
  • isLocked bleibt als internes Logikfeld bestehen, obwohl der Backend-Lock nicht mehr durchgesetzt wird.
  • PDF-Export hängt von Gotenberg ab und ist absichtlich optional.
  • Eine reine Umordnung von Fragen ist für Sprungziele logisch unkritisch, weil Sprünge an Frage-IDs hängen.
  • Ergebnisse vorhandener, weiterhin existierender Fragen folgen bei der Anzeige jedoch der aktuellen Sortierung der Frage.
    • Historische Antwort-Snapshots speichern zwar questionSorting, die Ergebnisansicht sortiert vorhandene Fragen aber primär nach der aktuellen Frage-Sortierung.
    • Das ist im toleranten Backend-Modell ein bewusst akzeptierter Side-Effect.

Empfohlene Einstiegspunkte für künftige Änderungen

  • Status-/Sichtbarkeitslogik: SurveyRepository, SurveyEditorService, MemberSurveyListController
  • Fragerouting: SurveyFlowService, SurveyConditionRepository
  • Ergebnisdarstellung: SurveyResultsViewService
  • Exportlogik: SurveyResultsExportService, SurveyResultsPdfService
  • Backend-Operationen und DCA-Verhalten: tl_survey.php, SurveyDcaListener