- Ernten: Jahrgang als grosse Serifenzahl mit auslaufender Linie, Karten
heben sich mit Honigschimmer, Pfeil und Unterstrich
- Ernte-Detail: grosser Titel, mitlaufendes Bild, Fuellstandsbalken je
Charge (verfuegbare von abgefuellten Glaesern)
- Chargen-Seite (QR-Code auf dem Glas): Echtheitssiegel stempelt sich ein,
Zeitleiste mit Waben und sich fuellender Linie, Werte in Serifenschrift
- Produktverweise heben sich, gezeichneter Pfeil statt Textzeichen
- Fix: aktive Seite in der Kopfzeile zeigte den Unterstrich in Textfarbe
statt in Honig (unlayered CSS ueberstimmte die Utility-Klasse)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Die Chargenseite (/charge/[code]) zeigt zwei Bildkacheln: links Ernte und
Jahr, rechts den Standort. Gefuellt wurden sie aus einer Bilder-LISTE, in
der allein die Reihenfolge entschied, welches Bild wohin kommt.
Damit liess sich das zweite Bild nicht allein setzen. Wer nur ein Foto vom
Standort pflegen wollte, bekam es auf Position 0 — also in die linke
Kachel, wo Ernte und Jahr stehen sollten. Welche Kachel ein Bild fuellt,
war im Formular ausserdem nirgends erkennbar.
Jetzt zwei benannte Slots statt einer Liste, im Chargen- wie im
Ernten-Formular:
Bild 1 — linke Kachel auf der Chargenseite
Bild 2 — rechte Kachel auf der Chargenseite (Standort)
Jeder Slot hat eine eigene Vorschau, einen eigenen Auswahl- und einen
eigenen Entfernen-Knopf. Beide tragen denselben Feldnamen "images" und
werden in dieser Reihenfolge uebermittelt.
Dafuer brauchte es einen positionserhaltenden Parser: strArray() trimmt
und filtert leere Werte heraus, wodurch ein leerer erster Slot
verschwunden und der zweite nach vorn gerutscht waere — genau der Fehler,
der behoben werden sollte. Neu ist strArraySlots(), das leere Slots als ""
stehen laesst und nur am Ende stehende entfernt (ein leeres Feld ergibt
weiterhin [] statt ["", ""]).
Die Konsumenten mussten entsprechend von "??" auf "||" wechseln: ein im
Admin geleerter Slot kommt als leerer String an, nicht als undefined. Mit
"??" haette dieser leere String als gueltiger Wert gegolten und das
Erntebild darunter verdeckt (charge/[code], ernten, ernten/[year]/[type]).
strArray/strArraySlots sind nach lib/form-fields.ts gewandert und werden
aus _shared.ts re-exportiert — saemtliche bestehenden Importe bleiben
unveraendert. Grund: _shared.ts zieht ueber lib/prisma.ts einen
Datenbank-Adapter nach und liess sich ohne DATABASE_URL nicht testen. Als
reine Funktionen ohne Datenbankbezug sind die beiden jetzt direkt
pruefbar, passend zur bereits dokumentierten Teststrategie des Projekts.
Sieben neue Tests decken den Kernfall ab (leerer erster Slot bleibt
offen), mit Gegenprobe, dass strArray denselben Fall weiterhin
zusammenrutschen laesst.
Produktbilder bleiben bewusst bei der bisherigen Liste: dort ist die
Bilderzahl offen und die Reihenfolge traegt keine Kachel-Bedeutung.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
/charge/[code] zeigt zwei Bild-Slots (Produkt-/Standortfoto). Die
bisherige Logik ersetzte bei JEDEM eigenen Chargenfoto beide Slots
komplett durch batch.images — wer nur ein Chargenfoto pflegte, verlor
dadurch das zweite (Standort-)Bild der Ernte statt es weiter anzuzeigen.
Jeder Slot faellt jetzt einzeln auf das jeweilige Erntebild zurueck.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Chargen hatten bisher kein eigenes Bild-Feld (nur Ernten und Produkte) —
neue Migration ergaenzt Batch.images (Json-Array, gleiches Muster wie
Harvest.images/Product.images). Admin kann Chargenfotos jetzt unter
/admin/chargen/[id] hochladen (AdminImageListField, identisch zum
Ernten-Formular). Die oeffentliche Chargen-Seite (/charge/[code]) zeigt
diese Fotos jetzt bevorzugt, faellt ohne eigene Chargenbilder weiterhin
auf die Erntebilder zurueck.
Hinweis fuer den naechsten Live-Deploy: die generierte Migration hat
nebenbei eine bereits laenger bestehende Schema-Abweichung mitkorrigiert
(harvests.images und products.images/tasteTags werden jetzt als NOT NULL
erzwungen, wie im Schema schon laenger deklariert) - "prisma migrate
deploy" schlaegt fehl, falls dort noch echte NULL-Werte stehen sollten.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Neues admin-editierbares Hintergrundvideo fuer den Startseiten-Hero
(home.hero.video, MP4/WebM) mit eigenem, magic-byte-validiertem
Upload-Endpunkt (app/api/admin/upload-video) analog zur bestehenden
Bild-Upload-Pipeline. Ausserdem: ScrollReveal-Animation von der
Startseite auf Imkerei, Herkunft, Unternehmen, Ernten, Wissen, Kontakt,
Shop sowie die Produkt-/Ernte-/Chargen-Detailseiten ausgerollt (bisher
nur Startseite), priority auf bislang lazy geladene Above-the-fold-Bilder
(Herkunft/Unternehmen) gesetzt, und passende sizes-Hints auf Grid-Bilder
(Produktkarten, Origin/Regional/Business-Teaser, Ernten-Liste) ergaenzt
statt voller Viewport-Breite.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Live Lighthouse + performance-trace audits against the running dev app
found two real, measurable defects:
- /checkout and /warenkorb rendered the cart-store's default empty
state before Zustand's persist middleware finished reading
localStorage, so any customer who already had items in their cart
saw an "empty cart" flash before the real form appeared (CLS 0.82).
Fixed by tracking hasHydrated in the store and rendering the
existing route skeleton until rehydration completes. Verified via
Chrome DevTools performance trace: CLS 0.82 -> 0.00 on both pages.
- axe-core (via Lighthouse) flagged text-ink/60 on the cream
background at 4.09:1 contrast, under the 4.5:1 WCAG AA minimum for
normal-size text. Bumped to text-ink/70 (5.61:1) everywhere the same
token is used at text-xs/text-sm across 7 files.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- format.ts: formatPrice/formatDate/formatDateTime/formatMonthYear nehmen
jetzt ein optionales Locale-Argument (Default "de" fuer unveraenderte
Aufrufer wie Rechnungen/Admin) und formatieren fuer "en" als en-GB statt
hart codiert als de-DE -- englischsprachige Kundschaft sah bisher trotz
gewaehlter Sprache "9,90 €" und "10. Mai 2026" statt "€9.90"/"10 May 2026".
Alle kundenseitigen Aufrufer (Produktseite, Warenkorb, Checkout, Bestell-
verfolgung, Konto-Bestellungen, Ernten-/Chargenseiten, Live-Suchvorschlaege,
Bestell-/Versand-E-Mails) reichen jetzt die aktive Locale durch.
- en.ts: openCart/shippingWithCount/shippingLabel pluralisieren "item(s)"
jetzt korrekt (vorher z.B. "Open cart, 1 items").
- product-gallery.tsx: Haupt- und Vorschaubilder nutzen jetzt next/image
statt <img> (hochgeladene Produktfotos sind immer same-origin unter
/uploads/..., brauchen also keine remotePatterns-Konfiguration wie der
entfernte Kommentar behauptete) -- Hauptbild mit priority/responsivem
sizes, da es auf der Produktseite das wahrscheinlichste LCP-Element ist.
- content-image.tsx: neue optionale priority-Prop; hero.tsx setzt sie fuer
das Startseiten-Hero-Bild (ebenfalls LCP-Kandidat).
- Sticky-Mobile-CTA-Leiste und useCartStock-Polling gegengeprueft: bereits
korrekt (pb-28-Platzreservierung bzw. Fetch nur bei Warenkorb-Aenderung
ohne Intervall) -- keine Aenderung noetig.
Verifiziert: tsc --noEmit clean, pnpm test (142/142 gruen), pnpm build erfolgreich.
Echter Datei-Upload statt CSS-Platzhaltergrafiken, gespeichert unter
UPLOAD_DIR (Laufzeit-Verzeichnis, weiss nichts von seiner tatsaechlichen
Speicher-Anbindung — in Produktion ein von der Betreiberin selbst per
Kubernetes eingebundenes Volume, z.B. SMB; lokal ein simples ./uploads).
- lib/uploads.ts: Kernlogik. Bildformat wird ausschliesslich aus den
tatsaechlichen Bilddaten erkannt (Magic-Bytes via sharp), nie aus
Client-Dateiname/MIME-Type. Zufaellige, nicht erratbare Dateinamen
(Path-Traversal-/Overwrite-Schutz). Re-Encoding validiert Pixel-Daten
vollstaendig und entfernt eingebettete Metadaten (EXIF/ICC/XMP) als
Nebeneffekt. Content-Type beim Ausliefern kommt ausschliesslich aus
einer festen Endung->MIME-Zuordnung, nie aus Dateiinhalt/-namen.
- app/api/admin/upload/route.ts: Admin-only Upload-Endpunkt (CSRF- +
Auth- + Rate-Limit-Pruefung, max. 8 MB, JPEG/PNG/WebP/AVIF).
- app/uploads/[...path]/route.ts: liefert hochgeladene Bilder aus
(public/ wird zur Build-Zeit fest ins Image gebacken, kann also keine
zur Laufzeit hinzugefuegten Dateien bedienen) — mit
Path-Traversal-Schutz und nosniff-Header.
- content-registry.ts: neuer "image"-Feldtyp fuer Hero-/Startseite-
Grafiken, admin/inhalte hat dafuer ein Upload-Widget.
- Produkt-/Ernten-Admin-Formulare: Mehrfach-Bild-Upload fuer
Product.images/Harvest.images (beide Felder existierten im Schema,
hatten aber bislang keine Admin-Oberflaeche). Oeffentliche Seiten
(Shop, Ernten, Chargen-QR-Seite) zeigen jetzt echte Fotos statt
Platzhalter, sobald vorhanden.
- Kubernetes: nur ein einfacher volumeMount in 05-app-deployment.yaml
(Platzhalter-emptyDir, klar als CHANGE-ME markiert) — die eigentliche
Volume-Quelle (SMB o.ae.) bindet die Betreiberin selbst ein, wie
gewuenscht keine SMB/CSI-spezifischen Manifeste hinzugefuegt.
Verifiziert: tsc --noEmit clean, 139/139 Tests gruen (21 neu), pnpm
build clean. Upload-/Serving-Pipeline live gegen einen laufenden
Server getestet (Auth-/CSRF-Gates, Path-Traversal-Ablehnung,
Format-Ablehnung) — ein vollstaendiger authentifizierter Upload
end-to-end (braucht echten Admin-Login inkl. 2FA) wurde nicht live
durchgespielt, nur per Code-Review verifiziert.
Co-Authored-By: Claude Sonnet 5 <[email protected]>