test(ops): verify Docker persistence, browser workflows and document deployment

This commit is contained in:
OpenCode committed 2026-10-24 15:43:37 +02:00
1 parent a58516e424
commit 8fa63415f1
35 files changed
+1105

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
# Lokale Assets
Alle zur Laufzeit benötigten Bild- und Schriftdateien werden vom eigenen Host ausgeliefert. Kein Browseraufruf an Unsplash, Google Fonts oder andere Drittanbieter.
Mitgelieferte Fotos (Unsplash-Lizenz, https://unsplash.com/license):
- `public/images/kitchen.jpg`: https://images.unsplash.com/photo-1473093295043-cdd812d0e601, Pasta mit Pesto.
- `public/images/garden.jpg`: CDN-ID `photo-1512621776951-a57141f2eefd`, Gemüse-Bowl.
- `public/images/soup.jpg`: CDN-ID `photo-1547592166-23ac45744acd`, cremige Gemüsesuppe.
Die Fotos illustrieren das Kochbuch und die optionalen Beispielrezepte; sie wurden nicht beim Kochen dieser konkreten Anleitungen aufgenommen. Eigene Bilder können im Editor hochgeladen werden.
Schriften: DM Sans und Fraunces, SIL Open Font License, über `@fontsource` als lokale Pakete. Icons: Lucide, ISC-Lizenz. HamHam-App-Icon: eigens als SVG erstellt; PNG-Ableitungen in 192/512 px.
+27
View File
@@ -0,0 +1,27 @@
# Wiederverwendbarer ChatGPT-Prompt
Kopiere den folgenden Text zusammen mit `public/examples/hamham.schema.json` in ChatGPT. Hänge idealerweise auch `pljeskavica.json` als Beispiel an.
---
Erstelle ein kulinarisch plausibles, vollständig nachkochbares Rezept für meine selbst gehostete Rezept-App **HamHam**.
**Mein Wunsch:** [z. B. Serbische Pljeskavica für 4 Personen]
Antworte ausschließlich mit gültigem JSON, ohne Markdown und ohne Begleittext. Halte dich exakt an das beigefügte JSON Schema. Die Hülle lautet `{"version":1,"recipes":[…]}`.
Regeln:
1. Deutsch, bevorzugt in Österreich übliche Zutatenbezeichnungen. Konkrete, sichere und nachvollziehbare Zubereitung.
2. `title`, `subtitle`, `description`, `cuisine`, `categories`, `tags`, `difficulty`, `prepMinutes`, `cookMinutes`, `restMinutes`, `servings`, `ingredients`, `steps`, `tips`, `storage` ausfüllen.
3. Mengen ausschließlich als positive JSON-Zahlen mit Dezimalpunkt. Keine Bruchstrings, keine Einheiten im Mengenfeld. „Nach Geschmack“ als `amount: null`, `unit: ""`, `note: "nach Geschmack"`.
4. Einheiten ausschließlich `g`, `kg`, `ml`, `l`, `Stück`, `TL`, `EL`, `Prise`, `Bund`, `""`.
5. Jede Zutat hat eine eindeutige lesbare `id`, einen `name`, `amount`, `unit`, `section`, `note`, `optional`.
6. Jeder Schritt hat eine eindeutige `id`, `title`, `text`, `ingredients` (Zutaten-IDs) und `timers` (ggf. leeres Array).
7. Mengen in Anleitungen ausschließlich mit `{{zutaten_id}}` referenzieren: `"{{fleisch}} in Würfel schneiden."`. Die App ersetzt die Referenz mit Menge, Einheit, Zutatenname und Hinweis. Nicht nochmals den Namen oder die Menge dahinter schreiben. Referenzen müssen existieren.
8. Für Warte-, Gar- und Ruhezeiten passende Timer angeben: `{"id":"eindeutige_id","label":"Nudeln kochen","seconds":600}`. IDs im gesamten Rezept eindeutig. Zeiten und Temperaturen nicht in Zutatenreferenzen umwandeln.
9. `image: ""`, `images: []`, `published: false`. Keine erfundenen Bild-URLs. Rezept-ID und Zeitstempel weglassen; die App erzeugt sie.
10. `nutrition: null`, sofern keine verlässlichen Nährwerte pro Portion verfügbar sind. Keine scheinpräzisen Werte erfinden.
11. Kein HTML, keine Kommentare, keine zusätzlichen Felder. Alle Pflichtfelder und mindestens eine Zutat und ein Schritt müssen vorhanden sein.
Prüfe vor der Ausgabe die Mengenverhältnisse, die Gesamtzeit, die Referenzen und die JSON-Syntax.
+79
View File
@@ -0,0 +1,79 @@
# Betrieb, Updates und Datensicherung
## Voraussetzungen und Erststart
Docker Engine und Compose v2+, empfohlen mindestens 2 CPU-Kerne und 2 GB RAM (Builds benötigen zeitweise mehr). Unterstützt werden die von Node 24 und PostgreSQL 17 angebotenen Linux-Architekturen; der konkrete ARM64-Test ist im Prüfbericht vermerkt.
```sh
cp .env.example .env
openssl rand -hex 32
# Ergebnis als POSTGRES_PASSWORD in .env eintragen.
docker compose up -d --build
docker compose ps
docker compose logs --tail=100 app
docker compose exec app npm run admin:create -- deinname
```
Passwort wird interaktiv verborgen abgefragt. Der Benutzername besteht aus 3–80 Zeichen (`a-z`, `0-9`, `.`, `_`, `-`); das Passwort hat 12–200 Zeichen. Doppelte Benutzernamen werden abgewiesen. Weitere Administratoren werden ebenfalls über dieses CLI angelegt. Über die CLI kann der Serverbetreiber neue Zugänge erstellen, wenn ein Passwort verloren geht.
App läuft als unprivilegierter Benutzer `node`, PID-1-Verwaltung über `tini`. PostgreSQL ist nur intern erreichbar. `database` und `images` sind persistente Named Volumes. Startup-Migrationen laufen in einer Transaktion unter einem PostgreSQL-Advisory-Lock. HTTP-Healthcheck prüft auch die Datenbankverbindung. Logs gehen nach stdout/stderr. `docker compose down` behält Daten; **`down -v` löscht sie**.
`DATABASE_URL` aus `.env` wird für lokale CLI/Entwicklung verwendet; Compose setzt die interne URL aus `POSTGRES_PASSWORD` zusammen. Deshalb ein URL-sicheres Passwort wählen (z. B. Hex). Der Wert `CHANGE_ME` muss ersetzt werden.
## Adresse und Reverse Proxy
- `HOST_PORT=3000` konfiguriert den Host-Port.
- `APP_ORIGIN` ist exakt die Browseradresse inklusive Scheme und ggf. Port, ohne Pfad; z. B. `https://rezepte.example.org`. Diese Einstellung ist für die Origin-basierte CSRF-Prüfung erforderlich.
- `COOKIE_SECURE=true` hinter HTTPS. Für ausschließlich lokales HTTP `false`.
- Proxy muss `Host` und `X-Forwarded-Proto` korrekt setzen; WebSocket-Unterstützung wird in Produktion nicht benötigt.
- `TRUST_PROXY=true` nur, wenn ausschließlich der vertrauenswürdige Proxy auf den App-Port zugreifen kann und `X-Forwarded-For` **überschreibt**, statt einen beliebigen Clientwert zu übernehmen. Ansonsten werden direkte Zugriffe in einem gemeinsamen IP-Limit zusammengefasst. Zusätzlich gilt immer ein benutzerbezogenes Limit.
- Uploadlimit am Proxy mindestens 10 MB, Importanfragen maximal 2 MB nach JSON-Verpackung; sinnvoller Proxy-Gesamtgrenzwert 11 MB. App prüft die gelesenen Bytes selbst.
- Kein öffentliches Caching für `/admin`, `/api`, dynamische HTML- oder RSC-Antworten. Bilder sind content-addressed-artig per zufälligem Dateinamen versioniert und dürfen langfristig gecacht werden.
- HSTS nach erfolgreicher HTTPS-Einrichtung am vorhandenen Proxy setzen. Keine spezielle Proxysoftware erforderlich.
Session-Cookie: HTTP-only, SameSite Strict, 7 Tage absolute Gültigkeit, optional Secure. Datenbank enthält ausschließlich den SHA-256-Hash des zufälligen 256-Bit-Tokens. Passwortwechsel widerruft alle bisherigen Sessions. Origin-Prüfung gilt auch für Login und Uploads.
## Backup
Ein konsistentes Backup umfasst Datenbank **und** Bilder. Für ein kleines Haushaltskochbuch ist eine kurze Schreibpause am einfachsten:
```sh
mkdir -p backups
docker compose stop app
docker compose exec -T db pg_dump -U hamham -d hamham -Fc > backups/hamham.dump
docker compose run --rm --no-deps --entrypoint tar app -C /app/uploads -czf - . > backups/images.tar.gz
docker compose start app
```
Zusätzlich `.env` sicher und separat sichern. Dumps enthalten Passwort-Hashes und aktive Session-Hashes und sind vertraulich. Backups verschlüsselt außerhalb des Hosts aufbewahren, regelmäßig rotieren und Restore testen. JSON-Export ist ein portables Rezeptformat, aber ersetzt kein vollständiges Backup (keine Bildbytes oder Authentifizierungsdaten).
## Restore
Auf einem Host mit kompatiblem Release und korrekter `.env`:
```sh
docker compose up -d db
docker compose stop app
docker compose exec -T db pg_restore -U hamham -d hamham --clean --if-exists < backups/hamham.dump
docker compose run --rm -T --no-deps --entrypoint tar app -C /app/uploads -xzf - < backups/images.tar.gz
docker compose exec -T db psql -U hamham -d hamham -c 'DELETE FROM sessions;'
docker compose up -d app
```
Vor dem Restore auf bereits benutzten Volumes alte Bilddateien bei Bedarf in ein separates Archiv verschieben. Keine nicht zugeordneten fremden Daten löschen. Anschließend Anmeldung, Bilder, Rezeptzahl und `/api/health` kontrollieren.
## Update und Migrationen
1. Releasehinweise lesen, Backup erstellen.
2. `git pull --ff-only` auf dem gewünschten Release-Branch.
3. `docker compose build --pull app`.
4. `docker compose up -d`.
5. `docker compose logs --tail=100 app` und Healthchecks prüfen.
Migrationen sind nummerierte, unveränderliche SQL-Dateien in `migrations/`. Bereits angewendete Namen stehen in `migrations`. Neue Migrationen müssen vor Auslieferung auf leerer Datenbank und einer Kopie vorhandener Daten getestet werden. Schemaänderungen möglichst additiv und abwärtskompatibel. Kein automatisches destruktives Downgrade: für inkompatible Rückkehr vorheriges Image plus dazugehöriges Datenbank-/Bildbackup wiederherstellen.
## Offline und Timer
Die PWA cached lokale statische Ressourcen und online geöffnete, veröffentlichte Rezept-/Kochseiten. Auch nach clientseitiger Navigation wird eine vollständige Dokumentfassung für die Offline-Nutzung gespeichert. Bereits besuchte öffentliche Listenansichten werden ebenfalls gesichert. Nicht besuchte Seiten zeigen eine klare Offline-Ansicht. Admin- und API-Antworten werden vom Service Worker nicht gecacht. Browser können Cachedateien aus Platzgründen entfernen; offline ist deshalb eine Komfortfunktion, kein Verfügbarkeitsversprechen. Für einen zuverlässigen Kochabend die benötigte Kochseite vorab online öffnen.
Timer speichern absolute Endzeiten und korrigieren ihren Stand nach Navigation, Schlafmodus und Reload. Signalton benötigt eine Nutzeraktion („Ton an“); Betriebssysteme können Hintergrundseiten und Audio pausieren. Für zeitkritische Vorgänge zusätzlich einen Geräte-/Küchentimer verwenden. Wake Lock ist eine progressive Browserfunktion und wird im UI mit seinem tatsächlichen Status angezeigt.
+55
View File
@@ -0,0 +1,55 @@
# Architektur und Entwicklung
## Stack
Next.js App Router, React, TypeScript, PostgreSQL, Drizzle, Zod, decimal.js, Motion, Lucide, Sharp und `@node-rs/argon2`. Konkrete kompatible Versionen sind in `package-lock.json` fixiert. Das UI nutzt bewusst eigenständiges CSS mit Designvariablen statt eines generischen Dashboard-Themes. Lokale DM Sans und Fraunces werden über Fontsource gebündelt.
## Verantwortlichkeiten
| Pfad | Verantwortung |
| -------------------------------- | -------------------------------------------------------------- |
| `src/lib/recipe.ts` | Gemeinsames Datenmodell, Validierung, Referenzintegrität |
| `src/lib/calculator.ts` | Reine präzise Skalierung, Einheiten, Anzeige und Einkaufsliste |
| `src/lib/import.ts` | JSON/XML-Dekodierung und verlustfreier Metadatenexport |
| `src/lib/cooking.ts` | Testbare Zeitstempel-Timer |
| `src/lib/server.ts` | Sessions, CSRF, Größenlimits, Rate Limit, Fehlerabbildung |
| `src/lib/repository.ts` | Rezeptpersistenz, Sichtbarkeit, Versionskonflikte |
| `src/db/` | Drizzle-Mappings und PostgreSQL-Pool |
| `src/app/api/[...path]/route.ts` | Dünne HTTP-Operationen mit Validierung/Autorisierung |
| `src/components/` | Bibliothek, Detail, Kochmodus, Editor, Administration |
| `migrations/` | Versionierte SQL-Migrationen |
| `scripts/` | Migration, Admin-Bootstrap, Schema/Beispielgenerierung |
Die Rezeptstruktur liegt als validiertes JSONB-Dokument vor, ergänzt um relationale ID, Titel, normalisierten eindeutigen Titel, Veröffentlichungsstatus und Zeitstempel. Damit werden Zutaten/Schritte atomar geändert und beim Import nicht auseinandergerissen. Sessions, Administratoren, Limits und Medien sind getrennte relationale Tabellen. Öffentliche Leseoperationen filtern serverseitig auf `published=true`.
## API
Alle Fehler: `{ "error": "Verständliche Meldung" }`. Statuscodes: 400 Syntax, 401 Anmeldung, 403 Origin, 404 fehlt, 409 Duplikat/Versionskonflikt, 413 Größe, 415 Dateityp, 422 Datenvalidierung, 428 fehlende Bearbeitungsversion, 429 Loginlimit, 500 unerwarteter Serverfehler. Unerwartete Fehler werden serverseitig geloggt, ohne interne Details an Clients auszugeben.
- `GET /api/recipes`: veröffentlichte Rezepte; `?admin=1` nur mit gültiger Session erweitert.
- `GET /api/recipes/:id`: öffentlich nur veröffentlicht, sonst autorisierter Admin.
- `POST /api/recipes`, `PUT /api/recipes/:id`, `DELETE /api/recipes/:id`: Admin, Origin erforderlich. PUT benötigt den letzten `updatedAt`-Wert.
- `POST /api/auth/login`, `POST /api/auth/logout`, `GET /api/auth/me`, `POST /api/auth/password`.
- `POST /api/import`: `{raw, format: "json"|"xml", preview: boolean}`.
- `GET /api/export?format=json|xml&id=UUID`: ID optional, sonst gesamte Sammlung.
- `GET/PUT /api/settings`: Kategorien-/Tag-Auswahllisten und Sammlungsname.
- `GET/POST/DELETE /api/media`: Rohbild-Upload mit Content-Type JPEG/PNG/WebP, Löschung per `{name}`. Bilder werden serverseitig dekodiert und als WebP neu kodiert (max. 40 MP, 10 MB Eingabe, 2400 px Kantenlänge). EXIF wird entfernt.
- `GET /api/health`: Datenbankverbindung + Anwendung.
Alle Admin-Operationen prüfen die Session auf dem Server. Passwörter werden mit Argon2id (64 MiB, 3 Iterationen) gehasht. CSRF-Schutz nutzt den verpflichtenden exakten Origin-Vergleich plus SameSite-Strict-Cookies. Automatisierte API-Clients müssen die konfigurierte Origin explizit setzen. Es gibt keine Token in Local Storage.
## Rechner und Referenzen
Faktoren bleiben als hochpräzise Dezimalstrings erhalten. Neue Zielmengen werden stets durch die unveränderte Originalmenge geteilt; Originalzutaten werden nie mutiert. Runden erfolgt nur zur Anzeige. Übliche Viertel-/Halbbrüche werden typografisch dargestellt, kleine Mengen mit bis zu sechs Nachkommastellen. Nährwerte sind pro Originalportion, Gesamtzeit wird nicht skaliert.
## Editor und lokale Speicherung
Änderungen werden lokal im Browser gesichert, ohne das veröffentlichte Rezept zu verändern. „Rezept speichern“ validiert und überträgt ausdrücklich auf den Server. Der Browser warnt beim Verlassen mit noch nicht auf den Server übertragenen Änderungen. Konflikte mit einer zwischenzeitlich geänderten Fassung werden als 409 zurückgegeben. Lokale Entwürfe bleiben dann erhalten.
Zutaten und Schritte lassen sich per Drag-Handle sowie per Auf/Ab-Buttons sortieren. So ist die Funktion auch ohne Maus bedienbar. Bilder werden sofort in die lokale Medienablage hochgeladen und können dort später entfernt werden, solange kein Rezept sie verwendet.
## Tests
Unit: `npm test`. E2E: `npm run build && npm run test:e2e`. Tests verwenden echte PostgreSQL-Persistenz, echte HTTP-Authentifizierung und Chromium ohne gemockte Produktfunktionen. Die E2E-Datenbank muss auf `_test` enden und wird zurückgesetzt. Playwright erzeugt Screenshots für Desktop (1440 px), Smartphone (390 px) und Tablet (768 px).
Bei Schemaänderungen `npm run schema:generate` ausführen und generierte Dokumente mitcommitten. Entwicklungsserver und Produktionsbuild teilen dieselbe Datenbankkonfiguration. Next-Telemetrie im Container ist deaktiviert; lokal bei Bedarf `NEXT_TELEMETRY_DISABLED=1` setzen.
+83
View File
@@ -0,0 +1,83 @@
# Importformat HamHam v1
## Ablauf
Administration → Importieren → JSON/XML auswählen → Datei hochladen oder Text einfügen → Vorschau prüfen → Import bestätigen. Maximal 2 MB und 100 Rezepte pro Vorgang. Kein Datensatz wird gespeichert, solange die Vorschau läuft. Ein Fehler oder Duplikat blockiert die gesamte Übernahme.
Titel werden für die Duplikatprüfung mit NFKC normalisiert, getrimmt und ohne Groß-/Kleinschreibung verglichen. Vorhandene IDs gelten ebenfalls als Duplikat. Import überschreibt keine bestehenden Rezepte. Zum Bearbeiten den Editor verwenden; zum Anlegen einer Variante eine neue ID bzw. keine ID und einen eigenen Titel verwenden.
JSON-Dateien haben diese Hülle:
```json
{
"version": 1,
"recipes": [{ "title": "Mein Rezept", "servings": 2, "ingredients": [], "steps": [] }]
}
```
Die leeren Arrays im obigen Strukturdiagramm müssen mit mindestens einer gültigen Zutat und einem Schritt befüllt werden. Ein vollständig gültiges Beispiel steht in `public/examples/pljeskavica.json`. Das verbindliche maschinenlesbare Schema liegt in `public/examples/hamham.schema.json` (JSON Schema 2020-12).
## Felder
Pflicht: `title` (2–180 Zeichen), `servings` (positiv), `ingredients` (1–200), `steps` (1–100).
Optional: `id` (UUID), `subtitle`, `description`, `image`, `images`, `categories`, `tags`, `cuisine`, `difficulty` (`Einfach`, `Mittel`, `Anspruchsvoll`), `prepMinutes`, `cookMinutes`, `restMinutes`, `tips`, `storage`, `nutrition`, `published`, `createdAt`, `updatedAt` (UTC ISO-8601). Gesamtzeit wird aus den drei Zeitfeldern berechnet. `published` ist standardmäßig `false`. Nährwerte gelten pro Originalportion; `nutrition` ist `null` oder `{ "kcal": 500, "protein": 25, "carbs": 60, "fat": 18 }`.
Zutat:
```json
{
"id": "huhn",
"name": "Hühnerbrust",
"amount": 400,
"unit": "g",
"section": "Hauptgericht",
"note": "ohne Haut",
"optional": false
}
```
`amount`: positive JSON-Zahl oder `null`. Dezimalpunkt verwenden, keine Zeichenketten wie `"1/2"`, `"400 g"` oder `"1,5"`. Für eine halbe Zitrone `0.5` und `Stück` verwenden. Bei Salz nach Geschmack `amount: null`, `unit: ""`, `note: "nach Geschmack"`.
Einheiten: `g`, `kg`, `ml`, `l`, `Stück`, `TL`, `EL`, `Prise`, `Bund`, `""`. Umrechnung: 1 kg = 1000 g, 1 l = 1000 ml, 1 TL = 5 ml, 1 EL = 15 ml. Masse wird nie ohne Dichteannahme in Volumen umgerechnet. IDs sind innerhalb des Rezepts eindeutig, bestehen aus Buchstaben, Ziffern, `_` oder `-`, maximal 80 Zeichen.
Schritt:
```json
{
"id": "anbraten",
"title": "Goldbraun anbraten",
"text": "{{huhn}} in Würfel schneiden und anbraten.",
"ingredients": ["huhn"],
"timers": [{ "id": "bratzeit", "label": "Hühnerbrust braten", "seconds": 480 }]
}
```
`{{huhn}}` wird zu **Menge + Einheit + Zutatenname + Hinweis** aufgelöst. Diese Referenz bleibt beim Skalieren konsistent. Keine konkreten skalierbaren Mengen als Freitext wiederholen. Temperaturen und Zeiten bleiben normaler Text. `ingredients` enthält die in diesem Schritt benötigten Zutaten-IDs. Timer-IDs müssen im gesamten Rezept eindeutig sein; ein Timer dauert 1–172800 Sekunden. Zeiten werden nicht mit Portionen skaliert.
## Bilder
Nur lokale URLs: `/media/<uuid>.webp` oder mitgelieferte `/images/<name>.jpg|webp|svg`. Bilder zuerst im Adminbereich hochladen, dann den lokalen Pfad verwenden. ChatGPT soll `image: ""` und `images: []` setzen und keine erfundenen URLs ausgeben.
JSON/XML-Exporte enthalten Bildreferenzen, keine Binärdaten. Für einen Umzug das Bildvolume mitnehmen; für ein vollständiges Backup zusätzlich die Datenbank sichern. Metadaten, Zutaten, Schritte, Timer, Zeitstempel und IDs bleiben beim Export erhalten.
## XML
Das vollständige Beispiel ist `public/examples/pljeskavica.xml`. UTF-8, Wurzel `<hamham version="1">`, darunter `<recipes><recipe>…</recipe></recipes>`. Rezeptfelder entsprechen JSON. Array-Container:
| JSON | XML |
| ---------------- | ----------------------------------------------------------- |
| recipes | `<recipes><recipe>…</recipe></recipes>` |
| ingredients | `<ingredients><ingredient>…</ingredient></ingredients>` |
| steps | `<steps><step>…</step></steps>` |
| timers | `<timers><timer>…</timer></timers>` |
| categories | `<categories><category>Vegetarisch</category></categories>` |
| tags | `<tags><tag>Schnell</tag></tags>` |
| images | `<images><image>/media/…webp</image></images>` |
| step.ingredients | `<ingredients><ref>huhn</ref></ingredients>` |
Leere Menge: `<amount/>`. Keine Nährwerte: `<nutrition/>`. Boolean: `true`/`false`. Texte standardkonform escapen (`&amp;`, `&lt;` etc.). DOCTYPE und Entity-Deklarationen sind verboten, es werden keine externen Ressourcen aufgelöst. Nach dem XML-Parsing gilt dieselbe Zod-Validierung wie bei JSON.
## Versionsstrategie
Die Hülle `version: 1` ist verbindlich. Inkompatible Änderungen bekommen eine neue Version mit expliziter Konvertierung. Unbekannte Versionen werden abgewiesen. Optionale Felder erhalten dokumentierte Standardwerte. HTML-Inhalte werden nicht übernommen.
+78
View File
@@ -0,0 +1,78 @@
# Prüfnachweise
Stand: **24. Oktober 2026**. Die folgenden Prüfungen wurden tatsächlich ausgeführt, nicht lediglich vorbereitet.
## Ergebnisse
| Prüfung | Ergebnis |
| ---------------------- | ------------------------------------------------------------------ |
| `npm run typecheck` | Erfolgreich, keine TypeScript-Fehler |
| `npm test` | **33/33 Tests bestanden**, 3 Testdateien |
| `npm run build` | Erfolgreicher Next.js-Produktionsbuild, keine Buildwarnungen |
| `npm run test:e2e` | **17/17 Tests bestanden**, keine Retries oder übersprungenen Tests |
| `npm audit --omit=dev` | **0 gemeldete Schwachstellen** zum Prüfzeitpunkt |
| `npm run test:docker` | Erfolgreich, tatsächlicher Linux/ARM64-Compose-Betrieb |
### Fachtests
- 2 → 8 Portionen; 400 → 800 g als Zutatenanker; Rückskalierung und Kommadezimalwerte.
- g/kg, ml/l, TL/EL, inkompatible Dimensionen, optionale Zutaten und Zutaten ohne Menge.
- Ungültige/negative/nichtendliche Eingaben, Division durch null, extreme Faktoren, 100 wiederholte Umskalierungen ohne kumulative Rundung.
- Bruchdarstellung, reine Anzeigerundung, strukturierte Referenzen in Kochtexten.
- Einkaufsliste: kompatible Mengen zusammenführen; verschiedene Einheiten/Notizen getrennt halten.
- Vollständiger JSON- und XML-Roundtrip inklusive optionaler Daten und Escaping.
- Unbekannte Formatversionen, kaputte Referenzen, doppelte IDs, HTML, Path Traversal, XML-Entities/DOCTYPE und Größenlimits.
- Timerstart, Pause, Fortsetzung, serialisierte Wiederherstellung, Ablauf im Hintergrund und korrekte Anzeige direkt beim Start.
### Browser- und API-Integration
Chromium gegen den **Produktionsbuild**, mit echter PostgreSQL-Testdatenbank:
- Öffentliche Sammlung ohne Anmeldung, keine unveröffentlichten Rezepte.
- Live-Zutatensuche und URL-Synchronisierung, Favoriten, Themewechsel und Theme-Persistenz.
- Portionsänderung, direkter Zutatenanker mit kg/g-Umrechnung, skalierte Kochtexte.
- Einkaufsliste und gespeicherter Abhakstatus, Drucklayout ohne Navigation.
- Kochmodus vor/zurück, Zutatenstatus, Timer über Schritte hinweg, Pause/Reset, Reload-Wiederherstellung, Abschluss und Neustart.
- Admin-Anmeldung, Anlage, Bearbeitung, Veröffentlichung, Bild-Upload, lokal wiederhergestellter Editorentwurf, Live-Vorschau, Exportdownload, Löschung und Logout.
- JSON/XML-Import über die grafische Oberfläche, prüfbare Vorschau und Duplikatsperre.
- 401 für unbefugte Schreib-/Exportversuche, 403 bei fremder Origin, Session-Cookie-Eigenschaften, Versionskonflikt 409, Datenvalidierung 422.
- Atomare Importablehnung, XXE-Abweisung und Ablehnung unzulässiger/gefälschter Bilddateien.
- Passwortwechsel mit Widerruf einer anderen Sitzung, Login-Brute-Force-Limit 429.
- Echte Browser-Offlineschaltung: besuchte Kochseite lädt weiter interaktiv, Timer bleibt erhalten, unbekannte Seite zeigt ehrliches Offline-Fallback.
- Editor in Dark Mode einschließlich Zutatenreihenfolge auf allen drei Viewports.
Viewports: Desktop **1440 × 1000**, Smartphone **390 × 664** (Chromium mit iPhone-13-Geräteprofil), Tablet **768 × 1024** (Chromium mit iPad-Mini-Profil). Geräteprofile sind Emulationen; damit wird keine Prüfung auf physischem iOS/Safari behauptet.
### Tatsächlicher Docker-Betrieb
Docker Engine über Colima, Compose 5.6.1; `node:24-bookworm-slim`, `postgres:17-bookworm`, Linux ARM64. Der automatisierte Test erstellt ein getrenntes zufällig benanntes Projekt und testet:
1. Image-Build und `compose up --wait`: App und Datenbank **healthy**.
2. App-UID **1000**, kein Root-App-Prozess.
3. Initialer Admin per CLI mit zufälligem Passwort über stdin.
4. HTTP-Anmeldung, Import von drei vollständigen Rezepten, echter JPEG-Upload mit WebP-Re-Encoding, Rezeptänderung und Gesamtexport.
5. Neustart von App und Datenbank, erneute Healthchecks.
6. Gleiche Rezeptzahl und exakt gleiche Bildbytes nach Neustart; Session weiterhin gültig.
7. `pg_dump -Fc`, Restore in eine neue Datenbank, Kontrolle der wiederhergestellten Rezeptzahl.
8. Wiederholt gestartete Migrationen bleiben idempotent.
Ein erster Docker-Prüflauf deckte einen wiederverwendeten, durch den Containerneustart geschlossenen HTTP-Testsocket auf. Die Testverbindungen werden nun explizit geschlossen; der vollständige anschließende Prüflauf bestand. Es wurden keine Fehler übersprungen und keine Produktfunktionen gemockt.
## Visuelle Inspektion
16 reale Browser-Screenshots liegen in [`docs/screenshots/`](screenshots/): Bibliothek, Rezeptseite, Kochmodus und Favoriten in Desktop/Mobile/Tablet sowie Editor in hellem und dunklem Design.
Tatsächlich angesehen: Desktop- und Mobile-Bibliothek, Tablet-Bibliothek, mobile Rezeptseite, mobiler Kochmodus, Desktop-Editor, mobiler Dark-Mode-Editor und dunkle Favoritenansicht. Dabei wurden eine falsche Suchfeld-Flexrichtung, ein zu schmaler mobiler Infobereich und eine Timer-Anzeigeabweichung nach Start korrigiert. Die aktualisierten Bilder wurden erneut kontrolliert. Browserprüfungen bestätigen fehlenden horizontalen Überlauf in den geprüften Bibliotheks-, Rezept- und Editoransichten.
## Präzise Grenzen
- Kein Test auf physischem Smartphone, Safari oder Firefox; keine tatsächliche iOS-/Android-Installation über das Betriebssystem-Installationsmenü.
- Wake Lock und akustische Signale sind implementiert, aber nicht auf gesperrten physischen Geräten validiert. Browser/OS können Hintergrundseiten suspendieren; Endzeitpunkte bleiben korrekt, ein akustischer Hintergrundalarm ist nicht garantiert.
- Reverse-Proxy-HTTPS, TLS-Zertifikate und Secure-Cookies müssen am Zielhost mit seiner Domain geprüft werden. Die lokale Integration verwendete HTTP.
- Linux ARM64 ist tatsächlich getestet; ein separater AMD64-Deploy wurde nicht durchgeführt.
- Kein externer Penetrationstest, keine Garantie, dass ein Abhängigkeitsaudit jede Schwachstelle erkennt. Es sind aus den durchgeführten Prüfungen keine kritischen Sicherheitsfehler bekannt.
- Editorentwürfe, Favoriten, Einkaufslisten und Kochfortschritt sind bewusst geräte-/browserlokal; kein geräteübergreifender Sync.
- JSON/XML transportiert Rezeptdaten und lokale Bildreferenzen. Bilddateien werden separat über das persistente Volume gesichert und übertragen.
- Offline umfasst tatsächlich gespeicherte öffentliche Ansichten; neue oder aus dem Browsercache entfernte Inhalte benötigen Netz. Administration bleibt onlinepflichtig.
Diese Grenzen sind keine als fertig ausgegebenen ungeprüften Plattformversprechen. Die oben genannten implementierten Kernabläufe wurden in der dokumentierten Umgebung erfolgreich geprüft.
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB