Hilfe & Anleitung

Diese Seite hat zwei Teile. Oben das Betreiber-Handbuch – es erklärt in einfacher Sprache, wie du die Webseite bedienst und jede Einstellung im Verwaltungsbereich (Backend) nutzt, auch ohne Technik-Vorkenntnisse. Darunter die technische Dokumentation für Entwickler (Architektur, Datenbank, Abläufe). Erreichbar über das ?-Symbol oben im Seitenkopf; das Fragezeichen kann im Backend unter Konfiguration → Hilfe-Button aus- und eingeblendet werden. Die Seite ist nicht für Suchmaschinen bestimmt (noindex,nofollow).

💡 Der schnellste Weg, alles gefahrlos auszuprobieren: Klicke im Backend bei einem Standort auf „Test-Session starten". Damit spielst du die komplette Schatzsuche ohne Bezahlung einmal komplett durch – von der Freischaltung über die Rätsel bis zur Urkunde. So siehst du sofort, was deine Besucher später erleben, ohne dass echtes Geld oder echte E-Mails im Spiel sind.

Backend-Handbuch – einfach erklärt

Dieser Teil richtet sich an dich als Betreiber der Webseite. Er ist bewusst ohne Fachbegriffe geschrieben. Du musst nichts programmieren – alle Inhalte pflegst du bequem über den Verwaltungsbereich im Browser.

Was ist das „Backend"? Das Backend (auch „Verwaltungsbereich" oder „Admin") ist der geschützte Teil der Webseite, in dem du alles einstellst: Standorte, Rätsel, Texte, Preise, Sprachen und mehr. Deine Besucher sehen davon nichts – sie sehen nur das Ergebnis, die „normale" Webseite (das nennt man „Frontend").

Kurz zum Produkt: Schatzilo ist eine Schatzsuche für Kinder. Eltern oder Schulen kaufen einen Zugang, das Kind spielt am Handy eine Tour an einem echten Ort (Station für Station ein kleines Rätsel), und am Ende gibt es eine persönliche Urkunde als PDF.

Goldene Regel: Fast jeder Text, den ein Besucher sieht, lässt sich im Backend ändern. Wenn dir irgendwo ein Wort nicht gefällt, gibt es dafür fast immer ein Feld im Verwaltungsbereich – du musst dafür nie eine Datei anfassen.

1. Anmelden im Backend

So kommst du in den Verwaltungsbereich:

  1. Rufe im Browser die Adresse deiner Webseite auf und hänge /admin an (z. B. deine-webseite.de/admin).
  2. Gib deine E-Mail-Adresse und dein Passwort ein.
  3. Du landest auf der Übersicht. Links siehst du das Menü mit allen Bereichen.
Passwort vergessen oder noch keinen Zugang? Ein neuer Zugang wird einmalig technisch angelegt (bzw. von einem bestehenden Inhaber unter Konfiguration → Administratoren). Wende dich an die Person, die die Seite technisch betreut.
Melde dich an einem fremden/öffentlichen Gerät immer wieder ab (Menüpunkt Abmelden). Dein Zugang darf nicht in falsche Hände geraten.

2. Übersicht (das Dashboard)

Die Startseite des Backends. Sie zeigt dir auf einen Blick:

  • Kennzahlen – z. B. wie viele Buchungen es gab.
  • Übersetzungs-Lücken – falls du mehrere Sprachen anbietest, siehst du hier, wo noch Übersetzungen fehlen.

Von hier aus springst du über das linke Menü in alle anderen Bereiche.

3. Standorte

Ein Standort ist ein Ort, an dem eine Schatzsuche stattfindet (z. B. ein bestimmter Park oder Bauernhof). Jeder Standort hat eine eigene Unterseite auf der Webseite und mindestens ein buchbares Angebot.

Einen Standort anlegen oder ändern

  1. Menü Standorte öffnen.
  2. Oben auf Neu klicken – oder einen bestehenden Standort zum Bearbeiten anklicken.
  3. Die Felder ausfüllen: Name, Überschrift, Beschreibung, Hinweise usw.
  4. Gibt es mehrere Sprachen, findest du oben Reiter (Tabs) pro Sprache – fülle jeden Reiter aus, damit der Standort in jeder Sprache Text hat.
  5. Speichern. Die öffentliche Standortseite ist sofort aktualisiert.

Wichtige Begriffe an einem Standort

Angebot / Tour-Typ
Was genau man buchen kann, mit Preis und Gültigkeitsdauer. Es gibt zwei Arten: Selbst spielen (das Kind bekommt sofort einen Zugang zum Spiel) und betreut (ein persönlicher Termin – hier entsteht kein Spiel, sondern eine Anfrage an dich).
Slug
Der Teil der Internet-Adresse für diesen Standort (z. B. knirpsenfarm in …/standort/knirpsenfarm). Kurz, klein geschrieben, keine Umlaute/Leerzeichen.

Bei jedem Standort findest du außerdem die Knöpfe „Stationen verwalten", „Karte & Positionen" und „Test-Session starten" – dazu gleich mehr.

4. Stationen

Eine Station ist ein Halt auf der Schatzsuche – ein Punkt am Ort, an dem das Kind ein Rätsel löst. Ein Standort hat mehrere Stationen in einer Reihenfolge.

  1. Beim Standort auf Stationen verwalten klicken.
  2. Station anlegen: Titel, Einleitungstext und optional ein Hilfetext.
  3. Die Reihenfolge bestimmt, in welcher Abfolge die Kinder die Stationen durchlaufen. Du kannst sie jederzeit ändern; vertauschte Positionen werden automatisch sauber getauscht.
Zu jeder Station gehört mindestens ein Rätsel (nächster Punkt). Eine Station ohne Rätsel hat für die Kinder keine Aufgabe.

5. Rätsel

Das Rätsel ist die eigentliche Aufgabe an einer Station: eine Frage, eine oder mehrere richtige Antworten und bis zu drei Hinweise.

  1. Bei einer Station auf Rätsel gehen und ein Rätsel anlegen.
  2. Frage eingeben.
  3. Akzeptierte Antworten: schreibe jede erlaubte Antwort in eine eigene Zeile. Alle Zeilen gelten als richtig (praktisch für Schreibweisen wie „Baum"/„der Baum").
  4. Optional Hinweis 1–3 und einen Erfolgstext, der nach richtiger Antwort erscheint.

Zwei nützliche Schalter

Groß-/Kleinschreibung ignorieren
Wenn aktiv, ist „baum" genauso richtig wie „Baum". Für Kinder empfehlenswert.
Leerzeichen ignorieren
Wenn aktiv, stören versehentliche Leerzeichen am Anfang/Ende nicht.
So funktionieren die Hinweise im Spiel: Gibt es mindestens einen gepflegten Hinweis, zeigt das Spiel den Schalter „Gibt es einen Hinweis?". Ein Tipp darauf deckt Hinweis 1 auf; sind weitere gepflegt, erscheint „Noch einen Hinweis anzeigen" für Hinweis 2 und danach Hinweis 3. So bekommt ein Kind, das nicht weiterkommt, genau so viel Hilfe, wie es sich holt – komplexe Fragen kannst du also mit bis zu drei Hinweisen ausstatten.

6. Landkarte (optional, aber schön)

Statt einfacher Seiten „Station für Station" kannst du eine echte Landkarte anzeigen: Das Kind sieht das Gelände als Karte, tippt die leuchtende Station an und die Ansicht gleitet nach jedem gelösten Rätsel weiter zur nächsten Station.

  1. Beim Standort auf Karte & Positionen klicken.
  2. Ein Kartenbild hochladen (Bild oder SVG-Zeichnung).
  3. Die Stationen mit der Maus an die richtige Stelle auf der Karte ziehen und speichern.
Die Karte erscheint im Spiel nur, wenn es eine Karte gibt und alle Stationen darauf platziert sind. Fehlt bei einer Station die Position, zeigt das Spiel automatisch die einfache „Seite-für-Seite"-Variante – es geht also nie etwas kaputt.

7. Startseite bearbeiten (Baukasten)

Die Startseite ist aus Blöcken zusammengesetzt (Überschrift, Text, Bild, Buttons, Kacheln …) – wie ein Baukasten. Du bearbeitest sie direkt auf der Seite.

  1. Als angemeldeter Admin die Startseite öffnen und den Bearbeiten-Modus starten.
  2. Einen Block anklicken – rechts öffnet sich ein Feld zum Ändern des Inhalts.
  3. Blöcke lassen sich hinzufügen, ändern und verschieben. Speichern nicht vergessen.
Du kannst nichts „zerstören": Es werden nur die Inhalte der Blöcke geändert. Wenn dir etwas nicht gefällt, ändere es einfach zurück.

8. UI-Texte (die kleinen Wörter überall)

UI-Texte sind die vielen kleinen, festen Beschriftungen der Webseite: Knopf-Aufschriften wie „Jetzt buchen", Menüpunkte, Hinweismeldungen, Betreffzeilen von E-Mails. Sie stehen nicht bei einem einzelnen Standort, sondern gelten überall.

  1. Menü UI-Texte öffnen.
  2. Über die Suche oder den Kategorie-Filter den gewünschten Text finden.
  3. Text ändern und speichern – er erscheint sofort überall dort, wo er verwendet wird.
Neben vielen Feldern siehst du ein lila Zauberstab-Symbol 🪄. Damit lässt du den Text automatisch in andere Sprachen übersetzen (siehe nächster Abschnitt).

9. Sprachen & Übersetzen

Die Webseite kann mehrsprachig sein. Standardmäßig ist Deutsch eingestellt. Du kannst weitere Sprachen freischalten.

Eine Sprache verwalten

  1. Menü Sprachen öffnen.
  2. Sprache anlegen oder aktivieren. Sobald zwei oder mehr Sprachen aktiv sind, erscheint für Besucher automatisch oben ein Sprach-Umschalter.
  3. Du kannst eine Standard-Sprache festlegen und je Sprache einstellen, wie Preise und Datum geschrieben werden (z. B. „6,49 €" vs. „€6.49").
Es muss immer mindestens eine Sprache aktiv sein, und die Standard-Sprache lässt sich nicht abschalten – so kann die Seite nie „ohne Sprache" dastehen.

Automatisch übersetzen (der Zauberstab 🪄)

Damit du nicht alles von Hand übersetzt, gibt es eine KI-Unterstützung. Es gibt zwei Wege:

A) Automatisch (mit Schlüssel)
Wenn im Backend ein KI-Schlüssel hinterlegt ist (siehe Konfiguration), klickst du einfach auf den 🪄 neben einem Feld – der Übersetzungs-Vorschlag erscheint, du übernimmst ihn. Es gibt auch einen Knopf „Sprache anlegen & alles übersetzen", der eine ganze neue Sprache in einem Rutsch befüllt.
B) Von Hand (ohne Schlüssel, kostenlos)
Kein Schlüssel? Kein Problem. Die Seite erzeugt dir einen fertigen Text zum Kopieren. Den fügst du in einen beliebigen kostenlosen KI-Chat (z. B. ChatGPT, Gemini, Claude) ein, kopierst die Antwort zurück und fügst sie wieder ein. Die Seite prüft die Antwort selbst und warnt, falls etwas fehlt – du kannst also nichts falsch machen.

Übersetzungs-Protokoll

Im Menü Übersetzungs-Protokoll siehst du, welche Texte wann automatisch übersetzt wurden – nützlich, um den Überblick zu behalten.

10. Medien (Bilder, Töne, Videos, PDFs)

Unter Medien verwaltest du alle Dateien: hochladen vom Computer oder als Link von woanders einbinden. Du ordnest jede Datei einem Zweck zu (z. B. Bild eines Standorts).

  1. Menü Medien öffnen, Neu wählen.
  2. Datei hochladen oder Link einfügen, den Zweck zuordnen, optional Bildbeschreibung angeben.
  3. Speichern. Erlaubt sind gängige Bild-, Ton-, Video- und PDF-Formate; sehr große Dateien werden abgelehnt.
Eine Bildbeschreibung (Alt-Text) hilft blinden Nutzern und Suchmaschinen – kurz beschreiben, was auf dem Bild zu sehen ist.

11. Anfragen

Unter Anfragen sammeln sich zwei Dinge: Nachrichten aus dem Kontaktformular und Buchungen betreuter Touren (die keinen automatischen Spielzugang erzeugen, sondern einen persönlichen Termin).

  1. Menü Anfragen öffnen.
  2. Über den Status-Filter z. B. nur „offene" anzeigen.
  3. Eine Anfrage bearbeiten und den Status umstellen (z. B. „erledigt"), damit du den Überblick behältst.

12. Urkunden

Am Ende jeder Schatzsuche bekommt das Kind eine persönliche Urkunde als PDF. Du gestaltest, wie diese Urkunde aussieht.

Vorlage gestalten

  1. Menü Urkunden-Vorlagen öffnen.
  2. Optional eine Hintergrund-PDF hochladen (dein schön gestaltetes Urkunden-Design).
  3. Textfelder platzieren: Wo soll der Name stehen, wo das Datum, wo der Ort? Du legst Position, Größe, Farbe und Ausrichtung fest.
  4. Mit „Test-PDF" kontrollierst du das Ergebnis mit Beispieldaten, bevor es echt wird.
Die Platzhalter {name}, {date} und {location} werden beim Erstellen automatisch durch den echten Namen, das Datum und den Ort ersetzt.

Erstellte Urkunden

Unter Erstellte Urkunden findest du jede tatsächlich ausgestellte Urkunde, kannst nach Name/E-Mail suchen und die PDF erneut herunterladen.

Wie das Kind an die Urkunde kommt

Nach dem Lösen aller Rätsel bekommt es einen kurzen Urkunden-Code und eine E-Mail. Damit ruft es die Seite Urkunde auf, gibt Name und E-Mail ein – und erhält die PDF zugeschickt. Das musst du nicht anstoßen, das läuft von allein.

13. Konfiguration (die zentralen Einstellungen)

Unter Konfiguration stellst du die technischen Grunddinge ein. Hier sind die Bereiche in einfachen Worten:

🪄 Künstliche Intelligenz (KI)
Hier hinterlegst du einen Schlüssel (API-Key), damit der Zauberstab automatisch übersetzen kann. Wichtig: Ein normales Chat-Abo (ChatGPT Plus o. ä.) ist nicht dasselbe wie ein API-Schlüssel. Nur Google Gemini bietet einen kostenlosen Schlüssel (mit Mengen-Grenze). Ohne Schlüssel funktioniert weiterhin die Von-Hand-Übersetzung (siehe Sprachen). Den Schlüssel bekommst du auf der Webseite des jeweiligen Anbieters – die genaue Adresse steht als Hilfetext direkt am Feld.
PayPal (Bezahlung)
Damit Kunden online bezahlen können. Zwei Modi: Sandbox = Testmodus ohne echtes Geld (zum Ausprobieren) und Live = echte Zahlungen. Du trägst die Zugangsdaten aus deinem PayPal-Konto ein. Für echten Verkauf muss auf Live umgestellt und die Live-Zugangsdaten eingetragen werden.
Hilfe-Button
Schaltet das Fragezeichen (?) oben auf der öffentlichen Seite ein oder aus. Ist es aus, sehen normale Besucher diese Anleitung nicht – du erreichst sie trotzdem jederzeit über den Knopf „Anleitung öffnen" in der Konfiguration.
Administratoren
Wer darf ins Backend? Hier siehst du alle Zugänge. Inhaber (owner) dürfen neue Zugänge anlegen und alles verwalten, Redakteure (editor) pflegen Inhalte, Betrachter (viewer) dürfen nur schauen. Vergib Passwörter mit mindestens 10 Zeichen.
Bei allen Schlüsseln/Geheimnissen gilt: Ein leeres Feld beim Speichern lässt den bisherigen Wert unverändert. Aus Sicherheit wird ein gespeicherter Schlüssel nie im Klartext angezeigt – nur „gesetzt" oder „nicht gesetzt".

14. Alles gefahrlos testen (Test-Session)

Bevor die Seite echt online geht, willst du sehen, ob alles klappt. Dafür gibt es die Test-Session – eine Probe-Schatzsuche ohne Bezahlung.

  1. Menü Standorte öffnen, den gewünschten Standort wählen.
  2. Auf „Test-Session starten" klicken.
  3. Du landest direkt im Spiel und kannst die komplette Tour durchspielen – Freischaltung, alle Rätsel, Landkarte und Urkunde.
Eine Test-Session kostet nichts, verschickt keine echten Bestell-Mails und ist 14 Tage gültig. Ideal, um nach jeder Änderung schnell zu prüfen: „Sieht das jetzt richtig aus?"

Eine ausführliche Prüf-Checkliste für jede einzelne Funktion (mit „so testest du"-Schritten) findest du weiter unten unter Funktions-Checkliste.

15. Häufige Fragen

Ich habe etwas geändert, sehe es aber nicht auf der Webseite.
Hast du Speichern geklickt? Lade die öffentliche Seite neu (Strg+F5). Bei Sprachen: Prüfe, ob du im richtigen Sprach-Reiter warst.
Der Zauberstab 🪄 ist grau / macht nichts.
Dann ist kein KI-Schlüssel hinterlegt. Nutze entweder die kostenlose Von-Hand-Übersetzung oder trage unter Konfiguration einen Schlüssel ein.
Eine Testbuchung führt zu keiner E-Mail.
Schau in den Spam-Ordner. Test-Sessions verschicken bewusst keine echten Bestell-Mails – für einen echten E-Mail-Test eine reguläre (Sandbox-)Buchung durchführen.
Kann ich etwas kaputt machen?
Im Alltag kaum: Inhalte lassen sich ändern und zurückändern. Vorsicht ist nur bei Konfiguration (Schlüssel/PayPal) und beim Löschen von Standorten/Stationen geboten – im Zweifel vorher kurz Rücksprache halten.
Wo stelle ich den Preis ein?
Beim jeweiligen Angebot (Tour-Typ) eines Standorts – dort stehen Preis, Währung und Gültigkeitsdauer.

Damit endet das Betreiber-Handbuch. Alles Weitere unten ist die technische Dokumentation für Entwickler – als Betreiber brauchst du sie normalerweise nicht.

1. Überblick

Schatzilo ist eine standortbasierte Schatzsuch-Plattform. Eltern oder Schulen kaufen einen Spiel-Token, Kinder spielen die zugehörige Tour offline-tolerant am Smartphone und erhalten am Ende eine personalisierte Urkunde.

Drei Kern-Anwendungen

Marketing-Site Page-Builder-Onepager (Startseite) + Standort-Landingpages mit Tour-Typ-Auswahl, Buchungsformular, mehrsprachig.
Spiel-Web-App Token-basiert (/play/<token>/…), Mobile-First, Outdoor-Kontrast.
Admin-Backend CRUD für alle Inhalte + UI-Strings (/admin/…).

Architektur-Prinzipien

  • Alle Texte aus der DB. Keine hartkodierten Strings im Code – Buttons, Hero-Texte, Mails, Rätselfragen, alles über ui_strings oder *_i18n-Tabellen.
  • Pflege ausschließlich über das Admin. Kein direktes SQL nötig.
  • Hauptzeile + i18n-Begleiter. Pro Entität gibt es foo (sprachneutrale Stammdaten) und foo_i18n (Texte pro Sprache).

2. Tech-Stack

SchichtTechnologieDatei
SprachePHP 8.1+ (8.1.2-1ubuntu2.25)composer.json
Routing / HTTPSlim 4app/Http/routes.php
DI-ContainerPHP-DI 7app/bootstrap.php
TemplatesTwig 3templates/
DatenbankMariaDB 10.6+ via PDO (no-emulate-prepares)app/Database/Connection.php
i18nEigenes System (DB-getrieben)app/I18n/{LanguageResolver,Translator}.php
Envvlucas/phpdotenvprivate/.env (Mode 0640, Gruppe web-marco)
MailPHPMailer – SMTP (Dev: 1blu) mit mail()-Fallbackapp/Support/Mailer.php
BezahlungPayPal Orders v2 (REST, ohne SDK) + Webhookapp/Payments/PayPalClient.php
Page-BuilderBlock-Baum-System (page_blocks) mit Inline-Editor (Side-Panel)app/Cms/{BlockRenderer,BlockRegistry}.php
PDF (Urkunden)FPDI + TCPDF – Hintergrund-PDF importieren + Text-Overlayapp/Support/CertificateRenderer.php
MedienEigene Verwaltung (media_assets) – Upload oder externer Link, optional pro Spracheapp/Media/MediaRepository.php, app/Admin/MediaController.php
LokalisierungZahl/Währung/Datum pro Sprache (kein ext-intl nötig) – Regeln in languagesapp/I18n/Formatter.php
Landkarte (Spiel)Inline-SVG mit Marker/Weg-Overlay, Pan/Zoom + Weg-Animationtemplates/play/karte.html.twig, public/assets/js/play-map.js
Frontend-CSSHandgeschrieben (kein Build-Schritt): app.css, admin.css, play.css. Tailwind noch nicht aufgesetzt.public/assets/css/
DB-Migrationeneinfacher SQL-Runner (idempotent)bin/migrate.php, database/migrations/
QR-Codesendroid/qr-code (installiert, noch nicht verdrahtet)

3. Verzeichnisstruktur

/var/www/marco/schatzilo/
├── app/                       PHP-Code (PSR-4: Schatzilo\)
│   ├── bootstrap.php           DI-Container, Slim-App-Factory
│   ├── Admin/                  Admin-Controller (Auth, Dashboard, CRUD)
│   ├── Database/Connection.php PDO-Factory
│   ├── Http/
│   │   ├── routes.php          Alle Slim-Routen
│   │   ├── Controllers/        Web-Controller (Home, Location, Play, Checkout, …)
│   │   └── Middleware/         Session, Sprache, Admin-Auth, Play-Session
│   ├── I18n/                   LanguageResolver, Translator, Formatter (Zahl/Datum)
│   ├── Media/                  MediaRepository (Auflösung sprachspezifisch→neutral)
│   ├── Cms/                    BlockRenderer, BlockRegistry (Page-Builder)
│   ├── Payments/               PayPalClient, PaymentService
│   ├── Certificates/           CertificateService (Code, Einlösen, PDF-Versand)
│   └── Support/                Csrf, Flash, AuditLogger, Mailer, RateLimiter,
│                               AccessCodeGenerator, CertificateCodeGenerator, CertificateRenderer
├── bin/
│   ├── composer.phar           lokal (nicht im Repo)
│   └── admin-create.php        CLI: php bin/admin-create.php <email> [pass]
├── database/
│   └── schema.sql              Vollständiges Schema + Seed-Daten
├── docs/                      Konzept-Dokumente (00–10, inkl. Spiel-/i18n-Plan)
├── private/                    nicht im DocumentRoot
│   ├── .env                    Secrets (Mode 0640, group web-marco)
│   ├── .env.example            Vorlage
│   ├── backups/                Future: mysqldump-Tarballs
│   └── uploads/
│       ├── certificates/        erzeugte Urkunden-PDFs (+ /templates/ Hintergrund-PDFs)
│       └── media/               hochgeladene Medien (Auslieferung via /media/{id})
├── public/                     Webserver-DocumentRoot
│   ├── index.php               Front-Controller
│   ├── .htaccess               Apache-Rewrite (zur Vollständigkeit; live: nginx)
│   ├── assets/css/             app.css, admin.css, play.css
│   ├── assets/js/              main.js, edit-mode.js, play-map.js (Landkarte)
│   ├── assets/maps/            Karten-SVGs (z. B. demo-trail.svg)
│   ├── dev/                    map-picker.html (Koordinaten-Werkzeug, Phase-3-Vorstufe)
│   └── (…)                     _tinyfm entfernt → private/legacy/ (2026-07-13)
├── storage/                    Cache + Logs (writable von web-marco)
├── templates/                  Twig
│   ├── layouts/                Web-Layout
│   ├── web/                    Marketing-Seiten
│   ├── play/                   Spiel-App
│   ├── admin/                  Backoffice
│   └── mail/                   Bestätigungs- + Urkunden-Mails
└── vendor/                     Composer-Pakete (über composer install)

4. Datenbank-Architektur

Schema in database/schema.sql. Engine InnoDB, Charset utf8mb4, BIGINT-IDs, durchgehend created_at/updated_at, Soft-Delete via deleted_at wo Audit relevant ist.

Sprachen & UI-Strings

TabelleInhalt
languagesSprach-Codes (de, en …), Eigenbezeichnung, Default- + Aktiv-Flag, Sortierung sowie Format-Regeln (decimal_sep, thousands_sep, currency_before, date_format). Pflege über /admin/sprachen.
ui_stringsStatische UI-Texte: PK (string_key, language_code), optionaler context-Filter (cta, navigation, play, mail, checkout …)

Inhalt: Hauptzeile + i18n-Begleiter

Hauptzeilei18n-BegleiterÜbersetzte Felder
regionsregions_i18nname, description
locationslocations_i18nname, hero_title, hero_text, description, instructions, meta_description
tour_typestour_types_i18nname, description (Self-Service vs. betreut via is_guided)
stationsstations_i18ntitle, intro, hint_intro
riddlesriddles_i18nquestion, accepted_answers (JSON), success_text, hint_1, hint_2, hint_3
page_blockspage_blocks_i18nblock-typ-abhängige Felder als JSON (fields, z. B. {"headline":…,"html":…})
pagespages_i18ntitle, body, meta_description (Body wandert in den Block-Baum)
certificate_templatescertificate_templates_i18ndisplay_name, html, css (Legacy – aktiv ist PDF-Overlay)
media_assetsmedia_assets_i18nalt_text, caption (Datei/Link sprachneutral od. pro Sprache; siehe Medien-Verwaltung)

Geschäftsdaten

Buchbare Angebote: location_tour_types verknüpft Standort × Tour-Typ und trägt Preis, Währung, Dauer und code_validity_minutes. Der Checkout läuft pro (location, tour_type) (?type=-Slug).

users (optional, Gast-Checkout möglich) → bookings (eine pro PayPal-Order, Status-Enum, tour_type_id NOT NULL) → payments (Roh-Webhook-Events) → sessions (eine pro Buchung, Token-basiert, access_code NOT NULL, certificate_code bei Abschluss) → session_progress (eine Zeile pro Rätsel-Versuch). Plus certificates, inquiries, page_blocks (Page-Builder-Baum), media_assets (Medien), location_maps + stations.map_x/map_y/map_path_to_next (Landkarte), admins, audit_log, rate_limits (IP-basiertes Limit für /code, Kontakt & Urkunde, siehe app/Support/RateLimiter.php).

Urkunden-Vorlagen (PDF-Overlay)

Die aktive Urkunde nutzt nicht mehr certificate_templates_i18n (HTML/CSS, Legacy), sondern PDF-Overlay-Spalten direkt in certificate_templates: background_pdf_path (optionale Hintergrund-PDF), orientation/page_format und fields_json – eine Liste positionierter Textfelder (content mit Platzhaltern {name}/{date}/{location}, x/y in mm, Größe, Farbe, Ausrichtung, fett). Erzeugte Dokumente landen in certificates (mit recipient_email, file_path).

Konventionen

  • FK-Auflösung über ON DELETE CASCADE für i18n-Begleiter, sonst ON DELETE RESTRICT.
  • Slugs sind sprachneutral. Sprach-Prefix kommt aus der URL (/de/standort/knirpsenfarm).
  • PDO::ATTR_EMULATE_PREPARES = false → Named Placeholder dürfen nicht mehrfach im selben Statement stehen (:loc_r, :loc_l statt einmal :loc).

5. i18n-System

Alles, was der User sieht, kommt aus der DB. Es gibt zwei Datenquellen, gegen die die Übersetzung läuft:

  • UI-Strings (Buttons, Labels, Mails): ui_strings.string_key per Twig-Filter {{ 'btn.start_hunt'|t }}.
  • Inhaltliche Felder (Standort-Name, Rätselfrage): direktes JOIN in der Query gegen locations_i18n.language_code = :lang.

Sprachen-Verwaltung (Admin)

Unter /admin/sprachen (app/Admin/LanguageController.php): Sprachen anlegen, bearbeiten (Name, Eigenbezeichnung, Sortierung, Format-Regeln), aktivieren/deaktivieren, Standard setzen, löschen – mit Abdeckungs-Anzeige (wie viel je Sprache übersetzt ist) und Schutzregeln (Default nicht deaktivier-/löschbar, immer ≥1 aktive Sprache). Der öffentliche Sprachumschalter erscheint automatisch ab ≥2 aktiven Sprachen, wechselt zur gleichen Seite (Pfad-Präfix-Tausch) und merkt die Wahl per Cookie.

Sprach-Auflösung

Reihenfolge in app/I18n/LanguageResolver.php:

  1. URL-Prefix ({lang}-Routenparameter)
  2. Cookie schatzilo_session_lang
  3. Accept-Language-Header
  4. Default-Sprache aus languages.is_default = 1 (zur Zeit de)

Twig-Filter / Funktionen

AufrufEffekt
{{ 'btn.start_hunt'|t }}UI-String aus aktueller Sprache (Fallback: Default → Key selbst)
{{ 'mail.booking.subject'|t({location: name}) }}Mit Platzhalter-Substitution {location}
{{ price(loc.price_cents, loc.currency) }}Preis sprachabhängig formatiert (de 6,49 €, en €6.49) – Regeln aus languages
{{ format_date(date) }}Datum nach languages.date_format der aktuellen Sprache

Beide lesen die aktuelle Sprache aus dem Render-Kontext (app/I18n/Formatter.php); im Admin (ohne Locale-Kontext) greift die Default-Sprache. ext-intl ist auf dem Server bewusst nicht vorausgesetzt – die Formatregeln liegen pflegbar in languages.

Spielsprache

Die sessions.language_code friert die Spiel-Sprache zur Buchung ein. Das PlaySessionMiddleware liest die Session-Sprache und überschreibt damit den URL-/Cookie-Resolver, damit ein in DE gekauftes Spiel auch DE bleibt, wenn das Kind aus Versehen en/… öffnet.

6. Routing

Definiert in app/Http/routes.php. Slim 4 mit Middleware-Gruppen.

Public

GET  /                            → 302 zur Default-Sprache
GET  /_health                      Health-Check (JSON)
GET  /dev/hilfe                    Diese Seite
GET  /{lang}/                      Startseite (Page-Builder-Onepager)
GET  /{lang}/standort/{slug}       Standort-Detail (Alias: /location/{slug})
GET  /{lang}/checkout/{slug}?type= Buchungs-Formular (pro Tour-Typ)
POST /{lang}/checkout/{slug}       PayPal-Order anlegen
GET  /checkout/return?token=…      PayPal-Return (Capture)
GET  /checkout/cancel              Abbruch-Seite
GET  /{lang}/checkout/danke/{token} Thanks + Spiel-Link
GET  /{lang}/checkout/betreut-danke Danke-Seite betreute Tour
GET/POST /{lang}/code              Freischaltcode einlösen (CSRF + Rate-Limit)
GET/POST /{lang}/code/vergessen    Code per Mail erneut senden (Anti-Enumeration)
GET/POST /{lang}/urkunde           Urkunde einlösen: Code → Name+Mail → PDF
POST /{lang}/urkunde/erstellen     PDF erzeugen + per Mail senden
GET  /{lang}/urkunde/download      zuletzt erzeugte Urkunde herunterladen
GET  /{lang}/so-funktionierts      „So funktioniert's"
GET  /{lang}/kontakt               Kontakt (Anfrageformular)
POST /{lang}/kontakt               Anfrage absenden → inquiries (CSRF, Honeypot, Rate-Limit)
GET  /{lang}/impressum             Impressum
GET  /{lang}/datenschutz           Datenschutz
GET  /media/{id}                   Medien-Auslieferung (Stream aus private/uploads od. 302 auf Link)
POST /api/paypal/webhook           PayPal-Webhook

Spiel

GET  /play/{token}/                Start oder Resume (→ Karte, falls vorhanden, sonst Station)
POST /play/{token}/begin           Markiert Session als active
GET  /play/{token}/karte           Interaktive Landkarte (Fallback: lineare Station)
GET  /play/{token}/station/{n}     Aktuelle Station mit Rätsel
POST /play/{token}/station/{n}/check  Antwort-Prüfung (JSON bei XHR, sonst 302)
GET  /play/{token}/finish          Erfolgsbildschirm; erzeugt Urkunden-Code + Glückwunsch-Mail,
                                   zeigt Code + Link zur Urkunden-Einlösung (/{lang}/urkunde)

Die frühere In-Game-HTML-Urkunde (POST /finish Namenseingabe, GET /play/{token}/urkunde) wurde durch den Code-basierten Einlöse-Flow ersetzt.

Admin

GET/POST /admin/login              Login (CSRF, Argon2id)
GET      /admin/logout
GET      /admin                    Dashboard mit KPIs
GET/POST /admin/standorte/…        CRUD Standorte (i18n-Editor)
GET/POST /admin/standorte/{id}/stationen/…  Sub-CRUD Stationen
GET/POST /admin/standorte/{id}/karte        Karteneditor: Upload + Stationsplatzierung
POST     /admin/standorte/{id}/karte/positionen  Stationskoordinaten speichern (JSON)
GET/POST /…/stationen/{sid}/raetsel/… Sub-CRUD Rätsel
GET/POST /admin/ui-texte/…         UI-Strings-Editor
GET/POST /admin/sprachen…          Sprachen-Verwaltung (aktiv/Standard/Format-Regeln)
GET/POST /admin/medien…            Medien-Verwaltung (Upload/Link, Owner, Sprache)
GET      /admin/anfragen           Kontaktanfragen (Status-Filter)
POST     /admin/anfragen/{id}/status  Bearbeitungsstatus setzen
GET/POST /admin/urkunden…          Urkunden-Vorlagen-CRUD (PDF-Upload, Feld-Editor)
GET      /admin/urkunden/{id}/test    Test-PDF mit Beispieldaten
GET      /admin/urkunden/erstellt     Liste erstellter Urkunden (+ /{id}/datei Download)
POST     /admin/standorte/{id}/test-session  Test-Session für QA
GET/PATCH/DELETE /admin/api/blocks/{id}      Page-Builder-Block-API
POST     /admin/api/pages/{id}/blocks        Block anlegen

Middleware-Stack

MiddlewareZweck
SessionMiddlewaresession_start() mit sicherer Cookie-Konfiguration
LanguageMiddlewareSprache resolven, Twig-Globals setzen, in Translator pushen
AdminAuthMiddleware302→/admin/login wenn keine $_SESSION['admin_id']
PlaySessionMiddlewareSession per Token laden, Sprache aus DB übernehmen

7. Admin-Backend

Login

Tabelle admins, Argon2id-Hash, CSRF-Token aus Session, session_regenerate_id(true) bei Erfolg. Anlage / Reset:

php bin/admin-create.php marco.schauart@gmail.com [neuesPasswort] --role=owner --name="Marco"

Module

  • Dashboard – KPI-Kacheln + Übersetzungs-Lücken-Report.
  • Standorte – CRUD mit Tab-Editor pro aktiver Sprache. Buttons „Stationen verwalten" und „Test-Session starten".
  • Stationen – Sub-CRUD pro Standort, Reihenfolge-Bulk-Update mit Auto-Swap bei Position-Konflikten.
  • Rätsel – Sub-CRUD pro Station, akzeptierte Antworten als „eine pro Zeile" → JSON-Array, bis zu 3 Hinweise.
  • Karteneditor – pro Standort (/admin/standorte/{id}/karte): Landkarte hochladen (viewBox-Erkennung) + Stationen per Drag platzieren (location_maps + stations.map_x/map_y).
  • UI-Texte – Tabellen-Editor mit Kontext-Filter und Suche. Neuanlage über /admin/ui-texte/neu.
  • Sprachen/admin/sprachen: Sprachen anlegen/aktivieren, Standard setzen, Format-Regeln (Zahl/Währung/Datum), Abdeckungs-Anzeige.
  • Medien/admin/medien: Bilder/Töne/Videos/PDF als Upload oder externer Link, zugeordnet zu Standort/Station/Rätsel, Rolle und optional Sprache; Alt-Text/Caption (i18n), Validierung (Whitelist, Größenlimit, Magic-Bytes). Auslieferung öffentlich über /media/{id}.
  • Anfragen – Liste der Kontaktanfragen (inquiries) mit Status-Filter und Inline-Status-Wechsel; verlinkt vom Dashboard. Betreute Touren landen nach Zahlung automatisch hier.
  • Urkunden-Vorlagen – CRUD für certificate_templates: Hintergrund-PDF hochladen, Felder (Text, Position mm, Farbe, Größe, Ausrichtung) im Editor setzen, „Test-PDF" zur Kontrolle. Standard- oder Standort-Vorlage.
  • Erstellte Urkunden – Liste der erzeugten certificates mit Suche (Name/E-Mail) und PDF-Download.
  • Page-Builder – Inline-Editor (Side-Panel, edit-mode.js) für page_blocks-Bäume; die Startseite läuft darüber. REST-API unter /admin/api/blocks, Block-Typen in app/Cms/BlockRegistry.php.

Audit-Log

Tabelle audit_log bekommt für jede mutierende Operation einen Eintrag mit action (create, update, delete, reorder, login.success, login.failed, translate.update, …), entity (Tabellenname), entity_id und JSON-diff.

8. Spielablauf

Aus bookings entsteht beim Capture genau eine sessions-Zeile mit 40-Hex-Char-Token. Lebenszyklus:

StatusBedeutungAktion bei GET /play/{token}/
issuedToken ausgestellt, noch nicht gestartetStart-Bildschirm
activeSpiel läuftRedirect zur aktuellen Station
finishedAlle Rätsel gelöstRedirect zu /finish
expiredÄlter als expires_at (echter Kauf: code_validity_minutes des Tour-Typs; Test-Session: +14 Tage)Fehler-Seite

Antwort-Matching

  1. riddles_i18n.accepted_answers ist ein JSON-Array.
  2. Wenn trim_whitespace = 1 → User-Eingabe und jede Antwort getrimmt.
  3. Wenn case_sensitive = 0 → Vergleich gegen mb_strtolower.
  4. Erste Übereinstimmung gewinnt → session_progress.solved_at wird gesetzt.

Hinweise auf Abruf

Alle gepflegten Hinweise (hint_1hint_3) sind auf Wunsch nacheinander aufdeckbar: Schalter „Gibt es einen Hinweis?" (play.hint.toggle) zeigt Hinweis 1, „Noch einen Hinweis anzeigen" (play.hint.more) den jeweils nächsten. Klassische Ansicht: verschachtelte <details> ohne JS; Karten-Panel: play-map.js (setHints/renderHints). Keine Fehlversuch-Staffelung mehr (bis 2026-07-13).

Landkarte (interaktive Spielführung)

Das Spiel hat zwei Auslieferungs-Modi, die sich dieselbe serverseitige Antwortprüfung teilen:

  • Klassisch-linear – Station für Station als einzelne Seiten (oben beschrieben).
  • Interaktive Landkarte (/play/{token}/karte) – das gesamte Gelände als Karte; das Kind tippt die leuchtende Station an, löst das Rätsel im Overlay-Panel, und der Kartenausschnitt gleitet entlang des Wegs zur nächsten Station.

Die Karte ist die Primäransicht, wenn der Standort eine aktive Karte hat und alle aktiven Stationen Kartenkoordinaten besitzen – sonst greift automatisch der lineare Flow. Ohne JavaScript zeigt ein <noscript>-Block Links zu den Stationsseiten. PlayController::map() baut den Spielzustand, check() liefert bei XHR JSON (richtig/falsch, Hinweise, nächste Station, Finish), sonst weiterhin 302.

DatenträgerInhalt
location_mapsKarte je Standort (SVG/Bild + viewbox_*); optional je Tour-Typ, NULL = Default
stations.map_x, map_yPosition auf der Karte, relativ 0…1 (Marker-Pixel = vb_x + map_x·vb_w)
stations.map_path_to_nextSVG-d-Pfad (viewBox-Einheiten) von dieser zur nächsten Station – Grundlage der Weg-Animation

Frontend: templates/play/karte.html.twig rendert ein Outer-<svg> mit der Karten-viewBox, die Karte als <image> und Marker/Wege darüber; public/assets/js/play-map.js macht Pan/Zoom, Stations-Panel, fetch-Antwort, Weg-Animation und respektiert prefers-reduced-motion. Die JSON-Insel im HTML enthält bewusst keine Lösungswörter.

Pflege im Admin (Karteneditor): Standorte → [Standort] → „Karte & Positionen" (/admin/standorte/{id}/karte, app/Admin/MapController.php): Karte als SVG/PNG/JPG/WEBP hochladen (viewBox wird bei SVG automatisch erkannt, sonst aus den Bildmaßen) → speichert in location_maps (Default-Karte, tour_type_id NULL); danach die Stationen per Drag auf der Karte platzieren (schreibt stations.map_x/map_y via /karte/positionen, JSON). Dateien liegen unter public/assets/maps/. Das frühere Standalone-Werkzeug /dev/map-picker.html bleibt als schneller Koordinaten-Picker bestehen.

Noch offen: Karte je Tour-Typ über den Editor (Schema kann es bereits), Editor für den Animationspfad (map_path_to_next), GPS-„Wo bin ich?", und Stationen mit mehreren Rätseln fallen in der Kartenansicht auf die klassische Stationsseite zurück.

9. PayPal-Flow

Implementiert ohne PayPal-SDK (reines curl in app/Payments/PayPalClient.php). Der Checkout ist tour-typ-bewusst: gebucht wird ein (location, tour_type)-Angebot. Bei betreuten Tour-Typen (is_guided=1) wird keine Spiel-Session erzeugt, sondern ein inquiries-Eintrag + „wir melden uns"-Mail (afterGuidedCapture).

Sync-Flow

  1. User klickt „Mit PayPal bezahlen" auf /{lang}/checkout/{slug}.
  2. PaymentService::startCheckout() legt bookings als pending an, ruft PayPal-Orders-API auf, speichert paypal_order_id.
  3. 302 zur PayPal-Approval-URL.
  4. Käufer approved → PayPal redirected zu /checkout/return?token=<orderId>.
  5. CheckoutController::returnFromPaypal() ruft captureOrder() auf, protokolliert in payments, ruft afterCapture().
  6. PaymentService::afterCapture() ist idempotent: setzt Status paid, legt sessions-Token an, sendet Bestätigungsmail.
  7. 302 zu /{lang}/checkout/danke/<token>.

Webhook

POST /api/paypal/webhook dient als Sicherheitsnetz, falls der Käufer den Browser schließt, bevor der Sync-Return läuft. Verarbeitet CHECKOUT.ORDER.APPROVED|COMPLETED und PAYMENT.CAPTURE.COMPLETED, ruft denselben afterCapture()-Pfad → durch DB-Idempotenz keine doppelten Sessions/Mails. Bei ORDER.APPROVED captured der Webhook serverseitig nach (sonst gäbe es bei „Tab geschlossen vor Rücksprung" den Code ohne Geldfluss).

Wenn PAYPAL_WEBHOOK_ID gesetzt ist, wird die Signatur via /v1/notifications/verify-webhook-signature geprüft.

Konfiguration in private/.env

PAYPAL_MODE=sandbox       # oder live
PAYPAL_CLIENT_ID=…
PAYPAL_CLIENT_SECRET=…
PAYPAL_WEBHOOK_ID=…       # nach Webhook-Anlage in der PayPal-Console

Status aktuell: Credentials gesetzt sandbox

10. Urkunden-Flow

Die Urkunde ersetzt die frühere druckbare HTML-Seite durch einen Code-basierten Einlöse-Flow mit echter PDF-Erzeugung (FPDI + TCPDF). Logik in app/Certificates/CertificateService.php, Rendering in app/Support/CertificateRenderer.php.

1) Bei Abschluss

GET /play/{token}/finish ruft issueOnFinish(): für eine abgeschlossene, gekaufte (nicht Test-)Session wird einmalig ein 5-stelliger sessions.certificate_code erzeugt und eine Glückwunsch-Mail mit Einlöse-Link + Code verschickt (idempotent, keine Doppel-Mail). Die Abschluss-Seite zeigt Code + Button.

2) Einlösen (öffentlich)

  1. GET /{lang}/urkunde?code=… – Code-Eingabe (aus Link vorbefüllt).
  2. POST /{lang}/urkunde – Code → resolveByCode() erkennt automatisch Ort + Abschlussdatum; danach Formular für Name + E-Mail. CSRF + IP-Rate-Limit; bewusst neutrale Antwort gegen Code-Enumeration.
  3. POST /{lang}/urkunde/erstellencreateAndSend() wählt die Vorlage (Standort → Default), rendert die PDF, speichert sie unter private/uploads/certificates/, legt eine certificates-Zeile an und versendet die PDF als Mail-Anhang.
  4. GET /{lang}/urkunde/download – Download der in dieser Session erzeugten PDF.

3) Vorlagen-Gestaltung (Admin)

Unter /admin/urkunden: optionale Hintergrund-PDF hochladen (sonst leeres A4), und in fields_json Textfelder positionieren – jeweils Inhalt mit Platzhaltern {name}/{date}/{location}, Position in mm (X als Anker je nach Ausrichtung L/C/R), Schriftgröße, Farbe, fett. „Test-PDF" rendert die gespeicherte Vorlage mit Beispieldaten.

Hinweis: TCPDF hat bekannte Security-Advisories; relevant nur bei Fremd-Input – hier wird ausschließlich aus admin-kontrollierten Vorlagen + eigenen Texten gerendert.

11. Lokal entwickeln

Dev-Server starten

php -S 127.0.0.1:8765 -t public

Greift auf private/.env zu. Der Built-in-Server reicht für alles außer .htaccess-Tests.

Test-Session ohne PayPal

Im Admin auf einem Standort den Button „Test-Session starten" klicken. Erzeugt eine TEST-…-Buchung (Status paid) und leitet direkt zur Spiel-App.

Composer

php bin/composer.phar install
php bin/composer.phar require <paket>

Datenbank

# Offene Migrationen anwenden (idempotent, bevorzugter Weg)
php bin/migrate.php

# Schema komplett neu einspielen (zerstört Daten!)
mysql -u marco_schatzilo -p marco_schatzilo < database/schema.sql

# Schnell-Check
mysql -u marco_schatzilo -p marco_schatzilo -e "SHOW TABLES"

Twig-Cache

Im Debug-Modus deaktiviert (app.debug = true). In Produktion nach storage/cache/twig/. Bei Template-Änderungen ohne Debug: rm -rf storage/cache/twig/*.

12. Deployment

Webserver

nginx mit PHP-FPM-Pool marco (User+Group web-marco). Config: /etc/nginx/sites-available/schatzilo.schauart.de.

  • DocumentRoot: /var/www/marco/schatzilo/public
  • Front-Controller-Rewrite: try_files $uri $uri/ /index.php?$query_string
  • Geblockt: /_tinyfm/, /includes/, Dot-Files, *.env|*.md|*.sql|*.ini|*.log|*.bak
  • PHP-Ausführung nur für /index.php, andere *.php-Anfragen werden zum Front-Controller umgeleitet.

Datei-Berechtigungen

PfadModusEigentümerGrund
private/2750ki-claude:web-marcoweb-marco darf rein, andere nicht
private/.env0640ditoweb-marco darf lesen, schreiben nur Owner
storage/2775ditoweb-marco schreibt Twig-Cache + Logs
private/uploads/2775ditofür PDF-Urkunden + Asset-Uploads

nginx neu laden

sudo nginx -t && sudo systemctl reload nginx

13. Live-Status

SchalterWert
APP_ENVdevelopment
APP_DEBUGan
PHP8.1.2-1ubuntu2.25
PayPal-Modesandbox
PayPal-Credentialskonfiguriert

14. Funktions-Checkliste – funktioniert alles?

Vollständige Liste aller Funktionen der Plattform. Jeder Block sagt, was die Funktion tut, wo sie liegt und wie du in unter einer Minute prüfst, ob sie funktioniert (mit erwartetem Ergebnis). Reihenfolge = grob der Nutzerreise: erst öffentliche Seite, dann Kauf, Spiel, Urkunde, zuletzt Admin & Betrieb.

Vorbereitung für die Spiel-/Urkunden-Tests: im Admin auf einem Standort „Test-Session starten" klicken – das erzeugt ohne PayPal einen gültigen Spiel-Token, mit dem du den kompletten Ablauf durchspielst. Basis-Adresse Dev: schatzilo.schauart.de.

A. Öffentliche Website (Marketing)

Startseite (Page-Builder-Onepager)

GET /{lang}/ · GET / leitet automatisch zur Default-Sprache
  1. Rufe die Domain-Wurzel / auf.
  2. Prüfe, dass du auf /de/ landest und die Startseite (Hero, Tour-Typ-Karten, Standorte) erscheint.

✓ Erwartet: 302-Weiterleitung auf die Default-Sprache, alle Blöcke werden gerendert, keine unaufgelösten Platzhalter (doppelte geschweifte Klammern) im Text.

Standort-Landingpage

GET /{lang}/standort/{slug} (Alias /location/{slug})
  1. Auf der Startseite einen Standort anklicken.
  2. Prüfe Hero-Titel/-Text, Beschreibung und die Liste der buchbaren Tour-Typen.

✓ Erwartet: Standort-Detailseite mit übersetzten Inhalten und mindestens einem „Buchen"-Button je aktivem Angebot.

Mehrsprachigkeit & Sprachumschalter

app/I18n/LanguageResolver.php, Umschalter erscheint ab ≥2 aktiven Sprachen
  1. Im Admin unter /admin/sprachen eine zweite Sprache (z. B. en) aktivieren.
  2. Auf der öffentlichen Seite den Sprachumschalter oben nutzen.
  3. Prüfe, dass die URL das Präfix tauscht (/de/… → /en/…) und derselbe Seiteninhalt in der anderen Sprache erscheint.

✓ Erwartet: gleiche Seite, andere Sprache; Wahl bleibt per Cookie beim Weiterklicken erhalten. Preise/Datum wechseln das Format.

„So funktioniert's"-Seite

GET /{lang}/so-funktionierts
  1. Seite aufrufen (Link im Footer/Navigation).
  2. Prüfe, dass die Erklärschritte angezeigt werden.

✓ Erwartet: HTTP 200, Schritt-für-Schritt-Erklärung sichtbar.

Kontakt- / Anfrageformular

GET/POST /{lang}/kontakt → Tabelle inquiries, Admin /admin/anfragen
  1. Formular ausfüllen und absenden.
  2. Danach im Admin unter /admin/anfragen nachsehen.

✓ Erwartet: Erfolgsmeldung nach dem Absenden; neuer Eintrag taucht in /admin/anfragen auf. (Schutz: CSRF, Honeypot, Rate-Limit.)

Impressum & Datenschutz

GET /{lang}/impressum, GET /{lang}/datenschutz
  1. Beide Seiten aufrufen.

⚠ Erwartet: Seiten laden (HTTP 200). Vor Go-Live: die Rechtstexte müssen noch inhaltlich befüllt werden (Pflicht!).

Medien-Auslieferung

GET /media/{id}
  1. Ein im Admin hochgeladenes Bild/Medium über seine /media/{id}-URL öffnen.

✓ Erwartet: Datei wird gestreamt (bzw. 302 auf externen Link); sprachspezifische Variante wird bevorzugt, sonst neutrale.

Health-Check

GET /_health
  1. URL im Browser oder per curl aufrufen.

✓ Erwartet: HTTP 200 mit JSON-Antwort (Status ok) – gut für Uptime-Monitoring nach dem Strato-Umzug.

B. Buchung & Bezahlung

Checkout & PayPal-Zahlung (Self-Service)

GET/POST /{lang}/checkout/{slug}?type=…, GET /checkout/return
  1. Auf einem Standort ein Self-Service-Angebot „Buchen" klicken.
  2. Formular ausfüllen, „Mit PayPal bezahlen".
  3. In der PayPal-Sandbox mit Test-Käufer bezahlen.
  4. Prüfe die Rückleitung auf die Danke-Seite mit Spiel-Link.

✓ Erwartet: bookings wird paid, eine sessions-Zeile mit Token entsteht, Danke-Seite zeigt Spiel-Link. (Setzt gültige PayPal-Sandbox-Credentials in .env voraus.)

Betreute Tour (Anfrage statt Spiel)

is_guided=1afterGuidedCapture(), Danke-Seite /checkout/betreut-danke
  1. Ein als „betreut" markiertes Angebot buchen und bezahlen.
  2. Danach /admin/anfragen öffnen.

✓ Erwartet: keine Spiel-Session, stattdessen „Betreut-Danke"-Seite + neuer inquiries-Eintrag + „wir melden uns"-Mail.

PayPal-Webhook (Sicherheitsnetz)

POST /api/paypal/webhook
  1. In der PayPal-Developer-Console einen PAYMENT.CAPTURE.COMPLETED-Test an die Webhook-URL senden.
  2. Prüfen, ob die zugehörige Buchung paid ist und genau eine Session existiert.

✓ Erwartet: Buchung wird abgeschlossen, auch wenn der Käufer den Browser vor dem Rücksprung schließt – dank DB-Idempotenz keine Doppel-Session/-Mail.

Bestätigungs-Mail

app/Support/Mailer.php, Templates templates/mail/*
  1. Nach einer Testbuchung das Postfach der Käufer-Adresse prüfen.

✓ Erwartet: Bestätigungsmail mit Spiel-Link kommt an (Dev via 1blu SMTP). Kommt keine Mail: Spam-Ordner + storage/logs prüfen.

C. Freischaltung & Spiel

Freischaltcode einlösen

GET/POST /{lang}/code
  1. Den Zugangscode aus der Bestätigungsmail (oder Test-Session) auf /{lang}/code eingeben.

✓ Erwartet: gültiger Code leitet ins Spiel; falscher Code zeigt Fehlermeldung; nach zu vielen Versuchen greift das Rate-Limit.

„Code vergessen?"

GET/POST /{lang}/code/vergessen
  1. E-Mail-Adresse einer bestehenden Buchung eingeben.

✓ Erwartet: Code wird erneut per Mail gesendet; die Antwort ist immer neutral (Anti-Enumeration – verrät nicht, ob die Adresse existiert).

Spielstart & Fortsetzen (Resume)

GET /play/{token}/, POST /play/{token}/begin
  1. Spiel-Token öffnen (aus Test-Session).
  2. Starten, eine Station lösen, Tab schließen, Token erneut öffnen.

✓ Erwartet: issued → Startbildschirm; active → springt zur aktuellen Station; finished → Abschluss-Seite; expired → Fehlerseite.

Lineare Stationen + Rätsel

GET /play/{token}/station/{n}, POST …/check
  1. Ein Rätsel bewusst falsch beantworten, dann richtig.

✓ Erwartet: falsch = Fehler + nächster Hinweis; richtig = Erfolgstext + Weiter zur nächsten Station. (Ohne Karte ist dies die Standard-Ansicht.)

Interaktive Landkarte

GET /play/{token}/karte, public/assets/js/play-map.js
  1. Voraussetzung: Standort hat eine Karte und alle Stationen haben Koordinaten (Karteneditor).
  2. Spiel starten – die Karte sollte statt der linearen Ansicht erscheinen.
  3. Leuchtende Station antippen, Rätsel im Overlay lösen.

✓ Erwartet: Karte mit Markern; nach richtiger Antwort gleitet der Ausschnitt zur nächsten Station. Fehlen Koordinaten → automatisch lineare Ansicht. Ohne JS zeigt <noscript> Stations-Links.

Antwortprüfung & Hinweise auf Abruf

riddles_i18n.accepted_answers (JSON), case_sensitive/trim_whitespace
  1. Eine akzeptierte Antwort mit anderer Groß-/Kleinschreibung und Leerzeichen eingeben.
  2. Bei einem Rätsel mit Hinweisen „Gibt es einen Hinweis?" antippen, dann ggf. „Noch einen Hinweis anzeigen".

✓ Erwartet: Matching gemäß Flags (case-insensitive/getrimmt); Hinweise erscheinen nur auf Abruf und nacheinander (1 → 2 → 3), unabhängig von Fehlversuchen.

Spielabschluss

GET /play/{token}/finish
  1. Alle Stationen lösen.

✓ Erwartet: Erfolgsbildschirm, einmalig erzeugter 5-stelliger Urkunden-Code + Glückwunsch-Mail, Button zur Urkunden-Einlösung. Kein Doppel-Code bei erneutem Aufruf.

D. Urkunde

Urkunde einlösen (Code → Name+Mail → PDF)

GET/POST /{lang}/urkunde, POST /{lang}/urkunde/erstellen
  1. Den 5-stelligen Code aus dem Spielabschluss auf /{lang}/urkunde eingeben.
  2. Name + E-Mail angeben und Urkunde erstellen.

✓ Erwartet: Ort + Datum werden automatisch erkannt; eine PDF wird erzeugt, per Mail als Anhang versendet und in certificates gespeichert.

Urkunden-PDF-Download

GET /{lang}/urkunde/download
  1. Direkt nach dem Erstellen den Download-Button klicken.
  2. PDF öffnen und Name/Datum/Ort prüfen.

✓ Erwartet: korrekt befüllte PDF (Platzhalter {name}/{date}/{location} ersetzt), Layout gemäß Admin-Vorlage.

E. Admin-Backend

Admin-Login

GET/POST /admin/login (Argon2id, CSRF)
  1. Mit Admin-Zugang anmelden (Anlage: php bin/admin-create.php <mail> <pass>).
  2. Falsches Passwort testen.

✓ Erwartet: korrekt = Dashboard; falsch = Fehlermeldung; ungeschützte /admin/…-Seiten leiten auf Login um.

Dashboard (KPIs + Übersetzungslücken)

GET /admin
  1. Nach Login das Dashboard ansehen.

✓ Erwartet: KPI-Kacheln (Buchungen etc.) und ein Report, wie viel je Sprache noch unübersetzt ist.

Standorte-CRUD

/admin/standorte/…
  1. Standort anlegen/bearbeiten, pro aktiver Sprache die Tabs füllen, speichern.
  2. Öffentliche Standortseite neu laden.

✓ Erwartet: Änderungen erscheinen sofort öffentlich; i18n-Tabs pro Sprache funktionieren.

Stationen-CRUD

/admin/standorte/{id}/stationen/…
  1. Stationen anlegen und die Reihenfolge ändern.

✓ Erwartet: Speichern klappt; bei Positions-Konflikt wird automatisch getauscht (Auto-Swap).

Rätsel-CRUD

/…/stationen/{sid}/raetsel/…
  1. Rätsel anlegen: Frage, akzeptierte Antworten (eine pro Zeile), bis zu 3 Hinweise.
  2. Im Spiel gegenprüfen.

✓ Erwartet: „eine Antwort pro Zeile" landet als JSON-Array; die Antworten werden im Spiel akzeptiert.

Karteneditor

/admin/standorte/{id}/karte, app/Admin/MapController.php
  1. Karte (SVG/PNG/JPG/WEBP) hochladen.
  2. Stationen per Drag positionieren, speichern.

✓ Erwartet: viewBox wird erkannt; Positionen landen in stations.map_x/map_y; die Karte erscheint danach im Spiel.

UI-Texte-Editor

/admin/ui-texte/…
  1. Einen UI-Text (Button/Label) ändern und speichern.
  2. Öffentliche Seite prüfen.

✓ Erwartet: geänderter Text erscheint überall dort, wo der string_key genutzt wird; Kontext-Filter + Suche funktionieren.

Sprachen-Verwaltung

/admin/sprachen, app/Admin/LanguageController.php
  1. Sprache anlegen/aktivieren, Standard setzen, Format-Regeln (Zahl/Datum) ändern.

✓ Erwartet: Abdeckungs-Anzeige stimmt; Schutzregeln greifen (Default nicht deaktivierbar, immer ≥1 aktive Sprache); Umschalter erscheint ab 2 Sprachen.

Medien-Verwaltung

/admin/medien
  1. Bild/Ton/Video/PDF hochladen oder als externen Link hinterlegen, einer Entität zuordnen.

✓ Erwartet: Upload-Validierung (Whitelist, Größenlimit, Magic-Bytes) greift; Auslieferung über /media/{id} klappt; Alt-Text/Caption pro Sprache speicherbar.

Anfragen-Verwaltung

/admin/anfragen
  1. Eine Anfrage öffnen, Status-Filter nutzen, Status inline ändern.

✓ Erwartet: Kontaktanfragen + betreute Buchungen erscheinen hier; Status-Wechsel wird gespeichert.

Urkunden-Vorlagen-Editor

/admin/urkunden, „Test-PDF"
  1. Hintergrund-PDF hochladen, Textfelder positionieren (mm, Farbe, Größe, Ausrichtung).
  2. „Test-PDF" mit Beispieldaten erzeugen.

✓ Erwartet: Test-PDF zeigt Felder an der richtigen Position; Standard- vs. Standort-Vorlage wählbar.

Erstellte Urkunden

/admin/urkunden/erstellt
  1. Nach einer Urkunden-Einlösung die Liste öffnen, nach Name/E-Mail suchen, PDF herunterladen.

✓ Erwartet: erzeugte Urkunde erscheint in der Liste, Download liefert dieselbe PDF.

Page-Builder (Inline-Editor)

/admin/api/blocks/…, public/assets/js/edit-mode.js
  1. Als Admin die Startseite mit Bearbeiten-Modus öffnen.
  2. Einen Block hinzufügen/ändern/verschieben, speichern, neu laden.

✓ Erwartet: Blöcke lassen sich per Side-Panel bearbeiten; Änderungen bleiben nach Reload erhalten (Baum in page_blocks).

KI-Übersetzung (API + manueller Modus)

app/Admin/AiController.php, 🪄 in UI-Texte/Stationen/Rätsel, „Sprache anlegen & alles übersetzen"
  1. API-Modus: bei gesetztem Provider-Key auf den Zauberstab 🪄 klicken → Vorschlag übernehmen.
  2. Manueller Modus: Prompt kopieren → in eine beliebige Chat-KI → Antwort zurück einfügen.

✓ Erwartet: Übersetzung landet im Feld; der manuelle Parser fängt Fehler selbst ab und warnt bei Platzhalter-/HTML-Abweichungen (kein DB-Zugriff nötig).

Test-Session (QA ohne PayPal)

POST /admin/standorte/{id}/test-session
  1. Bei einem Standort „Test-Session starten" klicken.

✓ Erwartet: TEST-…-Buchung (Status paid, +14 Tage gültig), direkte Weiterleitung ins Spiel – erzeugt keine echte Urkunde/Mail-Kette.

Audit-Log

Tabelle audit_log
  1. Eine beliebige Admin-Änderung vornehmen.
  2. Tabelle audit_log prüfen (aktuell noch per DB, Ansicht ist Phase 2).

✓ Erwartet: neuer Eintrag mit action, entity, entity_id und JSON-diff.

F. Betrieb & Sicherheit

Rate-Limiting

app/Support/RateLimiter.php, Tabelle rate_limits
  1. An /code, Kontakt oder /urkunde mehrfach schnell hintereinander absenden.

✓ Erwartet: nach dem Limit wird pro IP blockiert (Fehlermeldung) – Schutz gegen Brute-Force/Enumeration.

CSRF-Schutz

app/Support/Csrf.php
  1. Ein Formular absenden – im HTML muss ein _token-Feld stehen.
  2. (Optional) Absenden mit manipuliertem/fehlendem Token.

✓ Erwartet: normaler Submit klappt; fehlender/falscher Token wird abgewiesen.

Lokalisierte Formatierung (Preis/Datum)

app/I18n/Formatter.php, Regeln in languages
  1. Dieselbe Seite in zwei Sprachen mit unterschiedlichen Format-Regeln ansehen.

✓ Erwartet: Preis/Datum wechseln das Format (z. B. 6,49 € vs. €6.49) – ganz ohne ext-intl.

Backups (erstellen + einspielen)

/admin/backups, app/Support/BackupService.php, CLI/Cron: bin/backup.php
  1. Im Admin unter Backups „Backup jetzt erstellen" klicken.
  2. Zum Wiederherstellen unten Datenbank- und/oder Datei-Stand wählen und bestätigen.

✓ Erwartet: zwei Dateien je Stand (db_…sql.gz + uploads_…tar.gz, gleicher Zeitstempel = zusammengehörig) in private/backups/; beim Kombinieren unterschiedlicher Stände warnt das System und verlangt ein Zusatz-Häkchen. Vor jedem Einspielen wird automatisch ein Sicherheits-Backup des aktuellen Zustands angelegt. Behalten werden die letzten 14 Stände (BACKUP_KEEP in private/.env).

Tiny File Manager entfernt

verschoben nach private/legacy/_tinyfm/ (2026-07-13); Blocks in nginx + public/.htaccess bleiben
  1. Im Browser /_tinyfm/ aufrufen.

✓ Erwartet: HTTP 403/404 – der Ordner liegt nicht mehr im Webroot. Der frühere Go-Live-Blocker ist damit erledigt.

Diese Seite wird durch den Code unter app/Http/Controllers/HelpController.php und templates/web/dev-help.html.twig erzeugt. Anpassungen direkt dort, kein DB-Inhalt.