Version PWA Playwright License PHP Docker
- 📱 Progressive Web App - Installierbar auf allen Geräten, funktioniert offline
- 🔄 Vollständig Wiederverwendbar - Eine einzige
event.jsonfür jede Veranstaltung - 🌍 Mehrsprachig - Integrierte i18n-Unterstützung (Deutsch/Englisch)
- ⚡ Performance-Optimiert - Cache-Busting, Service Worker, Lazy Loading
- 📊 Event-Features - Sessionpläne, Zeitpläne, Speisekarten, Sponsoren, Voting
- ✏️ Online Content-Verwaltung - Event-Daten im Browser bearbeiten, ohne Deployment
- 🎨 Auto-Branding - PWA-Icons werden automatisch aus einem einzigen Quellbild generiert
- 🖌️ Custom Styles - Farbpalette und CSS pro Event ueber
content/assets/custom.cssueberschreibbar — ohne Rebuild - 🔗 SEO & Social-Previews - Server-seitig gerenderte Meta-, OpenGraph- und Twitter-Card-Tags aus
event.json, Canonical-URL pro Seite — Crawler und Social-Previews zeigen echte Event-Daten ohne JS - 🧪 100% Getestet - Playwright-Tests für Übersetzungen, PWA, UI/UX
- 🐳 Docker Ready - Entwicklungsumgebung mit einem Befehl
**Live: https://app.devops-camp.de oder https://app.joomladay.de**
- Quick Start
- Screenshots
- Konfiguration
- Content-Verwaltung (Admin)
- Voting-System
- Entwicklung
- Internationalisierung (i18n)
- Architektur
- Lizenz
git clone <repository-url>
cd pccampapp
cp .env.example .env
# Dev-Container starten — src/ live-mount, Live-Reload, Port 5173
make dev-start
open http://localhost:5173Kein lokales Node/npm noetig — alles laeuft im Container. Fuer lokale
Playwright-Tests auf dem Host zusaetzlich: make install (Playwright-Browser).
# Multi-Stage Build als Docker-Image, Port 5174 (Foreground)
make dev-prod-start
open http://localhost:5174make dev-prod-start baut exakt das Image, das auch auf den Server geht.
Auf dem Ziel-Server brauchst du nur Docker (docker compose). Kein Node,
kein PHP, keine Build-Toolchain:
# 1. Repo oder einfach nur diese Dateien klonen:
# Dockerfile, docker-compose.prod.yml, nginx.prod.conf, nginx.security-headers.conf,
# docker-entrypoint.sh, src/, seed/, build-cache-busting.cjs,
# package*.json, event.schema.json
git clone <repository-url>
cd pccampapp
# 2. Environment setzen (sicherer Admin-Key + Event-Domain!)
cp .env.example .env
vim .env # VOTING_ADMIN_KEY=<random>
# TRUSTED_HOSTS=camp.example.com,www.camp.example.com
# 3. Bauen und starten — Multi-Stage-Build erstellt das Image aus der Node-Stage,
# content/ wird beim ersten Start aus seed/ geseedet.
make prod-start
# 4. Reverse-Proxy (nginx/Caddy/Traefik) vorschalten und Port 5174 bzw. PROD_PORT
# auf deine Domain mit TLS routen.Persistente Daten: Alle admin-pflegbaren Inhalte (event.json, menu.json,
sessions.json, votes.json, Hash-Manifest, SW-Version, …) liegen im
Host-Verzeichnis ./content/. Das ist der einzige Pfad, den du beim Deploy
sichern musst.
# Backup
tar cf content-backup-$(date +%F).tar content/
# Restore
make prod-stop
tar xf content-backup-YYYY-MM-DD.tar
make prod-startUpdates (neue App-Version ausrollen):
git pull
make prod-startDas Image wird neu gebaut, content/ bleibt unveraendert — Admin-Aenderungen
ueberleben jedes Update. Service Worker invalidiert automatisch (neue
BUILD_VERSION im Image + bestehende content/sw-version.txt).
Screenshots anzeigen
pccampapp1
pccampapp2
pccampapp3
pccampapp4
pccampapp5
Die App ist komplett konfigurationsgesteuert — keine Code-Änderungen für neue Events. Es gibt zwei Wege, Inhalte zu pflegen:
| Weg | Wann nutzen? |
|---|---|
| Admin-UI (empfohlen) | Laufendes Event, Inhalte ändern sich regelmäßig, keine Dev-Tools nötig |
| seed/ (Initial-Vorlage) | Erstes Deployment eines neuen Events, Vorlage in Git versionieren |
Nach dem ersten Container-Start wird
seed/einmalig nachcontent/kopiert. Ab dann leben alle Inhalte (inkl. Admin-Uploads von Logo/Floorplan/ Sponsor-Logos) imcontent/-Volume und überleben jedes Image-Update.
# 1. Event-Konfiguration bearbeiten (Seed-Vorlage fuer frische Deployments)
vim seed/event.json
# 2. Nur PWA-Icon-Quelle ist noch Code (Image-gebacken, generiert 16/144/192/512)
cp ihr-icon.png src/assets/icon.png
# 3. Eventspezifische Inhalte aktualisieren (Seed — kopiert beim ersten Start
# ins Laufzeit-Volume content/; spaetere Admin-Edits leben dort)
vim seed/sessionplan/sessions.json # Sessionplan-Daten
vim seed/timetable/timetable.json # Zeitplan-Daten
vim seed/news.json # Event-News
vim seed/menu.json # Navigation
vim seed/food/menue.json # Speisekarten
vim seed/food/allergene.json # Allergen-Informationen
vim seed/sponsors/sponsors.json # Sponsoren-Liste
# 4. Event-Grafiken (content — tauschbar ohne Rebuild, Admin-Upload moeglich)
cp ihr-logo.png seed/assets/logo.png # Header/Brand
cp ihr-floorplan.jpg seed/floorplan/floorplan.jpg # Raumplan
cp ihr-sponsor.png seed/sponsors/logos/sponsor-placeholder.png # Sponsor-Logo
# 5. Bauen & deployen
make buildKeine Code-Änderungen nötig! Das Build-System automatisch:
- Generiert PWA-Manifest aus der Konfiguration
- Erstellt alle PWA-Icons (16x16, 144x144, 192x192, 512x512)
- Wendet Theme-Farben an
Alle event.json-Felder (Name, Titel, Beschreibung, Theme-Color, Copyright, Logo-Alt, Manifest, OpenGraph/Twitter-Cards, Canonical-URL) werden server-seitig beim Request aus content/event.json in den HTML-Head gerendert (die 6 oeffentlichen Einstiegsseiten sind PHP-Templates). Damit sehen Crawler und Social-Previews schon im initialen Response die echten Event-Daten — ganz ohne JavaScript. Admin-Edits im Tab „Event" schlagen ab dem naechsten Request durch; event-config-loader.js ergaenzt zusaetzlich ein DOM-Live-Update, damit offene Browser-Tabs die Aenderung auch ohne Reload sehen.
Wichtig fuer Prod: Die Canonical-/OG-URL nutzt den Request-Host, validiert gegen die
TRUSTED_HOSTS-Env. Ohne eingetragene Domain faellt sie auflocalhostzurueck — Social-Previews zeigen dann die falsche URL. Siehe .env.example und den Deploy-Abschnitt unten.
Die App verwendet mehrere JSON-Dateien für eventspezifische Inhalte. Diese müssen für jede neue Veranstaltung angepasst werden. Quelle dafür ist der seed/-Ordner — beim ersten Container-Start wird er nach content/ kopiert; ab dann leben alle Admin-Edits (und hochgeladene Grafiken) ausschließlich im content/-Volume.
Strukturiert Sessions nach Tagen und Zeitslots:
{
"samstag": {
"11:00 - 12:00": [
{
"id": "10",
"room": "Raum Alpha",
"title": "Einführung in moderne Entwicklungsmethoden",
"host": "Max Mustermann",
"votes": 0,
"cancelled": false
}
]
},
"sonntag": {
"10:00 - 11:00": [
{
"id": "100",
"room": "Raum Beta",
"title": "Erfahrungsaustausch",
"host": "Lisa Schmidt",
"votes": 0,
"cancelled": false
}
]
}
}Zeitplan für das gesamte Event:
{
"freitag": {
"18:00 - 21:00 Uhr": [
{
"room": "Empfangsbereich",
"title": "Check-in und Networking"
}
]
},
"samstag": {
"09:30 Uhr": [
{
"room": "Hauptraum",
"title": "Begrüßung und Vorstellungsrunde"
}
]
}
}Event-News mit Zeitfenstern:
{
"permanent": [
{
"id": "1",
"content": "Willkommen zur Event-Demo!",
"priority": "medium"
}
],
"days": {
"2025-11-15": [
{
"id": "10",
"content": "Die Türen öffnen um 18:00 Uhr.",
"timeFrom": "16:00",
"timeTo": "20:00",
"priority": "high"
}
]
}
}Menü nach Tagen und Mahlzeiten:
{
"samstag": {
"Frühstück": [
{
"name": "Brötchen-Auswahl",
"variants": [
{
"name": "Vollkorn",
"allergens": ["A"]
}
]
}
],
"Mittagessen": [
{
"name": "Pasta-Station",
"variants": [
{
"name": "Bolognese",
"allergens": ["A", "G", "I"]
}
]
}
]
}
}Allergen-Codes und Beschreibungen:
{
"A": "Glutenhaltiges Getreide",
"C": "Eier",
"G": "Milch",
"I": "Fleisch",
"L": "Schwefeldioxid und Sulfite"
}Sponsoren-Liste. Das Feld logo akzeptiert zwei Varianten:
- Lokal — Pfad relativ zu
content/sponsors/, z. B."logos/foo.png". Die Datei wird aus dem Content-Volume ausgeliefert (Seed:seed/sponsors/logos/, Laufzeit/Admin:content/sponsors/logos/). - Extern — absolute URL (
https://…) oder absoluter Pfad (/…) auf ein CDN- oder extern gehostetes Logo. Wird 1:1 in den<img src>uebernommen.
{
"sponsors": [
{
"name": "Sponsor 1",
"logo": "logos/sponsor-placeholder.png",
"url": "https://example.com",
"beschreibung": "Beschreibung des Sponsors"
}
]
}Das Floorplan-Bild lebt im Content-Volume (content/floorplan/floorplan.jpg).
Austausch ohne Rebuild — per Admin-Upload (Tab Event → Branding &
Medien), per Volume-Zugriff oder Ersatzdatei im Seed.
Das Header-/Brand-Logo lebt ebenfalls im Content-Volume
(content/assets/logo.png). Der Pfad wird in event.json als
branding.logo hinterlegt — akzeptiert relative Pfade (relativ zu
content/) oder absolute URLs (CDN). Admin-Upload im Tab Event →
Branding & Medien bumpt dabei sw-version.txt, sodass Clients beim
naechsten Reload das neue Logo aus einem frischen Service-Worker-Cache ziehen.
Hauptnavigation der App:
{
"items": [
{
"title": "Sessionplan",
"url": "/sessionplan/",
"description": "",
"icon": "calendar",
"active": true
},
{
"title": "WLAN",
"url": "",
"description": "EventWiFi / test123",
"icon": "wifi",
"active": true
}
]
}Schritt-für-Schritt-Anleitung:
Hinweis: Alle Pfade beziehen sich auf
seed/— die Vorlage fuer frische Deployments. Nach dem ersten Start sind Aenderungen ueber den Admin-Bereich im laufenden Container persistent und landen imcontent/-Volume.
- Sessionplan aktualisieren (
seed/sessionplan/sessions.json):
- Tage anpassen (z.B.
samstag,sonntag→freitag,samstag) - Zeitslots anpassen (z.B.
11:00 - 12:00→10:00 - 11:00) - Raum-Namen aktualisieren
- Session-Titel, Hosts und IDs anpassen
- Zeitplan erstellen (
seed/timetable/timetable.json):
- Event-Tage definieren
- Zeitslots mit Räumen und Aktivitäten hinzufügen
- Struktur:
"Tag": { "Zeit": [{"room": "Raum", "title": "Aktivität"}] }
- News konfigurieren (
seed/news.json):
- Permanente News in
permanentArray - Tages-spezifische News in
daysObjekt - Zeitfenster mit
timeFrom/timeTo(optional) - Prioritäten:
high,medium,low
- Speisekarten erstellen (
seed/food/menue.json):
- Mahlzeiten nach Tagen strukturieren
- Allergen-Codes aus
allergene.jsonverwenden - Varianten für verschiedene Optionen
- Sponsoren hinzufügen (
seed/sponsors/sponsors.json):
- Entweder: Logo-Datei nach
seed/sponsors/logos/legen (im Betrieb:content/sponsors/logos/) und als"logo": "logos/dateiname.png"referenzieren - Oder: Extern hostet —
"logo": "https://cdn.example.com/logo.png"(auch absolute Pfade/…moeglich); wird 1:1 verwendet - URLs und Beschreibungen anpassen
- Floorplan austauschen (
seed/floorplan/floorplan.jpg):
- Bild ersetzen; wird zur Laufzeit aus
content/floorplan/floorplan.jpgausgeliefert
- Navigation anpassen (
seed/menu.json):
- Menüpunkte aktivieren/deaktivieren
- URLs und Beschreibungen aktualisieren
- WLAN-Informationen anpassen
Tipp: Alternativ können alle JSON-Dateien über die Content-Verwaltung direkt im Browser bearbeitet werden — ohne Build oder Deployment.
Die Farbpalette und beliebige CSS-Regeln koennen pro Event ueber content/assets/custom.css ueberschrieben werden — ohne Rebuild. src/assets/app.css und die Modul-CSS nutzen CSS-Custom-Properties; content/assets/custom.css wird als letztes Stylesheet geladen und gewinnt.
/* content/assets/custom.css */
:root {
--color-primary: #d9174b;
--color-primary-dark: #b70529;
--color-primary-light: #ea285c;
--color-text: #41454b;
--color-text-strong: #111111;
--color-bg: #f5f5f5;
--color-border: #dadada;
}
/* Einzelne Komponenten gezielt ueberschreiben */
.card { border-radius: 4px; }Verfuegbare Tokens: --color-primary / -dark / -light, --color-text / -strong / -soft / -muted, --color-bg, --color-surface / -alt, --color-border / -strong, --color-success, --color-danger, --color-favorite / -hover. Default-Werte stehen im :root-Block von src/assets/app.css. Seed-Vorlage liegt in seed/assets/custom.css mit Token-Dokumentation. Admin-UI bleibt bewusst von dem Override ausgenommen.
Das Admin-Panel verwaltet alle Event-Daten (Sessions, Timetable, News, Food, Sponsoren, Navigation, Event-Config + Logos/Floorplan) und das Voting direkt im Browser — ohne Build, ohne Deployment, ohne Commit.
http://localhost:5173/admin/
Login über ein Formular mit dem Admin-Key. Der Key wird beim Deployment über
die VOTING_ADMIN_KEY Umgebungsvariable gesetzt (siehe .env.example)
und ist nur durch Container-Neustart änderbar. Logout über den Button oben
rechts im Admin-Panel.
| Bereich | Datei | Beschreibung |
|---|---|---|
| Sessions | sessions.json |
Sessionplan nach Tagen und Zeitslots |
| Timetable | timetable.json |
Event-Zeitplan |
| News | news.json |
Permanente und tagesspezifische Ankündigungen |
| Food | menue.json |
Speisekarten mit Allergenen |
| Allergene | allergene.json |
Allergen-Codes und Beschreibungen |
| Sponsors | sponsors.json |
Sponsoren-Liste |
| Menu | menu.json |
Navigation der App |
| Event | event.json |
Event-Konfiguration (Raw-JSON-Editor) + Upload von App-Logo und Floorplan-Bild (Branding & Medien) |
| Voting | — | Voting-Status steuern (aktivieren/deaktivieren/beenden), Ergebnisse anzeigen, Votes in sessions.json übertragen |
Features:
- Strukturierter Editor — Formulare für jede Resource (Felder, Checkboxen, Auswahlen)
- Raw JSON Editor — Umschaltbar für direktes JSON-Editing (Event-Tab ist Raw-only)
- Asset-Upload — App-Logo, Floorplan und Sponsor-Logos per Drag & Drop (im Event-Tab, MIME-validiert, max. 5 MB)
- Schema-Validierung — Payloads werden beim Speichern serverseitig geprüft (Typen, Feldlängen, nur sichere URL-Schemes)
- Backup/Restore — Vor jedem Speichern wird automatisch ein Backup erstellt
- Sofort live — Änderungen sind nach dem Speichern direkt für alle User sichtbar (Service Worker network-first für JSON)
- Tastaturkürzel —
Ctrl+S/Cmd+Szum Speichern
Persistenz: Admin-Edits leben in
content/und überleben jedes Image-Update. Der Build fasst das Content-Volume nicht an.
Das Voting-System ermöglicht es Teilnehmern, Sessions zu bewerten und die beliebtesten Sessions zu ermitteln.
Das Voting-System kann über event.json zeitgesteuert aktiviert werden:
{
"features": {
"voting": true,
"votingSchedule": [
{
"day": "samstag",
"dayLabel": "Samstag",
"dayOfWeek": 6,
"startTime": "16:00",
"endTime": "17:45"
}
]
}
}Der Admin-Key wird ausschließlich über die VOTING_ADMIN_KEY Umgebungsvariable gesetzt (siehe .env.example) — nicht mehr in event.json. Der frühere features.votingAdminKey-Fallback ist entfernt; der Admin-API-Schema-Validator lehnt das Feld beim Speichern aktiv ab. Zusätzlich kann das Voting auch über den Admin-Bereich de/aktiviert oder beendet werden.
- voting: Aktiviert/deaktiviert das Voting-System
- votingSchedule: Zeitfenster für Abstimmungen
day: Interner Tag-Name (z.B. "samstag")dayLabel: Anzeige-Name (z.B. "Samstag")dayOfWeek: Wochentag als Zahl (0=Sonntag, 6=Samstag)startTime/endTime: Zeitfenster für Abstimmungen
- VOTING_ADMIN_KEY (env): Geheimes Passwort für Admin-Bereich — in
.envsetzen
Das Voting-Fenster wird nur angezeigt, wenn alle folgenden Bedingungen erfüllt sind:
// event.json
{
"features": {
"voting": true // ← Muss auf true stehen
}
}// content/voting/voting-state.json
{
"status": "active" // ← Mögliche Werte: "inactive", "active", "ended"
}Der Status kann über den Admin-Bereich geändert werden.
Das aktuelle Datum und die Uhrzeit müssen innerhalb eines konfigurierten Zeitfensters liegen:
// event.json
{
"features": {
"votingSchedule": [
{
"dayOfWeek": 6, // Samstag
"startTime": "16:00", // ← Voting öffnet um 16:00 Uhr
"endTime": "17:45" // ← Voting schließt um 17:45 Uhr
}
]
}
}Prüfung: System vergleicht aktuellen Wochentag (0=Sonntag, 6=Samstag) und Uhrzeit mit der Konfiguration.
Testing: Für lokale Tests kann die Zeitfenster-Prüfung mit dem Query-Parameter
?vote=<tag>(z. B.?vote=samstag) übersprungen werden — Feature-Flag und Admin-Status müssen trotzdem aktiv sein.
Von Teilnehmern:
- Voting-Button erscheint im Sessionplan während der konfigurierten Zeitfenster
- Jeder Teilnehmer kann eine Stimme pro Tag abgeben
- Abstimmung erfolgt via Browser-Fingerprint (anonymisiert)
- Top 3 Sessions werden mit Medaillen (🥇🥈🥉) angezeigt
Voting wird im Admin-Panel im Tab "Voting" verwaltet:
- Live-Statistik und Teilnehmerzahlen
- Voting aktivieren / deaktivieren / beenden
- Ergebnisse in
sessions.jsonübertragen (Winner-Badge TOP 3) - Ergebnis-Ansicht mit Medaillen-Ranking
| Befehl | Was es tut |
|---|---|
make dev-start |
Dev-Container starten (Port 5173, Live-Reload aus src/) |
make dev-prod-start |
Prod-Image lokal testen (Port 5174, Multi-Stage-Build) |
make prod-start |
Prod-Container für Server-Deployment (detached, baut Image) |
make test-all |
Alle Playwright-Tests |
make clean |
node_modules, build-Output und Docker-Artefakte entfernen |
Jedes der drei start-Targets hat passende -stop, -build, -logs und
-remove-Varianten (z. B. make dev-stop, make prod-build). Eine vollständige
Übersicht liefert make help oder cat Makefile.
- 🇩🇪 Deutsch (
de) - 🇬🇧 Englisch (
en)
Sprache in event.json festlegen:
{
"event": {
"locale": "de"
}
}Alle UI-Labels (z.B. „Sessions", „Favoriten", „Voting aktiv") liegen in
src/translations/de.json und src/translations/en.json. Wer die App
auf eigene Vokabeln umstellen will (z.B. „Sessions" → „Vorträge",
„Voting" → „Abstimmung"), aendert die entsprechenden Werte direkt in
beiden Dateien. Schluessel-Namen bleiben unveraendert — sonst greifen die
data-i18n-Bindings im HTML nicht mehr.
⚠️ Build-Time, nicht Runtime: Translations sind Teil des Builds (anders alsevent.json,custom.cssoder Sponsoren-Logos imcontent/-Volume). Aenderungen werden erst nach einem Rebuild aktiv —make buildlokal bzw. Image neu bauen + redeployen in Prod. Es gibt aktuell keinen Admin-Editor fuer Translations.
- Schlüssel zu
src/translations/de.jsonhinzufügen:
{
"myFeature": {
"title": "Mein Feature"
}
}- Denselben Schlüssel zu
src/translations/en.jsonhinzufügen:
{
"myFeature": {
"title": "My Feature"
}
}- In HTML verwenden:
<h1 data-i18n="myFeature.title">My Feature</h1>- Überprüfen:
make test- Frontend: Vanilla JavaScript (keine Frameworks!)
- Styling: Reines CSS (keine Präprozessoren)
- Backend: PHP (Voting-System + Content-Verwaltung)
- PWA: Service Worker, Web App Manifest
- Build: Node.js im Multi-Stage Docker-Build (Cache-Busting, Icon-Generierung)
- Testing: Playwright (Cross-Browser)
- Server: nginx + PHP-FPM (Docker, Dev + Prod)
3-Schichten-Caching-System:
- Service Worker Cache — Network-first für JSON-Daten (damit Admin-Änderungen sofort sichtbar werden), Cache-first für statische Assets (HTML, CSS, JS, Bilder)
- localStorage Cache — reiner Offline-Fallback für JSON-Daten (wird bei jedem erfolgreichen Fetch aktualisiert, aber nicht als Primärquelle verwendet)
- Cache Busting — MD5-gehashte Dateinamen, zur Laufzeit aufgelöst via
assets-hashes.json(Code-Assets, Image-Build) undcontent/content-hashes.json(Content-Volume, Runtime-Rehash) — ermöglicht Rehashing ohne Rebuild nach Admin-Edits
Beispiel:
app.css→app.b417865e.cssmenu.json→menu.ec9daa78.jsonheader.js→header.e3796179.js
Hash-Updates erfolgen beim Build und zur Laufzeit (nach Admin-Edits via rehash.php).
- ✅ Installierbar - Zum Startbildschirm hinzufügen (Android, iOS, Desktop)
- ✅ Offline-First - Funktioniert ohne Internet
- ✅ Schnell - Gecachte Assets, sofortiges Laden
- ✅ Responsiv - Mobil, Tablet, Desktop
- ✅ Sicher - HTTPS erforderlich
- ✅ Auto-Updates - Service Worker Updates
Dieses Projekt ist lizenziert unter der GNU Affero General Public License v3.0 (AGPL-3.0).
- ✅ Freie Nutzung - Sie können die Software frei verwenden, modifizieren und verteilen
- ✅ Open Source - Der Quellcode bleibt immer verfügbar
- ✅ SaaS-geschützt - Auch bei Web-Service-Betrieb muss der Code offengelegt werden
⚠️ Copyleft - Änderungen müssen unter derselben Lizenz veröffentlicht werden⚠️ Netzwerk-Nutzung - Bei SaaS-Betrieb muss ein Link zum Quellcode bereitgestellt werden
Für Closed-Source oder proprietäre Nutzung ist eine separate kommerzielle Lizenz erforderlich.
Siehe LICENSE für den vollständigen Lizenztext.
Mit ❤️ entwickelt für das DevOps Camp.
© Proud Commerce | 2025