Gerichtnamen, Essenszeiten und Allergen-/Zusatzstoff-Bezeichnungen sind
jetzt genauso dynamisch mehrsprachig wie die Bedienoberfläche aus
v1.10.0, statt fest auf Englisch/Polnisch begrenzt zu sein. Neue
Übersetzungstabellen ersetzen die alten name_en/name_pl-Spalten,
DeepL-Auto-Übersetzung und CSV-Import/-Export unterstützen jetzt
beliebig viele aktive Sprachen.
Teil 1 des "mehrsprachig variabel"-Umbaus (Speiseplan-Inhalte folgen
in einer späteren Version). Sprachen sind jetzt zur Laufzeit über eine
neue Admin-Seite "Sprachen" verwaltbar statt fest im Code zu stehen:
- Neue Tabellen languages/ui_translations (Migration 0004), bestehende
Englisch-/Polnisch-Texte per generiertem Seed automatisch übernommen
(Migration 0005) - keine manuelle Nacharbeit, nichts geht verloren.
- lib/i18n/flatten.ts: verschachtelte Dict-Objekte (inkl. Arrays wie
weekdays.long) <-> flache Schlüssel-Wert-Paare, Basis für Vorlage-
Export/-Import.
- lib/i18n/template.ts: Vollständigkeits-/Veraltet-Berechnung (Hash
über die DE-Schlüsselliste) und Import-Validierung (Platzhalter wie
{message} müssen erhalten bleiben, unbekannte Schlüssel werden
ignoriert statt Fehler zu werfen).
- getDictionary()/getLocale() (lib/i18n/index.ts) jetzt asynchron und
DB-gestützt für Nicht-Basis-Sprachen, mit Fallback auf Deutsch für
fehlende Schlüssel. Locale-Typ bewusst von festem TypeScript-Tupel
auf string generalisiert (Kompromiss: Compiler prüft Vollständigkeit
nicht mehr automatisch, das übernimmt jetzt die Veraltet-Erkennung
zur Laufzeit).
- LanguageSwitcher/LocaleProvider lesen aktive Sprachen jetzt aus dem
Context statt aus einer festen Konstante; blendet sich bei nur einer
aktiven Sprache automatisch aus.
- Neue Admin-Routen app/api/admin/languages/* (Liste inkl.
Vollständigkeit, Anlegen, Aktivieren/Deaktivieren mit
"mindestens eine muss aktiv bleiben"-Schutz, Löschen außer
Basis-Sprache, Vorlage-Download, Datei-Import) + LanguageManager.tsx.
Vollständig gegen echten DB-Zustand getestet: Vorlage-Export/-Import
inkl. Platzhalter-Validierung, neue Sprache anlegen, auf eine Sprache
reduzieren (Umschalter verschwindet, "letzte Sprache"-Schutz greift),
Veraltet-Erkennung nach simuliertem neuen Schlüssel, Cascade-Delete.
Version 1.10.0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dritte Login-Methode neben Passwort und Passkey, konfiguriert über
OIDC_ISSUER/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET (optional, Button bleibt
ohne diese Vars ausgeblendet). Authorization-Code-Flow mit PKCE,
ID-Token-Signaturprüfung gegen das JWKS des Providers via neuer
Abhängigkeit "jose" (RS256 fest gepinnt gegen Algorithm-Confusion),
State/Nonce kurzlebig und einmalig verwendbar in neuer Tabelle
oidc_states (Migration 0003, analog zu webauthn_challenges).
Funktioniert ausschließlich für bereits angelegte Benutzer (Abgleich
per E-Mail) – kein Auto-Provisioning, keine Rollenübernahme aus
Claims, beides bleibt exklusiv über die bestehende Benutzerverwaltung
steuerbar.
app/login/page.tsx zu Server-Component-Wrapper reduziert (prüft
isConfigured(), liest ?error= aus searchParams), bisheriger Inhalt
nach components/LoginForm.tsx verschoben.
Version 1.9.0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neue Migration 0002_labeling_notice.sql: settings.labeling_mode
("detailed"/"notice") + labeling_notice(_en/_pl). Auf der Seite
"Allergene & Zusatzstoffe" (LabelingNoticeEditor) kann zwischen der
detaillierten Auswahl je Gericht und einem editierbaren Hinweistext
umgeschaltet werden. Im Hinweismodus entfällt auf der öffentlichen
Speisekarte (Desktop + Mobile) die Legende und die Kürzel an den
Gerichten, in der Gerichte-Verwaltung wird die Chip-Auswahl beim
Bearbeiten ausgeblendet. Umschalten/Bearbeiten ist für staff und admin
freigegeben, Farben in /api/settings bleiben admin-only.
Version 1.8.0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CSV-Import/-Export (Gerichte, Allergene, Zusatzstoffe): neue Export-/
Import-Routen unter app/api/*/export und */import, hand-geschriebener
CSV-Parser/-Serializer (lib/csv.ts, keine neue Abhängigkeit), Upsert
über Kürzel (Allergene/Zusatzstoffe) bzw. exakten Namen (Gerichte,
mit Ambiguitäts-Check bei Namensdopplung). UI-Buttons in DishManager
und CodeListManager.
RSS-Feed: <content:encoded> zusätzlich zu <description> ausgeben,
da mehrere Feed-Reader description als kürzbare Vorschau behandeln
und dadurch bei mehreren Essenszeiten pro Tag nur die erste zeigten.
Version 1.7.0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
app/feed.xml: RSS-2.0-Feed mit einem Eintrag pro Tag (aktuelle +
nächste Woche), Gerichte je Essenszeit im CDATA-Beschreibungstext.
Immer auf Deutsch, da Feedreader kein Sprach-Cookie mitschicken.
Autodiscovery-Link im <head> (app/layout.tsx) sowie sichtbares
RSS-Icon im Header der öffentlichen Speisekarte (neben Sprachumschalter/
Anmelden – die einzige Stelle, die auf allen Bildschirmgrößen sichtbar
ist).
AppHeader: "Vorschläge"-Navigationspunkt zeigt jetzt die Anzahl
offener Rezeptvorschläge als Zahl im roten Kreis (GET
/api/dish-suggestions, bereits staff/admin-beschränkt), aktualisiert
sich bei Navigation innerhalb des internen Bereichs.
Der Branding-Upload akzeptierte bisher jeden Dateityp, der clientseitig
mit "image/" beginnt, inkl. SVG. Anders als beim vorherigen Supabase-
Storage-Bucket (andere Origin) wird die Datei jetzt auf der eigenen
Origin unter /branding/[filename] ausgeliefert – eine SVG mit
eingebettetem <script> hätte bei direktem Aufruf der Datei-URL
(z. B. "Bild in neuem Tab öffnen") mit dem Session-Cookie eines
anderen Admins ausgeführt werden können und wäre dabei auch am
CSRF-Schutz vorbeigekommen (Origin stimmt ja).
Upload-Route erlaubt jetzt nur noch eine Whitelist an Rasterformaten
(png/jpg/webp/gif/ico), die Dateiendung wird aus dem geprüften
MIME-Typ abgeleitet statt aus dem vom Client gesendeten Dateinamen.
Die Ausliefer-Route kennt "svg" als Content-Type nicht mehr (Defense
in Depth). Gefunden bei der Sicherheitsprüfung vor dem Merge nach
master.
Fundament: db/migrations/0001_init.sql (users/sessions/passkey_credentials/
webauthn_challenges + Inhaltstabellen ohne RLS), db/migrate.mjs
(idempotenter Migrationsrunner), lib/db (pg-Pool + query-Wrapper),
lib/auth/password.ts (Node-crypto.scrypt, keine neue Abhängigkeit),
lib/auth/session.ts (opake DB-Session-Tokens, Sliding-Refresh in proxy.ts).
Passkeys: @simplewebauthn/server + /browser ersetzen Supabase's
experimentelles WebAuthn-API (dafür gab es bisher kein eigenes
Credential-Schema); RP-Konfiguration kommt jetzt direkt aus APP_URL statt
über scripts/configure-passkeys.mjs und die Supabase-Management-API.
scripts/bootstrap-admin.mjs legt bei leerer users-Tabelle automatisch
einen Admin mit zufälligem Passwort an und hängt die Zugangsdaten an
ZUGANGSDATEN.md an (idempotent, via predev/prestart-Hooks aufgerufen).
Bugfix im Zuge des Umbaus: "mind. 1 Passkey nötig, um Passwort-Login zu
deaktivieren" wurde bisher nur clientseitig geprüft – jetzt zusätzlich
serverseitig in PATCH /api/account.
E-Mail-Änderung im eigenen Konto läuft jetzt direkt nach
Passwort-Bestätigung statt über eine Supabase-Bestätigungsmail (kein
SMTP-Server vorhanden).
Rest der Anwendung (Gerichte/Essenszeiten/Kennzeichnung/Vorschläge/
Benutzerverwaltung/Branding) nutzt noch den alten Supabase-Client –
folgt in weiteren Commits.
Der Kopfbereich der öffentlichen Speisekarte zeigte seinen eigenen
LanguageSwitcher auch auf schmalen Bildschirmen an, obwohl
PublicMobileMenu dort bereits einen eigenen rendert. Der Header-Switcher
ist jetzt wie der Rest des Desktop-Layouts erst ab dem sm-Breakpoint
sichtbar.
Betrifft UI-Texte (DE/EN/PL), Route (/intern/bereiche ->
/intern/essenszeiten), Code-Kommentare, Changelog und README.
Fachlich unverändert (weiterhin die Tabelle/Komponente "sections"/
SectionManager im Code), da "Essenszeiten" den Zeitbezug des Features
klarer ausdrückt als der bisherige Sammelbegriff.
Statt den Button immer anzuzeigen und erst nach Klick auf die
fehlende Konfiguration hinzuweisen, wird er serverseitig anhand von
process.env.DEEPL_API_KEY ein-/ausgeblendet (Gerichte, Allergene,
Zusatzstoffe). Die dadurch überflüssige "translateUnavailable"-
Meldung wurde aus allen drei Sprachen entfernt.
Migration 0006: sections.key bekommt einen automatisch generierten
Schlüssel (wie dishes.id), menu_entries.section_key kaskadiert beim
Löschen eines Bereichs, neue RLS-Policy erlaubt Kantinenmitarbeitern/
Admins Schreibzugriff auf sections.
Neue Seite „Bereiche" im Kantinen-Bereich (SectionManager): Bereiche
anlegen, umbenennen (inkl. EN/PL), Uhrzeit setzen, löschen und per
Pfeil-Buttons neu anordnen.
Nicht angemeldete Besucher können unter /vorschlag ein fehlendes
Gericht vorschlagen (Name + optionale Beschreibung); Dublettenprüfung
gegen bestehende Gerichte, einfacher Honeypot gegen Bot-Spam. Staff/
Admin sichten eingereichte Vorschläge unter /intern/vorschlaege und
übernehmen sie als Gericht oder lehnen sie ab.
Alle Seiten und Komponenten (öffentliche Speisekarte, Login, Kantinen-
und Admin-Bereich, eigenes Konto) verwenden jetzt die Wörterbücher aus
lib/i18n statt fest codierter deutscher Texte. Gerichte/Bereiche/
Allergene/Zusatzstoffe zeigen den Namen zur aktuell gewählten Sprache;
Gerichte- und Kennzeichnungs-Formulare bekommen Englisch-/Polnisch-
Felder mit Übersetzen-Button (siehe vorherigen Commit). Datumsformat-
ierung (formatDateLong) nimmt jetzt ein locale-Argument statt fix
'de-DE'.
app/api/translate ruft DeepL für EN+PL auf und schreibt das Ergebnis
in die jeweilige _en/_pl-Spalte (Staff/Admin-only, Service-Role-Key).
Ohne DEEPL_API_KEY (neue Docker-Variable) antwortet die Route mit
configured:false, das Frontend zeigt dann den Hinweis zur manuellen
Eingabe statt eines Fehlers.
Wörterbücher unter lib/i18n statt next-intl (Next.js 16 ist neu genug,
dass eine externe Routing-Middleware zusätzliches Risiko bedeutet
hätte); Sprache liegt in einem Cookie, kein URL-Präfix nötig, dadurch
bleibt proxy.ts unverändert. LocaleProvider stellt Wörterbuch + Sprache
per Context für Client-Komponenten bereit (lib/i18n/shared.ts ist
bewusst getrennt von lib/i18n/index.ts, damit next/headers nicht in
den Client-Bundle gelangt).
Absolute Positionierung im Header kollidierte mit Logos in breitem
Seitenverhältnis (z. B. Banner-Format), da das Logo bei h-20 nahezu
bildschirmbreit gerendert wird. Anmelden-Link steht jetzt auf eigener
Zeile über dem Logo statt absolut positioniert in der oberen Ecke.
- SITE_TITLE (Default "Canteen Menu") steuert Browser-Tab-Titel und
H1-Fallback ohne Logo
- Favicon-Upload analog zum Logo über den Design-Bereich (Storage-
Bucket branding); generateMetadata setzt icons.icon jetzt immer
explizit, da Next.js sonst app/favicon.ico zusätzlich einbindet –
die Datei liegt daher als expliziter Fallback unter public/
Schalter im eigenen Konto, erzwingt vorher mindestens einen
registrierten Passkey (sonst Aussperrung). Durchsetzung beim Login:
nach erfolgreicher Passwort-Prüfung wird bei deaktiviertem Flag sofort
wieder abgemeldet; Passkey-Anmeldung bleibt davon unberührt.
Chip-Auswahl im Gericht-Formular (Klick zum An-/Abwählen) statt
Checkbox-Liste; Kürzel erscheinen hochgestellt neben dem Gerichtnamen
in Speisekarte, mobiler Ansicht und Wochenplan-Editor. Editor bekommt
außerdem einen Heute-Button.
- "Heute" springt direkt zur aktuellen Woche
- Unter dem sm-Breakpoint umschaltbar zwischen Tag (Standard) und
Woche statt der Tabelle; Desktop bleibt vollständig serverseitig
gerendert, nur der mobile Umschalter ist eine Client-Komponente
Nach dem Abmelden landete man auf 0.0.0.0:3000, weil die absolute
Redirect-URL aus request.url gebaut wurde und dort seit der Bindung an
0.0.0.0 die Server-Adresse statt des aufgerufenen Hosts steht.
Logout leitet jetzt relativ weiter; der Proxy baut die URL aus dem
Host-Header des Browsers (inkl. x-forwarded-proto hinter Proxies).
Eine Leiste für alles: Gruppe "Speiseplan" (Wochenplan, Gerichte,
Allergene & Zusatzstoffe) und für Admins zusätzlich Gruppe
"Verwaltung" (Benutzer, Design). Aktiver Menüpunkt wird hervorgehoben,
die Konto-Seite nutzt denselben Header – kein Hin- und Herspringen
mehr zwischen Kantinen-Bereich und Verwaltung.
- next dev/start binden jetzt explizit an 0.0.0.0 statt nur localhost
(Docker war über die Port-Freigabe bereits erreichbar, jetzt auch
beim nativen npm-Betrieb); docker-compose.yml macht das Binden an
alle Interfaces zusätzlich explizit
- package.json-Name, Docker-Image/Container/Netzwerk-Namen, lokaler
DB-Name (canteen_menu), Browser-Tab-Titel und README auf den
englischen Projektnamen "canteen-menu" umgestellt; deutsche
In-App-Texte für die Kantinen-Mitarbeiter bleiben unverändert
- Supabase-Projekt (Anzeigename) und native lokale Postgres-DB
entsprechend umbenannt (Ref/URL/Keys unverändert)
- Klick auf den Benutzernamen im Header (Kantinen- und Admin-Bereich)
öffnet /konto
- Name-Änderung per RLS-Policy mit spaltenscharfem Grant (nur
full_name), Rolle bleibt ausschließlich über die Admin-API änderbar
- E-Mail-Änderung über supabase.auth.updateUser mit Bestätigungslink;
Trigger synchronisiert profiles.email nach Bestätigung
- Passwortänderung verlangt zur Verifizierung das aktuelle Passwort
(Re-Login-Check) plus Wiederholung des neuen Passworts
- Passkey-Verwaltung aus /intern/konto hierher verschoben
- X-Frame-Options/CSP frame-ancestors gegen Clickjacking,
nosniff, Referrer-Policy, Permissions-Policy
- Origin-Check als CSRF-Defense-in-Depth für /api/admin/users
und /logout (zusätzlich zu SameSite=Lax-Cookies und JSON-Content-Type)