AppHeader.tsx: Hamburger-Menü unterhalb "sm" mit denselben Links gestapelt (Vorschläge-Badge-Logik in gemeinsame renderLinks()-Funktion ausgelagert), Panel schließt automatisch bei Navigation. Bisherige einzeilige Navigation bleibt ab "sm" unverändert. WeekEditor.tsx: Mobile Tag-für-Tag-Ansicht mit Tab-Leiste (Mo-Fr) statt der 6-spaltigen Tabelle, die bei 375px unbenutzbar gequetscht war. Zellinhalt (Eintragsliste + "hinzufügen"-Flow) in renderCell() extrahiert und von Tabelle und Karten-Ansicht gemeinsam genutzt, damit der Bearbeitungs-Zustand nicht dupliziert werden muss. Desktop-Tabelle unverändert ab "sm". DishManager/CodeListManager/UserManager/SectionManager/PasskeyManager: Kopf- und Listenzeilen brechen jetzt bei Bedarf um (flex-wrap) statt Buttons abzuschneiden, gleiches Muster wie bereits in SuggestionReview.tsx vorhanden. Version 1.9.5. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Canteen Menu – Speisekarte für die Betriebskantine
Digitale Speisekarte für eine Firmenkantine (ca. 250 Mitarbeiter) mit öffentlicher Wochenansicht, Kantinen-Bereich zur Pflege der Gerichte und Admin-Bereich für Benutzerverwaltung und Branding.
Stack: Next.js 16 (App Router, TypeScript, Tailwind CSS) · selbst
gehostetes PostgreSQL (via pg, eigene Migrationen) · eigene Session-Auth
(Passwort via node:crypto scrypt, Passkeys via @simplewebauthn) ·
DeepL als einzige externe API (optional). Läuft vollständig selbst
gehostet, keine Cloud-Abhängigkeit außer optional DeepL.
Änderungen je Version stehen in CHANGELOG.md, Git-Tags
markieren die jeweiligen Versionsstände (git tag). Schritt-für-Schritt-
Anleitung für die Inbetriebnahme auf einem neuen Server:
DEPLOYMENT.md.
Funktionen
| Bereich | Funktionen |
|---|---|
| Öffentlich (ohne Login) | Wochenplan Mo–Fr mit frei konfigurierbaren Essenszeiten; Navigation vorherige/nächste Woche/Heute; auf schmalen Bildschirmen umschaltbare Tages-/Wochenansicht (Tag ist Standard); Logo zentriert über der Karte; Allergen-/Zusatzstoff-Kürzel je Gericht, Legende unterhalb der Karte; Sprachumschalter Deutsch/Englisch/Polnisch; Rezeptvorschläge einreichen; RSS-Feed (/feed.xml, Icon im Kopfbereich) mit einem Eintrag pro Tag zum Abonnieren |
Kantinen-Team (/intern) |
Essenszeiten anlegen/bearbeiten/löschen/neu anordnen; Gerichte anlegen/bearbeiten/löschen (inkl. Live-Suche, Mehrfachauswahl zum Batch-Löschen, englischem/polnischem Namen mit automatischer DeepL-Übersetzung) mit Allergenen/Zusatzstoffen je Gericht; Gerichte per Suche Tagen/Essenszeiten zuweisen (mehrere pro Zelle) und wieder entfernen; globale Allergen-/Zusatzstofflisten pflegen (ebenfalls mehrsprachig); eingereichte Rezeptvorschläge sichten, übernehmen oder ablehnen (Anzahl offener Vorschläge als Badge in der Navigation); eigenes Konto verwalten (Name, E-Mail, Passwort, Passkeys, Passwort-Anmeldung an/aus) |
Admin (/admin) |
Kantinenmitarbeiter anlegen/bearbeiten/löschen (inkl. Passwort-Reset mit Wiederholung und Rollenvergabe); Farbgebung der Speisekarte anpassen; Logo und Favicon hochladen/entfernen |
Anmeldung mit E-Mail + Passwort oder alternativ per Passkey (Registrierung im eigenen Konto nach dem ersten Login). Die Passwort-Anmeldung kann pro Konto deaktiviert werden, sobald mindestens ein Passkey registriert ist (serverseitig erzwungen).
Die gesamte Oberfläche ist auf Deutsch, Englisch und Polnisch verfügbar
(Sprachumschalter, Cookie-basiert). Gerichte, Essenszeiten, Allergene und
Zusatzstoffe lassen sich zusätzlich auf Englisch/Polnisch benennen –
mit optionaler automatischer Übersetzung über DeepL (siehe
DEEPL_API_KEY unten) und jederzeit manuell korrigierbar.
Projektstruktur
app/ Next.js-Routen
page.tsx Öffentliche Speisekarte
feed.xml/ RSS-Feed des Speiseplans (öffentlich)
login/ Login (Passwort + Passkey)
vorschlag/ Öffentliches Rezeptvorschlag-Formular
intern/ Kantinen-Bereich (Rolle: staff oder admin)
vorschlaege/ Rezeptvorschläge sichten/übernehmen
admin/ Verwaltung (Rolle: admin)
branding/[filename]/ Logo/Favicon ausliefern (öffentlich)
api/auth/ Login, Logout, Passkeys
api/account/ Eigenes Konto (Name, E-Mail, Passwort, Toggle)
api/admin/users/ Benutzerverwaltung (nur Admin)
api/admin/branding/ Logo-/Favicon-Upload (nur Admin)
api/dishes/, api/sections/, api/allergens/, api/additives/,
api/menu-entries/, api/dish-suggestions/, api/settings/
CRUD-Routen für den Kantinen-/Admin-Bereich
api/translate/ DeepL-Anbindung (serverseitig)
api/locale/ Sprach-Cookie setzen
components/ Client-Komponenten (WeekEditor, DishManager, …)
components/i18n/ Sprachkontext, Sprachumschalter
lib/i18n/ Wörterbücher (de/en/pl), Locale-Helfer
lib/auth/ Sessions, Passwort-Hashing, WebAuthn-Konfiguration
lib/db/ pg-Pool, Query-Helfer, gemeinsame Ladefunktionen
lib/branding.ts Lokale Datei-Ablage für Logo/Favicon
lib/ Typen, Datums-/Formatierungshelfer
proxy.ts Session-Prüfung + Schutz von /intern, /admin, /konto
db/migrations/ Datenbankschema (reines SQL, nummeriert)
db/migrate.mjs Wendet neue Migrationen an (idempotent, bei jedem Start)
scripts/bootstrap-admin.mjs
Legt bei leerer Datenbank automatisch einen
Admin mit Zufallspasswort an (siehe ZUGANGSDATEN.md)
Entwicklung
Voraussetzung in beiden Varianten: .env.local mit den Werten aus
.env.example (liegt lokal bereits vor und ist per .gitignore vom
Repo ausgeschlossen).
Variante A – Docker (nichts vorinstallieren außer Docker):
docker compose --env-file .env.local --profile dev up dev
# http://localhost:3000 (Hot-Reload, Quellcode wird gemountet)
Variante B – lokal installiertes Node.js + PostgreSQL:
npm install
npm run dev # http://localhost:3000 (bindet an 0.0.0.0, auch im LAN erreichbar)
In beiden Varianten wird die Datenbank beim Start automatisch auf den neuesten Stand migriert und – falls noch kein Benutzer existiert – ein Admin-Konto mit Zufallspasswort angelegt (siehe Zugangsdaten).
Umgebungsvariablen (Live-Betrieb)
| Variable | Bedeutung | Geheim? |
|---|---|---|
DATABASE_URL |
Verbindung zur PostgreSQL-Datenbank, z. B. postgres://postgres:<passwort>@db:5432/canteen_menu |
ja |
APP_URL |
Öffentliche URL der App, z. B. https://menu.example.de (Origin-Check hinter Reverse-Proxy; zugleich Passkey-RP-Domain – wird direkt daraus abgeleitet, kein separater Konfigurationsschritt nötig) |
nein |
SITE_TITLE |
Titel im Browser-Tab und Überschrift ohne Logo, Default Canteen Menu |
nein |
ADMIN_EMAIL |
Optional: E-Mail des automatisch angelegten Admin-Erstnutzers (nur relevant, solange die Datenbank leer ist). Ohne Angabe: admin@<Hostname aus APP_URL> |
nein |
DEEPL_API_KEY |
Optional: aktiviert „Automatisch übersetzen“ für Gerichte/Allergene/Zusatzstoffe (DeepL, kostenloser Free-Tier). Ohne Key funktioniert alles weiter, nur automatisch übersetzen ist deaktiviert | ja – niemals im Browser/Repo! |
Build & Start auf dem Server – mit Docker (empfohlen, Node.js und PostgreSQL müssen nicht installiert sein):
docker compose --env-file .env.local up -d --build
# App: Port 3000 (hinter Reverse-Proxy mit HTTPS betreiben)
# PostgreSQL: kein Host-Port, nur intern im Compose-Netzwerk erreichbar
Vor dem ersten Start: eine leere ZUGANGSDATEN.md im Projektverzeichnis
anlegen (touch ZUGANGSDATEN.md), da diese Datei per Bind-Mount in den
Container eingebunden wird und die automatisch generierten Admin-
Zugangsdaten dort landen.
Oder klassisch ohne Docker (mit lokal erreichbarem PostgreSQL):
npm ci
npm run build
npm run start # Port 3000, bindet an 0.0.0.0 (hinter Reverse-Proxy betreiben)
Passkeys sind an die Domain gebunden (RP-ID = Hostname aus APP_URL).
Vor dem Livegang APP_URL auf die öffentliche URL setzen – ein
zusätzlicher Konfigurationsschritt ist nicht nötig. Bereits registrierte
Passkeys einer vorherigen Domain verlieren dabei ihre Gültigkeit und
müssen neu registriert werden.
Datenbank
Reines, selbst gehostetes PostgreSQL (keine Row-Level-Security-Policies) –
alle Zugriffsrechte werden in den Route-Handlern geprüft (lib/auth/session.ts,
requireRole).
- Schema:
db/migrations/*.sql, nummeriert, wird vondb/migrate.mjsautomatisch angewendet (idempotent, bei jedem Start des Servers bzw. Docker-Containers). - Neue Migration hinzufügen:
db/migrations/000N_beschreibung.sqlanlegen, wird beim nächsten Start automatisch eingespielt. - Manuell migrieren:
npm run db:migrate
Rechte-Modell (in Anwendungscode statt Datenbank-RLS):
- Öffentlich lesbar (kein Login nötig): Speisekarte, Essenszeiten, Allergene, Zusatzstoffe, Logo/Favicon.
staff/adminpflegen Gerichte, Essenszeiten, Wochenplan und Kennzeichnungen.- Nur
adminändert Einstellungen (Farben/Logo) und verwaltet Benutzer. - Logo/Favicon liegen als Dateien in
BRANDING_DIR(Docker-Volumebranding_data), öffentlich ausgeliefert über/branding/[filename].
Zugangsdaten
Beim allerersten Start mit leerer Datenbank legt scripts/bootstrap-admin.mjs
automatisch einen Admin-Benutzer mit zufällig generiertem Passwort an und
schreibt die Zugangsdaten nach ZUGANGSDATEN.md – diese Datei ist bewusst
nicht im Repository (.gitignore). Nach dem ersten Login bitte das
Startpasswort ändern.