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).
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.
1. Anmelden im Backend
So kommst du in den Verwaltungsbereich:
- Rufe im Browser die Adresse deiner Webseite auf und hänge
/adminan (z. B.deine-webseite.de/admin). - Gib deine E-Mail-Adresse und dein Passwort ein.
- Du landest auf der Übersicht. Links siehst du das Menü mit allen Bereichen.
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
- Menü Standorte öffnen.
- Oben auf Neu klicken – oder einen bestehenden Standort zum Bearbeiten anklicken.
- Die Felder ausfüllen: Name, Überschrift, Beschreibung, Hinweise usw.
- Gibt es mehrere Sprachen, findest du oben Reiter (Tabs) pro Sprache – fülle jeden Reiter aus, damit der Standort in jeder Sprache Text hat.
- 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.
knirpsenfarmin…/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.
- Beim Standort auf Stationen verwalten klicken.
- Station anlegen: Titel, Einleitungstext und optional ein Hilfetext.
- Die Reihenfolge bestimmt, in welcher Abfolge die Kinder die Stationen durchlaufen. Du kannst sie jederzeit ändern; vertauschte Positionen werden automatisch sauber getauscht.
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.
- Bei einer Station auf Rätsel gehen und ein Rätsel anlegen.
- Frage eingeben.
- Akzeptierte Antworten: schreibe jede erlaubte Antwort in eine eigene Zeile. Alle Zeilen gelten als richtig (praktisch für Schreibweisen wie „Baum"/„der Baum").
- 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.
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.
- Beim Standort auf Karte & Positionen klicken.
- Ein Kartenbild hochladen (Bild oder SVG-Zeichnung).
- Die Stationen mit der Maus an die richtige Stelle auf der Karte ziehen und speichern.
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.
- Als angemeldeter Admin die Startseite öffnen und den Bearbeiten-Modus starten.
- Einen Block anklicken – rechts öffnet sich ein Feld zum Ändern des Inhalts.
- Blöcke lassen sich hinzufügen, ändern und verschieben. Speichern nicht vergessen.
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.
- Menü UI-Texte öffnen.
- Über die Suche oder den Kategorie-Filter den gewünschten Text finden.
- Text ändern und speichern – er erscheint sofort überall dort, wo er verwendet wird.
9. Sprachen & Übersetzen
Die Webseite kann mehrsprachig sein. Standardmäßig ist Deutsch eingestellt. Du kannst weitere Sprachen freischalten.
Eine Sprache verwalten
- Menü Sprachen öffnen.
- Sprache anlegen oder aktivieren. Sobald zwei oder mehr Sprachen aktiv sind, erscheint für Besucher automatisch oben ein Sprach-Umschalter.
- Du kannst eine Standard-Sprache festlegen und je Sprache einstellen, wie Preise und Datum geschrieben werden (z. B. „6,49 €" vs. „€6.49").
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).
- Menü Medien öffnen, Neu wählen.
- Datei hochladen oder Link einfügen, den Zweck zuordnen, optional Bildbeschreibung angeben.
- Speichern. Erlaubt sind gängige Bild-, Ton-, Video- und PDF-Formate; sehr große Dateien werden abgelehnt.
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).
- Menü Anfragen öffnen.
- Über den Status-Filter z. B. nur „offene" anzeigen.
- 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
- Menü Urkunden-Vorlagen öffnen.
- Optional eine Hintergrund-PDF hochladen (dein schön gestaltetes Urkunden-Design).
- Textfelder platzieren: Wo soll der Name stehen, wo das Datum, wo der Ort? Du legst Position, Größe, Farbe und Ausrichtung fest.
- Mit „Test-PDF" kontrollierst du das Ergebnis mit Beispieldaten, bevor es echt wird.
{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.
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.
- Menü Standorte öffnen, den gewünschten Standort wählen.
- Auf „Test-Session starten" klicken.
- Du landest direkt im Spiel und kannst die komplette Tour durchspielen – Freischaltung, alle Rätsel, Landkarte und Urkunde.
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
/play/<token>/…), Mobile-First, Outdoor-Kontrast./admin/…).Architektur-Prinzipien
- Alle Texte aus der DB. Keine hartkodierten Strings im Code – Buttons, Hero-Texte,
Mails, Rätselfragen, alles über
ui_stringsoder*_i18n-Tabellen. - Pflege ausschließlich über das Admin. Kein direktes SQL nötig.
- Hauptzeile + i18n-Begleiter. Pro Entität gibt es
foo(sprachneutrale Stammdaten) undfoo_i18n(Texte pro Sprache).
2. Tech-Stack
| Schicht | Technologie | Datei |
|---|---|---|
| Sprache | PHP 8.1+ (8.1.2-1ubuntu2.25) | composer.json |
| Routing / HTTP | Slim 4 | app/Http/routes.php |
| DI-Container | PHP-DI 7 | app/bootstrap.php |
| Templates | Twig 3 | templates/ |
| Datenbank | MariaDB 10.6+ via PDO (no-emulate-prepares) | app/Database/Connection.php |
| i18n | Eigenes System (DB-getrieben) | app/I18n/{LanguageResolver,Translator}.php |
| Env | vlucas/phpdotenv | private/.env (Mode 0640, Gruppe web-marco) |
PHPMailer – SMTP (Dev: 1blu) mit mail()-Fallback | app/Support/Mailer.php | |
| Bezahlung | PayPal Orders v2 (REST, ohne SDK) + Webhook | app/Payments/PayPalClient.php |
| Page-Builder | Block-Baum-System (page_blocks) mit Inline-Editor (Side-Panel) | app/Cms/{BlockRenderer,BlockRegistry}.php |
| PDF (Urkunden) | FPDI + TCPDF – Hintergrund-PDF importieren + Text-Overlay | app/Support/CertificateRenderer.php |
| Medien | Eigene Verwaltung (media_assets) – Upload oder externer Link, optional pro Sprache | app/Media/MediaRepository.php, app/Admin/MediaController.php |
| Lokalisierung | Zahl/Währung/Datum pro Sprache (kein ext-intl nötig) – Regeln in languages | app/I18n/Formatter.php |
| Landkarte (Spiel) | Inline-SVG mit Marker/Weg-Overlay, Pan/Zoom + Weg-Animation | templates/play/karte.html.twig, public/assets/js/play-map.js |
| Frontend-CSS | Handgeschrieben (kein Build-Schritt): app.css, admin.css, play.css. Tailwind noch nicht aufgesetzt. | public/assets/css/ |
| DB-Migrationen | einfacher SQL-Runner (idempotent) | bin/migrate.php, database/migrations/ |
| QR-Codes | endroid/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
| Tabelle | Inhalt |
|---|---|
languages | Sprach-Codes (de, en …), Eigenbezeichnung, Default- + Aktiv-Flag, Sortierung sowie Format-Regeln (decimal_sep, thousands_sep, currency_before, date_format). Pflege über /admin/sprachen. |
ui_strings | Statische UI-Texte: PK (string_key, language_code), optionaler context-Filter (cta, navigation, play, mail, checkout …) |
Inhalt: Hauptzeile + i18n-Begleiter
| Hauptzeile | i18n-Begleiter | Übersetzte Felder |
|---|---|---|
regions | regions_i18n | name, description |
locations | locations_i18n | name, hero_title, hero_text, description, instructions, meta_description |
tour_types | tour_types_i18n | name, description (Self-Service vs. betreut via is_guided) |
stations | stations_i18n | title, intro, hint_intro |
riddles | riddles_i18n | question, accepted_answers (JSON), success_text, hint_1, hint_2, hint_3 |
page_blocks | page_blocks_i18n | block-typ-abhängige Felder als JSON (fields, z. B. {"headline":…,"html":…}) |
pages | pages_i18n | title, body, meta_description (Body wandert in den Block-Baum) |
certificate_templates | certificate_templates_i18n | display_name, html, css (Legacy – aktiv ist PDF-Overlay) |
media_assets | media_assets_i18n | alt_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 CASCADEfür i18n-Begleiter, sonstON 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_lstatt 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_keyper 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:
- URL-Prefix (
{lang}-Routenparameter) - Cookie
schatzilo_session_lang Accept-Language-Header- Default-Sprache aus
languages.is_default = 1(zur Zeitde)
Twig-Filter / Funktionen
| Aufruf | Effekt |
|---|---|
{{ '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
| Middleware | Zweck |
|---|---|
SessionMiddleware | session_start() mit sicherer Cookie-Konfiguration |
LanguageMiddleware | Sprache resolven, Twig-Globals setzen, in Translator pushen |
AdminAuthMiddleware | 302→/admin/login wenn keine $_SESSION['admin_id'] |
PlaySessionMiddleware | Session 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
certificatesmit Suche (Name/E-Mail) und PDF-Download. - Page-Builder – Inline-Editor (Side-Panel,
edit-mode.js) fürpage_blocks-Bäume; die Startseite läuft darüber. REST-API unter/admin/api/blocks, Block-Typen inapp/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:
| Status | Bedeutung | Aktion bei GET /play/{token}/ |
|---|---|---|
issued | Token ausgestellt, noch nicht gestartet | Start-Bildschirm |
active | Spiel läuft | Redirect zur aktuellen Station |
finished | Alle Rätsel gelöst | Redirect zu /finish |
expired | Älter als expires_at (echter Kauf: code_validity_minutes des Tour-Typs; Test-Session: +14 Tage) | Fehler-Seite |
Antwort-Matching
riddles_i18n.accepted_answersist ein JSON-Array.- Wenn
trim_whitespace = 1→ User-Eingabe und jede Antwort getrimmt. - Wenn
case_sensitive = 0→ Vergleich gegenmb_strtolower. - Erste Übereinstimmung gewinnt →
session_progress.solved_atwird gesetzt.
Hinweise auf Abruf
Alle gepflegten Hinweise (hint_1–hint_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äger | Inhalt |
|---|---|
location_maps | Karte je Standort (SVG/Bild + viewbox_*); optional je Tour-Typ, NULL = Default |
stations.map_x, map_y | Position auf der Karte, relativ 0…1 (Marker-Pixel = vb_x + map_x·vb_w) |
stations.map_path_to_next | SVG-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
- User klickt „Mit PayPal bezahlen" auf
/{lang}/checkout/{slug}. PaymentService::startCheckout()legtbookingsalspendingan, ruft PayPal-Orders-API auf, speichertpaypal_order_id.- 302 zur PayPal-Approval-URL.
- Käufer approved → PayPal redirected zu
/checkout/return?token=<orderId>. CheckoutController::returnFromPaypal()ruftcaptureOrder()auf, protokolliert inpayments, ruftafterCapture().PaymentService::afterCapture()ist idempotent: setzt Statuspaid, legtsessions-Token an, sendet Bestätigungsmail.- 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)
GET /{lang}/urkunde?code=…– Code-Eingabe (aus Link vorbefüllt).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.POST /{lang}/urkunde/erstellen–createAndSend()wählt die Vorlage (Standort → Default), rendert die PDF, speichert sie unterprivate/uploads/certificates/, legt einecertificates-Zeile an und versendet die PDF als Mail-Anhang.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
| Pfad | Modus | Eigentümer | Grund |
|---|---|---|---|
private/ | 2750 | ki-claude:web-marco | web-marco darf rein, andere nicht |
private/.env | 0640 | dito | web-marco darf lesen, schreiben nur Owner |
storage/ | 2775 | dito | web-marco schreibt Twig-Cache + Logs |
private/uploads/ | 2775 | dito | für PDF-Urkunden + Asset-Uploads |
nginx neu laden
sudo nginx -t && sudo systemctl reload nginx
13. Live-Status
| Schalter | Wert |
|---|---|
APP_ENV | development |
APP_DEBUG | an |
| PHP | 8.1.2-1ubuntu2.25 |
| PayPal-Mode | sandbox |
| PayPal-Credentials | konfiguriert |
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
- Rufe die Domain-Wurzel
/auf. - 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})
- Auf der Startseite einen Standort anklicken.
- 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
- Im Admin unter
/admin/spracheneine zweite Sprache (z. B.en) aktivieren. - Auf der öffentlichen Seite den Sprachumschalter oben nutzen.
- 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
- Seite aufrufen (Link im Footer/Navigation).
- 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
- Formular ausfüllen und absenden.
- Danach im Admin unter
/admin/anfragennachsehen.
✓ 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
- 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}
- 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
- URL im Browser oder per
curlaufrufen.
✓ 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
- Auf einem Standort ein Self-Service-Angebot „Buchen" klicken.
- Formular ausfüllen, „Mit PayPal bezahlen".
- In der PayPal-Sandbox mit Test-Käufer bezahlen.
- 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=1 → afterGuidedCapture(), Danke-Seite /checkout/betreut-danke
- Ein als „betreut" markiertes Angebot buchen und bezahlen.
- 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
- In der PayPal-Developer-Console einen
PAYMENT.CAPTURE.COMPLETED-Test an die Webhook-URL senden. - Prüfen, ob die zugehörige Buchung
paidist 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/*
- 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
- Den Zugangscode aus der Bestätigungsmail (oder Test-Session) auf
/{lang}/codeeingeben.
✓ 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
- 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
- Spiel-Token öffnen (aus Test-Session).
- 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
- 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
- Voraussetzung: Standort hat eine Karte und alle Stationen haben Koordinaten (Karteneditor).
- Spiel starten – die Karte sollte statt der linearen Ansicht erscheinen.
- 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
- Eine akzeptierte Antwort mit anderer Groß-/Kleinschreibung und Leerzeichen eingeben.
- 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
- 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
- Den 5-stelligen Code aus dem Spielabschluss auf
/{lang}/urkundeeingeben. - 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
- Direkt nach dem Erstellen den Download-Button klicken.
- 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)
- Mit Admin-Zugang anmelden (Anlage:
php bin/admin-create.php <mail> <pass>). - Falsches Passwort testen.
✓ Erwartet: korrekt = Dashboard; falsch = Fehlermeldung; ungeschützte /admin/…-Seiten leiten auf Login um.
Dashboard (KPIs + Übersetzungslücken)
GET /admin
- 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/…
- Standort anlegen/bearbeiten, pro aktiver Sprache die Tabs füllen, speichern.
- Öffentliche Standortseite neu laden.
✓ Erwartet: Änderungen erscheinen sofort öffentlich; i18n-Tabs pro Sprache funktionieren.
Stationen-CRUD
/admin/standorte/{id}/stationen/…
- Stationen anlegen und die Reihenfolge ändern.
✓ Erwartet: Speichern klappt; bei Positions-Konflikt wird automatisch getauscht (Auto-Swap).
Rätsel-CRUD
/…/stationen/{sid}/raetsel/…
- Rätsel anlegen: Frage, akzeptierte Antworten (eine pro Zeile), bis zu 3 Hinweise.
- 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
- Karte (SVG/PNG/JPG/WEBP) hochladen.
- 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/…
- Einen UI-Text (Button/Label) ändern und speichern.
- Ö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
- 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
- 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
- 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"
- Hintergrund-PDF hochladen, Textfelder positionieren (mm, Farbe, Größe, Ausrichtung).
- „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
- 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
- Als Admin die Startseite mit Bearbeiten-Modus öffnen.
- 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"
- API-Modus: bei gesetztem Provider-Key auf den Zauberstab 🪄 klicken → Vorschlag übernehmen.
- 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
- 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
Tabelleaudit_log
- Eine beliebige Admin-Änderung vornehmen.
- Tabelle
audit_logprü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
- An
/code, Kontakt oder/urkundemehrfach schnell hintereinander absenden.
✓ Erwartet: nach dem Limit wird pro IP blockiert (Fehlermeldung) – Schutz gegen Brute-Force/Enumeration.
CSRF-Schutz
app/Support/Csrf.php
- Ein Formular absenden – im HTML muss ein
_token-Feld stehen. - (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
- 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
- Im Admin unter Backups „Backup jetzt erstellen" klicken.
- 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 nachprivate/legacy/_tinyfm/ (2026-07-13); Blocks in nginx + public/.htaccess bleiben
- 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.