Cocktail-Bestellsystem für eine Party. Gäste bestellen per Handy, der Barkeeper sieht Bestellungen live.
pnpm install
pnpm prisma migrate dev
DATABASE_URL="file:./data/dev.db" pnpm tsx prisma/seed.ts
pnpm devÖffne http://localhost:3000
Kopiere .env.example nach .env und passe die Werte an:
cp .env.example .envopenssl rand -hex 32Den generierten Wert in .env als SESSION_SECRET eintragen.
BAR_PIN=1234 # PIN für Barkeeper-Zugang (/bar)
ADMIN_PIN=9999 # PIN für Admin-Zugang (/admin) – auch für /bar nutzbarWichtig: Beide PINs vor der Party auf sichere Werte ändern!
# 1. .env mit echten Werten anlegen (SESSION_SECRET, BAR_PIN, ADMIN_PIN)
mkdir -p ./data/images
# 2. Starten
docker compose up -d --builddocker compose up schlägt mit klarer Fehlermeldung fehl, wenn SESSION_SECRET nicht gesetzt ist.
Seed-Daten laden (einmalig nach erstem Start):
docker compose exec app node_modules/.bin/tsx prisma/seed.tsFür Portainer-Stacks wird jetzt standardmäßig ein named volume (bar_data) verwendet.
Dadurch funktioniert der Deploy ohne host-spezifische Pfade.
- Stack aus diesem Repository deployen (Compose + Dockerfile).
- In Portainer unter Environment variables mindestens setzen:
SESSION_SECRET(Pflicht, z. B.openssl rand -hex 32)BAR_PINADMIN_PIN
- Optional
HOST_PORTsetzen (Standard3000).
Wenn SESSION_SECRET fehlt, bricht der Deploy jetzt direkt mit klarer Meldung ab
(Compose-Interpolation), statt erst später im Container.
Für persistente Daten nutzt der Stack das Volume bar_data
(app.db + images).
Die App läuft als reiner HTTP-Container. TLS wird durch den vorgelagerten Proxy (z. B. Nginx auf dem Host) terminiert. Caddy wird nicht verwendet.
| Variable | Standard | Bedeutung |
|---|---|---|
HOST_PORT |
3000 |
Host-Port, auf dem der Container erreichbar ist |
TRUST_PROXY |
false |
Auf true setzen, wenn HTTPS durch Nginx terminiert wird – setzt Secure-Flag auf Session-Cookies |
Beispiel .env für Nginx-Deployment:
SESSION_SECRET=<openssl rand -hex 32>
BAR_PIN=1234
ADMIN_PIN=9999
HOST_PORT=3000
TRUST_PROXY=trueSSE-kompatible Konfiguration (wichtig für Live-Updates):
server {
listen 443 ssl;
server_name bar.example.com;
ssl_certificate /etc/ssl/certs/bar.crt;
ssl_certificate_key /etc/ssl/private/bar.key;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE / long-polling: Puffern deaktivieren
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_connect_timeout 10s;
proxy_send_timeout 3600s;
# Chunked Transfer beibehalten
chunked_transfer_encoding on;
}
}Beide Verzeichnisse sichern:
# Datenbank
cp ./data/app.db ./backups/app-$(date +%Y%m%d-%H%M).db
# Bilder
tar -czf ./backups/images-$(date +%Y%m%d-%H%M).tar.gz ./data/images/Bilder werden beim Löschen/Ersetzen automatisch entfernt. Wurde ein Upload gestartet,
aber der Cocktail nie gespeichert, bleibt ein verwaistes .webp in /data/images liegen.
Das ist für eine Party unkritisch – bei Bedarf manuell bereinigen:
# Liste der referenzierten Dateinamen aus der DB:
sqlite3 ./data/app.db "SELECT imageFilename FROM Cocktail WHERE imageFilename IS NOT NULL;"
# Dann mit ls /data/images/ vergleichencurl http://localhost/api/healthÜber den Status-Button oben rechts im Bar-Dashboard (Aktiv / Pausiert)
kann die Bestellannahme global gestoppt werden (Pinkelpause,
Tortenanschnitt). Optional mit Nachricht und Auto-Resume nach
5/10/15/30 Min. Gäste sehen sofort (live via SSE) einen Banner; der
Bestell-Button ist gesperrt. Beim Fortsetzen verschwindet der Banner mit
grüner Bestätigung.
Im Bar-Dashboard öffnet der Button „Verfügbarkeit“ einen Drawer mit allen Cocktails und einem Toggle pro Drink. Umschalten wirkt sofort (live) auf der Gäste-Seite – Karten werden ausgegraut, Warenkorb-Items markiert.
/bar/stats (Bar+Admin): Live-Auswertung der Partynacht – KPIs,
Bestellungen pro Zeit, Top-Cocktails, Gäste-Bestenliste,
Wartezeit-Verteilung, Stornos. Zeitraum-Filter (Heute / Letzte Stunde /
Letzten 15 Min / Alle). Aktualisiert sich live (debounced).
/screen (ohne Login, für Beamer/TV): Fullscreen-Bestenliste + Live-Ticker
der letzten Bestellungen. Zeigt bewusst nur Vornamen und Zahlen, keine
Notizen/Tags.
/admin/export (Admin): CSV (Bestellungen, Items, Events, Cocktails –
UTF-8 BOM, ;-Separator für deutsches Excel), JSON-Dump der ganzen DB,
und ein komplettes ZIP-Backup (dump.json + konsistente
db/app.db-Kopie + alle Bilder).
Empfehlung: Backup vor der Party (leerer Stand), einmal mittendrin und nach der Party ziehen.
Stündliches Cron auf dem Host (Header-Secret in ENV setzen und in einer kleinen Auth-Erweiterung prüfen):
0 * * * * curl -sS -H "X-Backup-Secret: $BACKUP_SECRET" \
https://bar.franzi.app/api/admin/export/backup.zip \
-o /backups/bar-$(date +\%Y\%m\%d-\%H\%M).zip- PINs in
.envändern (BAR_PIN,ADMIN_PIN) SESSION_SECRETgenerieren (openssl rand -hex 32) – App startet in Production ohne nicht- DNS (
bar.franzi.app) auf Server-IP zeigen lassen docker compose up -d --build- Cocktails im Admin (
/admin) anlegen - Test-Bestellung machen (
/→/cart→/status/[id]) - Backup ziehen (
/admin/export) - Sound an der Bar testen (
/bar, Sound-Toggle) /screenauf Beamer/TV öffnen
| Route | Beschreibung | Auth |
|---|---|---|
/ |
Cocktailkarte für Gäste | – |
/cart |
Warenkorb + Bestellung | – |
/status/[id] |
Bestellstatus | – |
/bar |
Bar-Dashboard (Kanban) | Bar-PIN |
/bar/stats |
Statistik-Dashboard | Bar-PIN |
/bar/login |
Bar-Anmeldung | – |
/screen |
Party-Screen (Beamer) | – |
/admin |
Cocktail-Verwaltung | Admin-PIN |
/admin/export |
Export & Backup | Admin-PIN |
/admin/cocktails/new |
Neuer Cocktail | Admin-PIN |
/admin/cocktails/[id] |
Cocktail bearbeiten | Admin-PIN |
/api/cocktails |
Verfügbare Cocktails (GET) | – |
/api/orders |
Bestellung erstellen (POST) | – |
/api/bar-state |
Pause-Status (GET) | – |
/api/guest/stream |
Gäste-SSE (Pause/Verfügbarkeit) | – |
/api/bar/pause · /api/bar/resume |
Pause steuern (POST) | Bar-PIN |
/api/bar/stats |
Aggregierte Statistik (GET) | Bar-PIN |
/api/screen · /api/screen/stream |
Party-Screen-Daten | – |
/api/admin/export/* |
CSV/JSON/ZIP-Export | Admin-PIN |
/api/auth/login |
Anmelden (POST) | – |
/api/auth/logout |
Abmelden (POST) | – |
/api/health |
Healthcheck (GET) | – |
Falls alte Datenbankeinträge noch englische/kleingeschriebene Kategorien enthalten, führe nach dem Deployment einmalig aus:
docker compose exec app tsx scripts/fix-categories.ts