Feature spec, system design, design system (colors/typography/components), and per-view HTML specs for Erbstücke Wannsee. Also includes Claude personas used during design sessions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.3 KiB
Erbstücke-Reservierung — System Design
Datum: 2026-05-04 Status: Freigegeben zur Implementierung Referenz: 2026-05-04-erbstuecke-reservierung-design.md
1. Stack
| Schicht | Technologie | Begründung |
|---|---|---|
| Framework | SvelteKit (Node adapter) | SSR + server actions in einem Prozess, kein separates Backend nötig |
| Datenbank | SQLite via better-sqlite3 |
Zero-Ops, kein DB-Container, WAL-Modus für atomare Reservierungen |
| Foto-Verarbeitung | sharp |
Resize + WebP-Konvertierung serverseitig bei Upload |
| Passwort-Hashing | bcryptjs |
Admin-Authentifizierung |
| Deployment | Docker (single container) + Caddy (Host) | Root-Server, Subdomain, TLS via Caddy |
2. Deployment
# docker-compose.yml
services:
app:
build: .
volumes:
- db:/app/db
- uploads:/app/uploads
environment:
DATABASE_PATH: /app/db/erbstuecke.db
UPLOAD_DIR: /app/uploads
SESSION_SECRET: <random-string>
ADMIN_MARCEL_PASSWORD_HASH: <bcrypt-hash>
ADMIN_RENATE_PASSWORD_HASH: <bcrypt-hash>
ADMIN_BERIT_PASSWORD_HASH: <bcrypt-hash>
restart: unless-stopped
volumes:
db:
uploads:
Caddy auf dem Host übernimmt TLS-Terminierung und Reverse-Proxy zum Container. Die drei Admin-Konten (Marcel, Renate, Berit) sind fest in Umgebungsvariablen — kein Admin-UI zur Benutzerverwaltung.
3. Datenbankschema
Schema wird beim App-Start angelegt, falls die DB-Datei noch nicht existiert. Kein Migrations-Framework.
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS codes (
id INTEGER PRIMARY KEY,
code TEXT NOT NULL UNIQUE, -- 8-stellig, z. B. "AB3K7MN2"
display_name TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE TABLE IF NOT EXISTS artikel (
id INTEGER PRIMARY KEY,
titel TEXT, -- optional
kategorie TEXT NOT NULL,
notiz TEXT, -- optional
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE TABLE IF NOT EXISTS artikel_fotos (
id INTEGER PRIMARY KEY,
artikel_id INTEGER NOT NULL REFERENCES artikel(id) ON DELETE CASCADE,
position INTEGER NOT NULL, -- 0 = Galerie-Thumbnail
filename TEXT NOT NULL, -- relativer Pfad im Uploads-Volume
UNIQUE(artikel_id, position)
);
-- UNIQUE auf artikel_id = atomare First-come-first-served-Garantie
CREATE TABLE IF NOT EXISTS reservierungen (
id INTEGER PRIMARY KEY,
artikel_id INTEGER NOT NULL UNIQUE REFERENCES artikel(id),
code_id INTEGER NOT NULL REFERENCES codes(id) ON DELETE CASCADE,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
Die UNIQUE-Constraint auf reservierungen.artikel_id ist die vollständige Nebenläufigkeitsstrategie: zwei gleichzeitige INSERTs, einer gewinnt, einer bekommt einen Constraint-Fehler — kein Application-Level-Locking erforderlich.
4. Authentifizierung & Sessions
Familienmitglieder
- Code wird einmalig gegen die
codes-Tabelle geprüft - Bei Erfolg: HTTP-only Same-Site-Cookie
family_codegesetzt hooks.server.tsliest Cookie bei jedem Request →locals.familyCode+locals.displayName- URL-Parameter
?code=AB3K7MN2wird auf dem Gate-Screen automatisch validiert und der Cookie gesetzt - Kein Inhalt ohne gültigen Cookie sichtbar
Admins
- Login-Formular: Benutzername (
Marcel|Renate|Berit) + Passwort - Server prüft Benutzername → lädt zugehörigen Bcrypt-Hash aus Env →
bcrypt.compare - Bei Erfolg: HTTP-only Cookie
admin_sessionmit Admin-Name hooks.server.ts→locals.admin- Unbekannter Benutzername gibt dieselbe Fehlermeldung wie falsches Passwort (kein User-Enumeration)
Trennung
- Gültiger
family_code-Cookie gewährt null Zugang zu/admin/* - Gültiger
admin_session-Cookie gewährt null Zugang zur Familiengalerie +layout.server.tsunter/admin/prüftlocals.adminund leitet bei Fehlen zu/admin/loginum
5. Foto-Handling
Upload
- Admin sendet Multipart-Formular
- SvelteKit Server-Action empfängt Dateien via
request.formData() sharpverkleinert auf max. 1200 px lange Seite, konvertiert zu WebP (~800 KB Ziel)- Datei wird geschrieben nach
/app/uploads/{artikel_id}/{uuid}.webp artikel_fotos-Zeile mit relativem Pfad und Position wird eingefügt
Auslieferung
Route GET /uploads/[...path]/+server.ts streamt die Datei vom Disk. URLs sind UUID-basiert und nicht erratbar — keine Auth-Prüfung erforderlich.
Reihenfolge ändern
Admin verschiebt Thumbnails → Client sendet neues position-Array → Server aktualisiert alle artikel_fotos-Zeilen in einer Transaktion.
Löschen
- Einzelnes Foto:
artikel_fotos-Zeile löschen + Datei von Disk entfernen - Artikel löschen: CASCADE löscht
artikel_fotos-Zeilen, Server entfernt Dateien imuploads/{artikel_id}/-Verzeichnis
6. Routenstruktur
src/
hooks.server.ts # Cookie → locals.familyCode / locals.admin
routes/
+page.svelte # Gate Screen
+page.server.ts # ?code= validieren, Cookie setzen
uploads/[...path]/
+server.ts # Foto von Disk streamen
galerie/
+page.svelte # Responsives Grid + Kategorie-Filter
+page.server.ts # Artikel + Reservierungsstatus laden
artikel/[id]/
+page.server.ts # Reservieren / Aufheben (Form Actions)
admin/
+layout.server.ts # Auth-Guard
+layout.svelte # Sidebar-Navigation
login/
+page.svelte
+page.server.ts # Bcrypt-Verify, Session-Cookie setzen
inventar/
+page.svelte
+page.server.ts # CRUD Artikel + Foto-Upload
codes/
+page.svelte
+page.server.ts # Codes verwalten, Link kopieren
reservierungen/
+page.svelte
+page.server.ts # Übersicht nach Person gruppiert
uebersicht/
+page.svelte
+page.server.ts # Dashboard-Kennzahlen
lib/
db.ts # better-sqlite3-Singleton, WAL on init
photos.ts # sharp resize + write/delete
auth.ts # bcrypt verify, Cookie-Helpers
Alle Daten werden in +page.server.ts-Dateien geladen — kein separater API-Layer, keine Client-seitigen Fetch-Calls für Datenzugriff. Mutationen laufen über SvelteKit Form Actions (progressive enhancement, funktioniert ohne JS).
7. Nicht-funktionale Entscheidungen
| Anforderung | Umsetzung |
|---|---|
| NFR-SEC-001 Codes kryptographisch zufällig | crypto.randomBytes → zufällige Auswahl aus A-Z + 0-9, 8 Zeichen |
| NFR-SEC-002 Passwörter gehasht | bcrypt, work factor 12 |
| NFR-SEC-003 Nur eigene Reservierungen aufheben | Server prüft reservierungen.code_id = locals.familyCode.id |
| NFR-SEC-004 Admin-Trennung | Separate Cookie-Namen, separate Auth-Guards in hooks |
| NFR-PERF-001 500 Artikel flüssig scrollen | SSR-gerenderte HTML-Liste, kein Client-Side-Fetch beim Laden |
| NFR-PERF-002 Fotos max. ~800 KB | sharp WebP-Konvertierung bei Upload |
| NFR-DATA-002 Atomare Reservierungen | UNIQUE-Constraint auf reservierungen.artikel_id |