# Lessons Learned — Rakku Website V2

Wird laufend ergänzt. Zweck: Fehler nicht wiederholen, getroffene Entscheidungen nicht erneut zur Diskussion stellen müssen.

## Fehler / Korrekturen

- **Eckrahmen-Motiv falsch angewendet.** Der Sucher-Rahmen von der Visitenkarte wurde zuerst eng um den Text-Block gelegt (`.rk-frame` mit viel Padding), dadurch wirkten die Ecken zufällig/schwebend, weil der Textinhalt viel kleiner war als die Box. Richtig: Der Rahmen gehört an die Kanten des ganzen Foto-/Hero-Ausschnitts (fester Inset vom Sektionsrand), nicht um einen Textblock. **Warum wichtig:** Das Motiv ist ein Kamerasucher — er muss etwas *rahmen* (ein Bild, eine Szene), nicht Text umschließen, sonst wirkt er dekorativ statt gestaltet.
- **Reiner Text-Hero ohne Foto war ein Fehler**, obwohl er dem Wortmarken-Look der Visitenkarte technisch treu war. Für eine Fotografie-Seite ist die eigentliche Arbeit (echte Fotos) das, was im ersten Moment überzeugt — nicht Typografie allein. Seitdem: Hero + „Ausgewählte Arbeiten" zeigen von Anfang an echte Aufnahmen.
- **Next.js Settings-Seiten müssen `dynamic = "force-dynamic"` haben**, sobald sie live aus MongoDB lesen (z. B. Wartungsmodus-Flag). Ohne das backt Next.js den Wert beim Build ein (SSG) — der Admin-Schalter hätte dann nicht sofort gewirkt, sondern erst nach einem Rebuild. Beim ersten Bauen übersehen, beim Boot-Test aufgefallen (Route war als „SSG" statt „Dynamic" markiert).
- **Next.js 16: `middleware.ts` ist deprecated, heißt jetzt `proxy.ts`** (gleiche API, nur umbenannt). Beim ersten Build kam eine Warnung, seitdem `src/proxy.ts`.
- **Gestartete Next-Dev-Prozesse sauber stoppen ist nicht trivial:** `npx next start -p 3009` spawnt Kindprozesse, deren Kommandozeile nicht zwingend `-p 3009` enthält — `pkill -f "next start -p 3009"` hat dadurch einmal nicht gegriffen und der Prozess lief weiter. Zuverlässig: `fuser -k <port>/tcp`, danach mit `ss -tln` verifizieren, dass der Port wirklich frei ist.

- **CSS-Animation überschreibt statisches `transform` komplett, statt es zu kombinieren.** Der Scroll-Down-Button war mit `left-1/2 -translate-x-1/2` (Zentrierung) UND einer Bounce-Keyframe-Animation auf `transform` (Wippen) auf demselben Element — die Animation hat die Zentrierung während der Laufzeit ständig überschrieben, der Button hing sichtbar rechts von der Mitte. Fix: Zentrierung und Animation immer auf zwei verschachtelte Elemente verteilen, wenn beide `transform` brauchen. **Woran erkennbar:** wirkt "fast mittig, aber nicht ganz" und bewegt sich dabei.

- **Impressum liegt in V1 extern**, nicht auf der eigenen Seite: Der Footer-Link zeigt auf `https://impressum4u.de/rakkulive/` (Drittanbieter-Impressum-Generator), das eigene `impressumText`-Feld in der DB ist nie ausgefüllt worden (Platzhalter-Text). V2 übernimmt denselben externen Link, statt ein eigenes Impressum zu erfinden — echte rechtliche Angaben (Anschrift, Steuernummer etc.) hat nur der Nutzer, die dürfen nicht geraten werden.
- **Testskripte, die direkt per Mongoose/MongoDB-Treiber Testdaten anlegen, müssen `dbName: process.env.MONGODB_DB` (= `rakkuV2`) explizit an `mongoose.connect()` übergeben** — ohne das landen Schreibzugriffe in der Standard-DB der Connection-URI (die selbst keinen DB-Namen enthält), nicht in `rakkuV2`. Ein Cleanup-Skript ohne `dbName` läuft dadurch "erfolgreich" durch, löscht aber nichts — die Testkonten/-Aufträge bleiben in der echten Datenbank liegen (06.08.2026 passiert, erst beim nächsten Testlauf bemerkt, weil ein Testkonto als "erstes Vollzugriff-Konto" gefunden wurde statt Tills echtem Account). Immer nach jedem Cleanup mit einer separaten Abfrage verifizieren, dass wirklich nichts mehr da ist, nicht nur den Exit-Code des Skripts vertrauen.
- **Testdaten, die per direktem `insertOne()` (nicht über die App/Mongoose-Validierung) angelegt werden, müssen trotzdem gültige Enum-Werte verwenden** (z. B. `shootingType`) — sonst wirft eine spätere `order.save()` aus einer echten Server-Action (z. B. Rabattcode einlösen) eine `ValidationError`, die wie ein echter Produktbug aussieht, aber nur am ungültigen Testdatum liegt. Einmal fälschlich fast als Bug gemeldet, beim Log-Check richtiggestellt.
- **`findOneAndUpdate(..., { upsert: true })` wendet Mongoose-Schema-Defaults NICHT automatisch auf neu eingefügte Felder an**, wenn sie nicht explizit in `$setOnInsert` stehen (`setDefaultsOnInsert: true` fehlte). Der lazy angelegte "Aufträge"-Systemkalender (`getOrCreateOrdersCalendar()`) entstand beim allerersten Seitenaufruf, **bevor** die Google-Felder zum `Calendar`-Schema hinzukamen — das Dokument bekam nie ein `googleCalendarId`-Feld, der `default: "primary"` aus dem Schema griff nie. Ergebnis: Google-Push schlug mit "404 Not Found" fehl, aber **komplett lautlos** (kein Log-Eintrag), weil der Push-Code Fehler bewusst nur loggt statt zu werfen (damit eine kaputte Google-Verbindung nie das eigentliche Speichern blockiert) — der Nutzer hat den fehlenden Termin bemerkt, nicht ein Fehlerlog. **Lehre:** bei lazy-erzeugten Singleton-Dokumenten über `$setOnInsert` entweder jedes Feld explizit auflisten (nicht auf Schema-Defaults verlassen) oder `setDefaultsOnInsert: true` setzen — und bei "Fehler nie blockierend, nur loggen"-Design zusätzlich einen sichtbaren Weg einbauen, node stille Fehlschläge zu bemerken (hier: der neue 2-Stunden-Cronjob als Sicherheitsnetz, holt liegengebliebene Termine automatisch nach).
- **`html { scroll-behavior: smooth }` in globals.css lief gleichzeitig mit Lenis (JS-Sanft-Scroll)** — beide Systeme haben um die Scroll-Position konkurriert. Nutzer meldete: teils lässt sich ohne Reload nicht mehr scrollen, und am unteren Seitenende hängt es kurz fest. Klassischer, in der Lenis-Doku selbst erwähnter Konflikt. Da jeder JS-ausgelöste Scroll im Code ohnehin schon über `scrollToTarget()` (lib/lenisInstance.ts) läuft — nutzt Lenis wenn aktiv, sonst natives `behavior: "smooth"` pro Aufruf als Fallback für reduced-motion — war die globale CSS-Regel komplett redundant und nur schädlich. Ersatzlos entfernt, mit echtem Wheel-Scroll-Test (nicht nur `scrollTo()`) über Playwright verifiziert, inkl. Szenario kurze-Seite-dann-lange-Seite-Navigation ohne Reload.
- **`req.nextUrl.origin` in Next.js Route Handlers ist hinter dem nginx-Reverse-Proxy nicht verlässlich** — bei den neuen Google-OAuth-Routen (`/api/auth/google/connect` und `/callback`) damit gebaute Redirect-URLs zeigten auf `http://localhost:3009` statt `https://v2.rakku.de` (vom Nutzer per Screenshot gemeldet: "Die Website ist nicht erreichbar" nach erfolgreichem Google-Login). Der eigentliche Verbindungsaufbau hatte trotzdem funktioniert — nur die abschließende Weiterleitung war kaputt. Fix: konsequent `getSiteUrl()` (liest `SITE_URL` aus `.env.local`) statt `req.nextUrl.origin` für alle Redirects auf die eigene Seite verwenden — genau der Grund, warum `getSiteUrl()` an anderer Stelle im Code schon existierte. Bei jeder neuen Route mit `NextResponse.redirect()` künftig direkt daran denken.
- **Live-Chat-Skript (`LiveChat.tsx`) hatte `crossOrigin="anonymous"` unnötig gesetzt** — dadurch erzwang der Browser einen CORS-Modus-Request für das tawk.to-Skript, was zu einer sichtbaren Konsolenfehlermeldung führte. Entfernt (Drittanbieter-Embed-Skripte wie tawk.to sind nicht für CORS-Laden gedacht). **Tieferliegendes, nicht von uns behebbares Problem bleibt bestehen:** tawk.tos CDN liefert das Skript mit `Content-Type: application/x-javascript` ohne `Cross-Origin-Resource-Policy`-Header aus — das blockiert Chromium-Browser teils per Opaque-Response-Blocking (ORB), unabhängig von unserem Code. Bekanntes, von anderen tawk.to-Nutzern gemeldetes Problem (siehe tawk.to-Community-Forum), nicht in unserem Code lösbar — höchstens über tawk.to-Support ansprechbar oder durch Selbst-Hosten/Proxy des Skripts mit korrigierten Headern.

## Workflow-Änderung: persistenter Preview

Ab jetzt läuft V2 **dauerhaft** unter `https://v2.rakku.de` (nginx-Vhost + Zertifikat, per pm2 als Prozess „rakku-v2" verwaltet, wie auch andere Projekte auf diesem Server schon per pm2 laufen). Das alte Muster „`next start` für einen Test hochfahren, danach mit `fuser -k` wieder stoppen" gilt **nicht mehr**.

**Neuer Ablauf nach Code-Änderungen:**
```
cd /var/www/html/rakku-v2 && npx next build
pm2 restart rakku-v2
```
Kein manuelles Starten/Stoppen mehr nötig. `pm2 list` zeigt den Status, `pm2 logs rakku-v2` die Logs.

- **V1-Beschreibungstexte wiederholen oft den Titel als ersten Absatz** (z. B. Titel „Helldiver - Part 4", Beschreibung beginnt mit nur „Helldiver") — beim Anzeigen von Beschreibungs-Ausschnitten (Karten, Teaser) nicht per exaktem String-Vergleich filtern, sondern mit Enthalten-Prüfung in beide Richtungen, sonst rutschen Kurzformen durch.

- **Wheel-Scroll-Physik nicht selbst bauen.** Für "butterweiches" Scrollen (statt der nativen stückhaften Mausrad-Bewegung) wurde bewusst die etablierte Bibliothek **Lenis** verwendet statt einer eigenen wheel-event-Lösung — Marke Eigenbau bricht erfahrungsgemäß Trackpad-Momentum, Anchor-Links, Tastatur-Scrolling und Barrierefreiheit. Überall wo programmatisch gescrollt wird (Scroll-Cue, Nach-oben-Button, Custom-Scrollbar), `lib/lenisInstance.ts` (`scrollToTarget`/`getLenis`) nutzen, nicht `window.scrollTo` direkt — sonst läuft es nicht synchron zur Lenis-Instanz.
- **Modals müssen Lenis explizit stoppen**, nicht nur `body.style.overflow = hidden` setzen — Lenis fängt Wheel-Events selbst ab, das native Overflow-Hidden reicht allein nicht, um Hintergrund-Scrollen bei offener Lightbox zu verhindern. Immer `getLenis()?.stop()` beim Öffnen und `.start()` beim Schließen.

- **Client-Komponenten dürfen nichts aus `models/*.ts` importieren**, auch nicht nur Konstanten (z. B. `SHOOTING_TYPES`) — die Modell-Dateien importieren `mongoose`, das wiederum Node-only-Pakete wie `mongodb`/`tls` zieht, die im Browser-Bundle nicht auflösbar sind (Build bricht mit "Module not found: tls" ab). Enums/Konstanten, die sowohl Client- als auch Server-Code brauchen, gehören in eine eigene Datei ohne Mongoose-Import (hier: `lib/orderConstants.ts`).

- **Admin-Login (Phase 2, env-basiert) wurde am 03.08.2026 vorzeitig auf ein echtes Rollen-Modell migriert**, statt bis Phase 6 zu warten — Nutzer wollte, dass Mitarbeiter sich ganz normal über `/login` einloggen können und mit demselben Account auch eigene Aufträge anlegen. Umsetzung: `role: "client" | "staff"` am Client-Model, `isAdminSession()` prüft jetzt die Rolle der aktuellen Client-Session statt eines separaten Cookies — Funktionsname blieb gleich, dadurch mussten die ~15 Stellen, die `isAdminSession()` aufrufen, nicht angefasst werden. Phase 6 baut darauf auf (feinere Rechte/Teams), muss aber keine Migration mehr nachholen.
- **Playwright + Chromium lassen sich in dieser Umgebung nachinstallieren** (`npx playwright install --with-deps chromium`, ca. 300 MB, ~1–2 Min.) — damit sind echte Screenshot-Verifikationen gegen die Live-Seite möglich, nicht nur HTML/curl-Checks. Für Ad-hoc-Skripte reicht ein `npm install <pakete> --no-save` in einem Scratch-Verzeichnis (nicht im Projekt-Repo!) — Achtung: jeder erneute `npm install` ohne `package.json` im selben Ordner **entfernt vorher installierte Pakete wieder** (npm behandelt es als exakten Soll-Zustand), also alle benötigten Pakete in einem einzigen `npm install`-Aufruf zusammenfassen.

## Bestätigte Entscheidungen

- **Keinen neuen Nextcloud-Ordner „Kunden" anlegen** — es gibt bereits eine echte, gewachsene Struktur unter `[PHOTOGRAPHY]/<Kundenname>` inkl. `[VERTRÄGE]/[VORLAGEN]`. V2 baut darauf auf, statt eine Parallelstruktur zu erfinden.
- **Kein technischer Extra-Nextcloud-User** — ein App-Passwort auf dem bestehenden `Rakku`-Account reicht, ist jederzeit widerrufbar und spart eine unnötige Nutzerverwaltung.
- **Quantify nur für Headlines/Wordmark, nie für Fließtext** — ausdrücklich vom Nutzer so eingeschränkt, obwohl die Schrift kostenlos und frei verfügbar ist.
- **Design-Feedback grundsätzlich mit einer echten, interaktiven Vorschau (Artifact) beantworten**, nicht nur textlich beschreiben — in dieser Umgebung gibt es keinen Browser, um Ergebnisse direkt zu zeigen.
- **Planungsdokumente (dieses Verzeichnis) getrennt vom Code-Repo** unter `/var/www/html/rakku-v2-plan/`, wie in der vom Nutzer bereitgestellten Workflow-Anleitung beschrieben — verhindert versehentliches Mitbauen/Deployen der Planungsdateien.
- **Quadratisches Quellfoto randvoll komponiert → weder "breiter" noch "quadratisch" kann das Objektiv freistellen**, weil bei `object-fit: cover` die volle Bildbreite so oder so immer gezeigt wird (nur oben/unten wird bei anderen Seitenverhältnissen mehr weggeschnitten). Das Objektiv sitzt im Original-Foto selbst schon am linken Bildrand — kein Crop-Bug, sondern die tatsächliche Motiv-Komposition. Lösung, die wirklich funktioniert: `object-fit: contain` (zeigt das komplette Foto unbeschnitten) auf einem geblurrten, hochskalierten Hintergrund desselben Bilds — wirkt breiter, verliert nichts vom Motiv. Für `rakku-portrait.jpg` jetzt so umgesetzt.
- **About-Foto final geklärt (nach mehreren Runden):** Der Nutzer will explizit `aspect-[4/3]`, `object-cover`, kein Blur-Trick, kein Ring-Border — oben/unten beschnitten ist für ihn okay, Hauptsache breites/„langgezogenes" Format. Das war meine zweite Implementierung überhaupt ("eine der ersten Versionen von dir"). **Nicht mehr ändern**, ohne dass der Nutzer das selbst wieder anspricht — das Hin und Her (quadratisch ↔ breit ↔ Blur) hat mehrere Runden gekostet, weil ich zwischendurch zu stark auf „technisch korrekt/kein Beschnitt" statt auf den tatsächlich geäußerten Geschmack optimiert habe.
- **Bildplatzierung Hero vs. About:** Hero zeigt immer die fotografische *Arbeit* (Ergebnis), personenbezogene Fotos von Rakku selbst gehören in die About-Sektion. Vom Nutzer ausdrücklich bestätigt, als das Foto `DSC03919.jpg` (Rakku beim Fotografieren) ins Spiel kam — nicht wieder zur Diskussion stellen, außer der Nutzer bringt es selbst auf.
