Rollen-Farben statt reiner Markenfarben, damit hell und dunkel aus einem
Satz Klassen entstehen:
- heading (Ueberschriften, Links, Rahmen): hell Waldgruen, dunkel Creme
- surface (abgesetzte Flaeche): hell Creme, dunkel angehobenes Dunkelgruen
- forest bleibt die dunkle Markenflaeche (Hero, Baender, Knoepfe,
Admin-Leiste), cream der helle Text darauf
- honey-deep als fester Honigton fuer Flaechen; honey-dark (Text) wird
im Dunkelmodus heller und bleibt lesbar
Codemod ueber src: text-/border-forest -> *-heading, bg-cream ->
bg-surface, bg-honey-dark -> bg-honey-deep. Im hellen Modus haben die
Rollen exakt die bisherigen Werte - das Erscheinungsbild bleibt gleich.
Umschalter (components/theme-toggle.tsx) Automatisch -> Hell -> Dunkel in
Kopfzeile, Mobilmenue und Admin-Leiste. Die Wahl liegt im Cookie "theme"
(nur light/dark, geprueft in lib/theme.ts); der Server setzt data-theme
schon beim Ausliefern, nichts blitzt hell auf. "Automatisch" folgt der
Systemeinstellung. Gewaehlter und automatischer Dunkelblock in
globals.css werden per Test auf Gleichheit geprueft.
Logo, Biene und Select-Pfeil nutzen die Rollenfarben. In der Kopfzeile
steht "Honig entdecken" zwischen 1280 und 1535 px nicht mehr doppelt
neben "Shop" (sonst stiess die Navigation an), der Slogan erscheint erst
ab 1680 px.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Eine gezeichnete Biene fliegt beim Scrollen eine geschwungene Bahn ueber
"Aktuelle Ernte" und zieht eine gepunktete Honigspur hinter sich her;
Fluegelschlag und leichtes Schweben per CSS (components/bee.tsx).
- Neuer Abschnitt nach der Philosophie: Portraet des Imkers in Wabenform,
die Punkte des Wabenrings wandern an der Kante entlang, eine Biene
umkreist das Bild. Daneben der eigene Satz von der Imkerei-Seite als
Zitat - kein neuer Text.
- Herkunft-, Region- und Firmenkunden-Foto bewegen sich beim Scrollen
leicht mit (Zoom und Parallax, nur transform; components/parallax-media).
Alles rein dekorativ (aria-hidden) und mit Pfad fuer reduzierte Bewegung:
die Biene sitzt dann still, Ring und Fotos stehen.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Die drei Pakete standen doppelt: als Deko-Kacheln (mit grossem
Waben-Wasserzeichen) ueber dem Formular und darin noch einmal als
Auswahlliste. Jetzt waehlt man sie direkt im Formular als Karten mit
sichtbarem Auswahl- und Fokuszustand; die Kacheln entfallen. Feldname
und Werte (package = KLASSIK/GESCHENK/INDIVIDUELL) sind unveraendert.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Imkerei, Herkunft und Ernten endeten mit dem letzten Absatz direkt vor
der Fusszeile. Neuer PageOutro: zwei naechste Wege als grosse
Serif-Links mit Kurztext (Ernten/Shop bzw. Herkunft/Shop).
- Kopfzeile: bei 1280 px lief sie 54 px ueber (horizontale Scrollleiste),
weil die nicht umbrechbare Slogan-Zeile unter dem Logo schon ab xl
erschien. Jetzt erst ab 2xl; geprueft 1024 bis 1920 px ohne Ueberlauf.
- Wissen: Inhaltsverzeichnis statt zehn gleicher Bloecke mit je einem
"Weiterlesen" - ganze Zeile ist der Link, Titel und Kurztext
nebeneinander. Ungenutzter Schluessel readMore entfernt.
- Produktseite: bei ein, zwei Empfehlungen steht die Ueberschrift als
erste Rasterspalte statt ueber einer einzelnen verlorenen Karte.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Die Galerie lief im Querformat 4:3 neben einer langen Detailspalte und
wirkte daneben schmaechtig. Ab md jetzt 4:5, das Glas traegt die Seite;
auf dem Handy bleibt 4:3, damit der Kaufen-Knopf oben bleibt.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Fehlt ein Foto, zeigte die Seite einen Farbverlauf mit Diagonallinien -
ein generisches Muster, das auf Produkt- und Inhaltskacheln wie
unfertige Vorlage wirkte. Jetzt: ruhige Markenflaeche mit leisem
Wabenraster, das gezeichnete Waben-Zeichen und darunter der Name in
gesperrten Serif-Versalien, wie auf dem Etikett. In kleinen Groessen
(Warenkorb, Galerie-Vorschau) bleibt per Container Query nur das Zeichen.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Aktuelle Ernte: bei ein, zwei Produkten steht die Ueberschrift als
eigene Spalte daneben, statt ueber einem halb leeren Dreierraster.
- Herkunft: Foto randlos ueber die halbe Seitenbreite, Text daneben,
ohne Rahmen.
- Philosophie: typografische Reihe mit Trennlinien und grossen
Serif-Titeln statt vier Icon-Karten.
- Region: breites Bild, der Text liegt als Tafel ueberlappend darueber.
- Geschaeftskunden: kompakte Tafel mit randlosem Bild statt eines dritten
grossen Bild/Text-Wechsels.
Inhalte, Texte und Content-Schluessel bleiben unveraendert.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Jede zweite Ueberschrift trug eine kleine Versalien-Zeile mit Wabenpunkt
("• STIMMEN", "• AKTUELL VERFUEGBAR", "• KUNDENKONTO" ...). Auf 29 Seiten
und Abschnitten wiederholt las sich das wie eine Vorlage. SectionHeading
rendert sie nicht mehr, die Prop ist entfernt; die Ueberschrift traegt den
Abschnitt allein.
Die eyebrow-Texte, die weiterhin als Seitentitel, Bildbeschriftung oder
Breadcrumb dienen, bleiben; 18 danach ungenutzte Eintraege sind aus Typ und
beiden Woerterbuechern entfernt.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Admin-Leiste (Desktop und Mobilmenue): Link "Zur Website" oberhalb von
"Angemeldet als". Der Shop-Kopf ist im Admin ausgeblendet, bisher fuehrte
von dort kein sichtbarer Weg zurueck.
- Startseite, Kundenstimmen: das grosse Schriftzeichen "„" sass auf der
Grundlinie und ragte mit leading-[0.6] in die Sternebewertung. Ersetzt
durch ein gezeichnetes SVG mit fester Hoehe und Abstand.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Neue Gruppe "Logo und Favicon" unter Admin > Inhalte mit drei Bildfeldern:
Logo (Kopfzeile), optionales Logo fuer dunkle Flaechen (Mobilmenue,
Fusszeile, Admin-Leiste; faellt sonst auf das normale Logo zurueck) und
Favicon. Leer bleibt alles wie bisher (gezeichnetes Waben-Zeichen bzw.
Standard-Icon).
- lib/brand.ts liest die Pfade und prueft sie erneut gegen das
Upload-Muster, bevor sie in ein src-Attribut gelangen.
- Statische app/icon.svg und app/favicon.ico ersetzt durch dynamische
app/icon.tsx (512 px PNG) und app/apple-icon.tsx (180 px auf Creme);
lib/brand-icon.ts rendert Upload oder Standard-Icon per sharp und liest
Dateien nur ueber resolveUploadPath. Cache-Control no-cache, damit ein
neues Favicon ohne Tage Verzoegerung erscheint.
- /favicon.ico per Rewrite auf /icon, Manifest nutzt /icon.
- Hochgeladenes Logo mit fester Hoehe und begrenzter Breite (Kopfzeile
mobil 56 px, ab sm 112 px), damit breite Wortmarken nichts sprengen.
Uploads bleiben auf Rasterbilder beschraenkt (kein SVG, XSS-Schutz).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Derselbe "Alles synchronisieren"-Knopf stand auf /admin/produkte und
/admin/ernten und sah dadurch wie zwei verschiedene Abgleiche aus. Er
sitzt jetzt einmal auf dem Dashboard, das Ergebnis des Laufs erscheint
dort oben. Die Action springt standardmaessig nach /admin zurueck; die
alten Ziele bleiben in der Whitelist erlaubt.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Sicherheit: "Jetzt per Stripe erstatten" loeste die Rueckerstattung bisher
mit einem einzigen Klick aus. Erstatten und Ablehnen einer Retoure laufen
jetzt ueber ConfirmForm; die Rueckfrage nennt Bestellnummer und Betrag.
Admin: Produkte, Bestellungen und Retouren nutzen dieselben Bausteine wie
die uebrigen Listen (Seitenkopf, Suche mit Zusatzfiltern und Archiv-
Parameter, Status-Badges, Leerzustaende). Produkttabelle ohne seitliches
Scrollen bei 1440 px: Bearbeiten ueber den Produktnamen, Charge ohne
Umbruch. Geschenk-Markierung als SVG statt Emoji. Letzte rohe Knoepfe
(Bestelldetail, ApiCore, Inhalte) auf Button umgestellt.
Mobil (390 px): Ueberlauf auf /konto (Datenexport) und /kontakt
(E-Mail-Adresse) behoben. Tippziele unter 24 px vergroessert
(Zurueck-Link, "Zum Honig", Links auf der Produktseite). Wunschliste mit
SVG-Stern statt Unicode-Zeichen.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Alle Anlege- und Bearbeiten-Formulare nutzen jetzt die Bausteine aus
components/admin-form.tsx wie das Produktformular: Rahmen mit fester
Speicherleiste, Abschnitte mit Serif-Ueberschrift (Gutschein: Code und
Rabatt / Bedingungen / Zeitraum und Status; Ernte: Angaben / Bilder /
Sichtbarkeit; Charge: Abfuellung und Bestand / Bilder), Schalter statt
Checkbox, buendige Eingabefelder in Rastern.
Benutzer bearbeiten: Sicherheitsbereich mit Status-Badges, Passwort-Reset
als Sekundaerknopf, Sperren/Loeschen in eigenem abgesetzten Bereich.
AdminField/AdminSelect koennen jetzt min und einen Hinweistext.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Neue gemeinsame Bausteine in components/admin-list.tsx: Seitenkopf mit
Hauptaktion, Suchleiste, Status-Badge (gleiche Form wie der Bestellstatus,
wartende Eintraege mit pulsierendem Punkt), Leerzustand mit Wabe und
naechstem Schritt sowie Zeilenaktionen mit einheitlichem Unterstrich-
Hover (Loeschen rot, Sperren/Zurueckziehen gedaempft).
Umgestellt: Bewertungen (SVG-Sterne statt Unicode), Benutzer, Gutscheine
(neu: Status "Abgelaufen"), Versand, Ernten, Chargen (Anlegeformulare auf
admin-form), Firmenanfragen (Status als Badge, beschriftete Auswahl) und
Audit-Log. Zahlen und Daten in Tabellen mit tabular-nums.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Gemeinsame Formularbausteine in components/admin-form.tsx (Feld,
Auswahl, Abschnitt, Schalter, Formularrahmen mit fester Speicherleiste).
Das Anlegen-Formular nutzt dieselben Abschnitte wie das Bearbeiten und
blendet Ernte/Charge sowie Geschmack per :has() aus, sobald keine
Honig-Kategorie gewaehlt ist. Eingabefelder in Rastern stehen buendig,
auch wenn Beschriftungen mobil umbrechen.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Admin ohne Shop-Kopf-/Fusszeile (HideOnAdmin); kein doppeltes <main> mehr
- Seitenleiste gruppiert (Verkauf, Sortiment, Kundschaft, Seite), aktive
Seite als wandernde Flaeche mit Wabe, Zaehler fuer Wartendes: bezahlte
offene Bestellungen, neue Retouren, unfreigegebene Bewertungen, neue
Firmenanfragen (COUNT-Abfragen erst nach Admin-/2FA-Pruefung)
- Schnellsuche (Strg/Cmd+K): Seiten und haeufige Aktionen per Tastatur,
tolerante Wortsuche, Ziele nur aus fester Liste
- Dashboard: Begruessung nach Tageszeit (Berliner Zeit), eine Leitzahl
"Umsatz heute" mit Vergleich zu gestern und Mini-Verlauf, "Zu erledigen"
mit direkten Links bzw. "Alles erledigt"-Siegel, ruhigere Kennzahl-Kacheln
- Diagramme nach dataviz-Skill: per Validator gepruefter Diagrammton
#a0722f (>=3:1; #b88a44 erreichte nur 2,93:1), gerundete Balkenkappen,
2px Luecke, Tooltip je Balken (auch per Tastatur), Meter mit Spur im
selben Ton, Rang der Top-Produkte als Wabe
- Einheitliche Admin-Details zentral: groessere Ueberschriften,
Tabellenkopf als Etikett, feste Ziffernbreite, eigene Auswahlpfeile
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Aktive Seite mit Honig-Wabe statt farbigem border-left, feines Wabenraster
in der Seitenleiste (liegt per isolate/-z-10 unter dem Menue).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Adressen im Zweierraster, Standardadresse mit Honigrahmen und Wabenmarke,
neues Adressformular in der Formular-Karte
- Retoure: Positionen als Liste mit Waben, Formular in der Karte
- Suche: Vorschlaege gleiten gestaffelt herein, Suchbegriff hervorgehoben,
Lupe wird zum Schliessen-Symbol; das Feld legt sich ueber die Navigation
statt sie zu verschieben (vorher: waagerechter Scrollbalken)
- Cookie-Hinweis als schwebende Karte; "Nur notwendige" und "Alle
akzeptieren" gleich gewichtet (vorher war Akzeptieren Waldgruen auf
Waldgruen und kaum als Knopf erkennbar)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- DHL-Verlauf: neuestes Ereignis als Wabe in Statusfarbe (pulsiert, solange
unterwegs), aeltere als leere Waben, Eintraege erscheinen gestaffelt
- Bestellung verfolgen: Suchformular in der Formular-Karte, Ergebnis als
abgehobene Karte
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Gemeinsame FormPanel-Karte fuer Passwort- und Zwei-Faktor-Seiten
- Zwei-Faktor-Code: grosses, zentriertes Ziffernfeld
- Backup-Codes als nummerierte Kacheln statt langer Liste
- Newsletter-Bestaetigung: Willkommensrabatt als Gutschein mit
gestricheltem Rand und Kopier-Knopf (bestaetigt "Kopiert")
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Artikel: Lesefortschritt als feine Honiglinie am oberen Rand, Initiale
in Serifenschrift und Honig, groesserer Titel, angenehme Zeilenlaenge
- Wissen-Uebersicht und "Weitere Artikel" als ruhige Liste mit grossen
Serifen-Titeln, Unterstrich und gezeichnetem Pfeil statt "→"
- Rechtliche Seiten mit klarerer Ueberschriften-Hierarchie
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- 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]>
- Kopfzeile: aktive Seite mit Honig-Unterstrich (auch im Handy-Menue),
einlaufende Unterstriche beim Hover, beim Scrollen Glas und Schatten;
weicht beim Runterscrollen aus und ist beim Hochscrollen sofort zurueck
(steht still, solange Warenkorb oder Menue offen sind)
- Fusszeile mit Wabenraster und einlaufenden Link-Unterstrichen
- Mengensteller: gezeichnete Plus/Minus, taktiles Druecken, tickende Zahl
- Newsletter als eigene Flaeche mit Wabenraster; Erfolg zeichnet ein
Haekchen in eine Wabe, Fehler werden als Alert angesagt
- 404: Wabe mit einer fehlenden Zelle als Bild fuer die fehlende Seite
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Konto-Kacheln: Icon in einer Wabe, die sich beim Hover mit Honig fuellt,
einlaufender Pfeil, Serifen-Titel
- Bestell-Statusleiste: Waben statt Punkte, erledigte Abschnitte fliessen
nacheinander voll, aktueller Schritt pulsiert sanft
- Status-Badge mit pulsierendem Punkt bei laufenden Bestellungen
- Leere Zustaende mit gestrichelter Wabe und Serifen-Titel
- Formularfelder: einheitlicher Honig-Schein beim Fokus
- Groessere Abschnittstitel im Konto, Bestellsummen in Serifen-Preisschrift
- Nur CSS-Animationen (Komponenten rendern auch auf Admin-Serverseiten);
reduzierte Bewegung schaltet Puls und Fuellen ab
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Hero: deutlich groessere Headline, langsam atmendes Bild, Wabenraster,
Honigtropfen als Scroll-Hinweis, Hauptaktion in Honig
- "Sechs Schritte" als Weg: eine Honiglinie fuellt sich beim Scrollen und
laesst die Waben der Schritte nacheinander aufleuchten (waagerecht ab lg,
senkrecht auf dem Handy; reduzierte Bewegung: sofort gefuellt)
- Philosophie mit Waben statt Nummern, Verantwortung als geteiltes Layout
mit mitlaufender Ueberschrift
- Bilder mit versetztem Honigrahmen, Wabenraster auf dunklen Flaechen,
grosse Anfuehrungszeichen bei den Kundenstimmen
- Knopf-Varianten "inverted"/"accent" fuer dunkle Flaechen ersetzen
!-Overrides, die den Fuell-Effekt ausgehebelt haetten
- Fix: Ueberschrift der Kundenstimmen war Waldgruen auf Waldgruen
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Fundament: feine Papierkoernung, Scrollbar/Cursor/Fokusring/Markierung in
Markenfarben, gleich breite Ziffern fuer Preise, einlaufende Unterstriche
- Knoepfe fuellen sich beim Hover von unten wie ein Glas (Honig/Waldgruen),
geben beim Klick nach
- Wabenmotiv aus dem Logo als Komponente (Raster, Markierung, Haekchen)
- In den Warenkorb: ein Honigtropfen fliegt im Bogen zum Korb, der Korb
wippt, der Zaehler springt, erst dann oeffnet die Schublade; Knopf zeigt
gezeichnetes Haekchen. Reduzierte Bewegung: Schublade oeffnet sofort.
- Produktkarten: Honigschimmer, Anheben, Serifen-Preis, gestaffeltes
Erscheinen, Pfeil-Links
- Produktseite: mitlaufendes Bild auf Wabenraster, groesserer Titel und
Preis, sich fuellende Geschmacksskala, Waben-Haekchen statt "✓"
- Shop-Filter als einheitliche Werkzeugleiste mit Schalter
- Warenkorb-Schublade: Dialog-Semantik, animierte Positionen, tickende
Summe, gestalteter leerer Zustand
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Viele Aktionen gaben keine Rueckmeldung (Retoure annehmen, Archiv,
Bewertungen, Adressen, Newsletter, Kundensperre ...) oder meldeten Erfolg,
obwohl die zugehoerige E-Mail nie rausging.
- lib/flash: Meldung als kurzlebiges Cookie; Toast im Root-Layout
(FlashToast). Ersetzt ?message=/?error= in der URL — darueber liess sich
bisher beliebiger Text als Systemmeldung im Wabenhain-Layout anzeigen
(Phishing-Hebel). Server Components leiten ueber /api/hinweis mit festen
Codes und festen Zielen weiter.
- fail()/succeed() zentral und asynchron; ein Quelltext-Test stellt sicher,
dass jeder Aufruf mit await/return erfolgt (sonst liefe eine Action nach
einem Fehler weiter).
- sendMail meldet, ob die Mail angenommen wurde; Aktionen mit Mail zeigen
ehrlich an, wenn der Versand scheitert (z.B. "Link erneut senden").
- Jede Server Action meldet jetzt Erfolg oder den Grund des Abbruchs.
- SubmitFeedback: geklickter Knopf zeigt sofort einen Ladezustand und ist
bis zur Antwort gegen Doppelklick gesperrt.
- 2FA: nach dem Einrichten zurueck ins Profil statt auf die Seite mit
"Deaktivieren".
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Abgeschlossene Eintraege wandern automatisch in einen Tab "Archiv", damit
die aktiven Listen nur zeigen, was noch zu tun ist:
- Bestellungen: versendet/abgeholt/storniert, keine offene Retoure,
seit einem Monat nicht bearbeitet
- Retouren: angenommen/abgelehnt/erstattet, seit einem Monat nicht bearbeitet
- Produkte: seit einem Monat Bestand 0 (nur Admin-Sicht, Shop unveraendert)
Die Automatik laeuft beim Oeffnen der jeweiligen Liste (hoechstens alle
5 Minuten) und holt Eintraege zurueck, sobald sie wieder offen sind.
Zusaetzlich "Archivieren"/"Wiederherstellen" per Hand; von Hand
archivierte Eintraege fasst die Automatik nicht an. Alle Verschiebungen
landen im Audit-Log.
Neue Felder archivedAt/archivedManually samt Index an orders,
return_requests und products (Migration 20260925010000_archive_fields).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
/bestellung-verfolgen holt den Sendungsverlauf serverseitig ueber die
DHL Shipment Tracking - Unified API und zeigt ihn im eigenen Layout an,
statt Kundschaft auf dhl.de weiterzuleiten.
- lib/dhl-tracking.ts: Abfrage mit API-Key, Zod-Validierung, Timeout,
Cache pro Sendung (unterwegs 10 min, zugestellt 12 h, Fehler 2 min)
- Abfrage erst nach erfolgreicher Pruefung von Bestellnummer + E-Mail,
damit die Seite nicht als offener Proxy zu DHL dient
- Ohne freigeschalteten Key bleibt die Seite unveraendert (Nummer + Link)
- Optional DHL_TRACKING_API_KEY, falls das Tracking in eigener DHL-App liegt
- Datenschutzerklaerung um die Statusabfrage bei DHL ergaenzt
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Regression aus dem vorigen Commit (Turnstile per Prop statt process.env):
sobald das CAPTCHA auf der Newsletter-Anmeldung tatsaechlich rendert, sprengt
es die Eingabezeile.
Das Formular war "flex flex-col sm:flex-row gap-3 max-w-lg" ohne flex-wrap,
und das Widget lag als dritter Eintrag in derselben Zeile. Ab 640 px teilten
sich damit Eingabefeld, Knopf und ein rund 300 px breites CAPTCHA eine Zeile
von 512 px. Auf wabenhain.com bei 1440 px nachgemessen: Eingabefeld 34x71,
Knopf 174x71, CAPTCHA 300x71.
Solange kein Sitekey ankam, fiel das nicht auf — das Widget erschien nie, die
Zeile hatte nur zwei Eintraege. Der Fehler lag also schon laenger im Markup
und wurde erst sichtbar, als das CAPTCHA wie vorgesehen erschien.
Jetzt bilden Eingabefeld und Knopf eine eigene Zeile, das CAPTCHA steht
darunter. Die Firmenanfrage ist nicht betroffen, sie stapelt ueber space-y-5.
Dazu zwei Einstellungen am Widget selbst, die von Anfang an gefehlt haben:
- language: Turnstile folgte der BROWSER-Sprache statt der Seitensprache. Auf
der deutschen Seite stand englisch "Verify you are human".
- theme: "light": Turnstile folgte dem Farbschema des Betriebssystems. Wer es
auf "dunkel" stehen hat, bekam einen schwarzen Kasten mitten in einen
Abschnitt, den diese Seite ausschliesslich in Creme und Waldgruen zeichnet.
Die Seite hat keinen Dunkelmodus, "auto" ist hier also immer falsch.
Beides betrifft alle Formulare, nicht nur den Newsletter.
tsc: 0 Fehler. vitest: 359/359 gruen. eslint: 0 Warnungen. next build: gruen.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Die Ortszeile ueber der Ueberschrift stand in honey-light auf einem Foto mit
25 % Grundabdunklung plus einem to-tr-Verlauf, der die Textspalte bei rund
44 % Gesamtdeckung liegen liess. Gegen ein helles Motiv (Holz, Himmel)
gerechnet ergab das:
Ortszeile 1,34:1 (WCAG AA verlangt 4,5:1 bei 12 px)
Ueberschrift 2,73:1 (verlangt 3:1 bei grosser Schrift)
Fussnote 1,90:1
Lighthouse meldet davon nichts und gibt der Seite weiterhin 100 fuer
Accessibility — Kontrast von Text ueber Bildern laesst sich nicht statisch
pruefen, weil der Hintergrund vom hochgeladenen Motiv abhaengt. Genau deshalb
muss die Abdunklung den schlechtesten Fall abdecken und nicht das Bild, das
gerade hinterlegt ist.
Drei Aenderungen, alle nachgerechnet:
- Verlauf ab md waagerecht (85 % -> 70 % -> 0) statt diagonal. Die Textspalte
endet bei etwa 45 % Breite und liegt damit ganz im dunklen Teil, waehrend
die rechte Bildhaelfte unangetastet bleibt.
- Auf schmalen Schirmen senkrecht, weil der Text dort ueber die volle Breite
laeuft und rechts sonst in den durchsichtigen Teil geriete.
- Neuer Farbton honey-pale (#e4cfa4) fuer die Ortszeile; honey-light haelt
4,5:1 selbst bei 89 % Abdunklung nicht (4,14:1). Fussnote von cream/60 auf
cream/80.
Ergebnis in der Textspalte: Ortszeile 4,81:1 bis 6,97:1, Ueberschrift 6,41:1
bis 9,28:1, Fussnote 4,80:1 bis 6,59:1 — AA auf allen Breiten.
tsc: 0 Fehler. vitest: 359/359 gruen. eslint: 0 Warnungen. next build: gruen.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Beide Formulare lasen NEXT_PUBLIC_TURNSTILE_SITE_KEY direkt aus process.env,
und zwar im Modulrumpf einer Client Component. NEXT_PUBLIC_-Variablen werden
dort zur BUILD-Zeit ins Bundle eingebacken. Gesetzt wird der Wert hier aber
erst zur Laufzeit (k8s/01-configmap.yaml) — im Docker-Build existiert er nicht
und wird auch nicht als Build-Arg hereingereicht.
Der Wert war damit dauerhaft undefined, das Widget erschien nie. Solange
TURNSTILE_SECRET_KEY ebenfalls leer ist, faellt das nicht auf:
verifyTurnstileToken gibt ohne Secret bewusst true zurueck. Sobald das Secret
aber gesetzt wird — also genau beim Scharfschalten des Schutzes —, verlangt
der Server einen Token, den der Browser nirgends erzeugen kann. Newsletter-
Anmeldung und Firmenanfrage haetten dann jede Eingabe abgewiesen, ohne dass
irgendwo ein Fehler sichtbar wird.
Alle uebrigen Formulare (Login, Registrierung, Passwort-Reset, Zwei-Faktor,
Checkout, Verfuegbarkeits-Benachrichtigung) lesen den Wert bereits im Server
Component und reichen ihn als Prop weiter — dort wirkt die Laufzeit-Variable.
Diese beiden folgen jetzt demselben Muster.
Bewusst so und nicht ueber ein Docker-Build-Arg geloest: ein Build-Arg wuerde
den Schluessel ins Image einbacken, womit ein Schluesselwechsel einen Neubau
braeuchte und die ConfigMap fuer diese beiden Stellen wirkungslos bliebe. Ein
einziger Weg fuer alle Formulare ist weniger fehleranfaellig als zwei.
Hinweis fuer den Betrieb: der Schutz ist damit noch nicht aktiv. Dafuer
muessen NEXT_PUBLIC_TURNSTILE_SITE_KEY (ConfigMap) UND TURNSTILE_SECRET_KEY
(Secret) gesetzt sein — beide oder keiner.
tsc: 0 Fehler. vitest: 359/359 gruen. eslint: 0 Warnungen. next build: gruen.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Hero, ScrollReveal und ProductCard starten mit opacity 0 und werden per
motion/react eingeblendet. Diese Startwerte stehen als inline-style bereits im
server-gerenderten HTML — auf wabenhain.com an 38 Elementen nachgezaehlt. Laeuft
kein JavaScript, blendet sie niemand wieder ein: der Besucher sieht das
Hero-Bild und darunter eine leere Seite. Kein Teilausfall, sondern der
komplette Inhalt.
Fuer Suchmaschinen war das unkritisch, der Text steht im HTML. Fuer Menschen
mit deaktiviertem JavaScript war es ein Totalausfall.
Die drei Bausteine tragen jetzt data-reveal, und ein noscript-Block im
Root-Layout hebt darueber die Startwerte auf. Das Attribut statt eines
Treffers auf [style*="opacity:0"] ist der Kern der Sache: Warenkorb-Schublade,
Menue und Cookie-Banner blenden ueber AnimatePresence ebenfalls mit opacity 0
ein, sollen ohne JavaScript aber verborgen BLEIBEN. Sie tragen das Attribut
nicht.
!important ist hier notwendig und nicht nachlaessig — einen inline-style
ueberschreibt sonst nichts.
Grenze der Loesung: noscript deckt "JavaScript deaktiviert" ab. Laedt das
Bundle dagegen und schlaegt erst spaeter fehl, greift es nicht; dafuer waere
ein Zeitgeber noetig, der die Animation im Normalfall stoeren wuerde.
tsc: 0 Fehler. vitest: 359/359 gruen. eslint: 0 Warnungen. next build: gruen.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Im Anmeldeformular standen CAPTCHA, Absenden-Knopf und der
"Passwort vergessen"-Link in EINER Flex-Zeile ohne Umbruch:
<div className="flex items-center justify-between gap-4">
Das Turnstile-Widget ist ein iframe mit FESTER Breite von rund 300 px und
schrumpft nicht mit. Die Zeile steht zudem in einer der beiden Spalten
der Kontoseite, hat also nur die halbe Breite zur Verfuegung. Der Platz
reichte daher nie — die Elemente schoben sich uebereinander, und zwar
nicht nur auf schmalen Bildschirmen.
Jetzt steht das Widget auf einer eigenen Zeile, Knopf und Link darunter
in einer Zeile mit "flex-wrap". Das Registrierungsformular daneben hat es
schon immer so gemacht; die beiden sind damit endlich gleich aufgebaut.
Damit das an keiner Stelle wiederkommen kann, traegt das Widget selbst
jetzt "w-fit max-w-full overflow-x-auto". Das ist kein Schoenheitsmittel,
sondern eine Zusicherung: der iframe bleibt in jedem Fall innerhalb
seines Kastens. Ist der zu schmal, laesst sich der Inhalt seitlich
schieben, statt das Layout zu zerlegen — unabhaengig davon, wie eine
kuenftige Seite ihn einbettet.
Die uebrigen sieben Einbindungen (Registrierung, Passwort vergessen,
Passwort zuruecksetzen, Zwei-Faktor-Code, Produktseite, Firmenanfrage,
Newsletter, Checkout) habe ich geprueft: dort steht das Widget bereits
auf einer eigenen Zeile.
tsc: 0 Fehler. vitest: 359/359 gruen. eslint: 0 Warnungen.
"next build" laeuft durch.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Symptom: Das CAPTCHA fehlte gelegentlich und war erst nach einem
Neuladen der Seite da.
Ursache ist kein Wettlauf, sondern ein deterministischer Ausfall.
next/script fuehrt onLoad fuer eine bereits geladene Quelle NICHT erneut
aus — der Schluessel ist dabei die URL (id || src) und damit fuer alle
Widgets derselbe. In next/dist/client/script.js:
const cacheKey = id || src;
// Script has already loaded
if (cacheKey && LoadCache.has(cacheKey)) {
return;
}
Das Widget wartete aber genau auf dieses onLoad, bevor es
turnstile.render() aufrief. Also bekam nur das ERSTE Widget nach einem
echten Seitenaufbau eines; jedes weitere blieb leer:
- die Kontoseite zeigt Anmeldung UND Registrierung untereinander, also
zwei Widgets — eines davon war immer tot;
- jedes Formular, das ueber einen Link im Seitenverlauf erreicht wurde
statt ueber F5.
Weil die Newsletter-Anmeldung im Footer steht, laedt das Skript auf
praktisch jeder Seite. Damit fuehrte fast jeder Weg durch die Seite in
den kaputten Zustand — und ein Neuladen half, weil das betroffene Widget
dann wieder als erstes an der Reihe war. Das erklaert das "manchmal".
Folgen waren groesser, als es aussieht: ohne Widget kein Token, ohne
Token weist der Server das Formular ab. Anmeldung, Registrierung,
Passwort-Reset, Zwei-Faktor-Eingabe und Checkout waren blockiert, ohne
dass irgendwo eine Fehlermeldung stand.
Das Widget laedt das Skript jetzt selbst, ueber ein von allen Instanzen
geteiltes Versprechen, und haengt an keinem Ereignis mehr, das man
verpassen kann: es zaehlt allein, ob window.turnstile vorhanden ist.
Dazu drei Dinge, die vorher fehlten:
- "render=explicit", damit Turnstile nicht zusaetzlich selbst nach
Containern sucht;
- turnstile.remove() beim Verlassen, statt das Widget stehen zu lassen;
- ein SICHTBARER Hinweis mit "Erneut versuchen", wenn das Skript nicht
laedt (Netzaussetzer, Inhaltsblocker). Vorher blieb dort eine leere
Flaeche, und das Formular liess sich ohne erkennbaren Grund nicht
abschicken.
Der Callback liegt in einem Ref: eine bei jedem Render neu erzeugte
Funktion haette das Widget sonst abgebaut und mit ihm ein bereits
geloestes CAPTCHA.
Fuenf Regressionstests (src/components/turnstile-widget.test.tsx), allen
voran "baut sich auch dann auf, wenn das Skript schon geladen ist". Per
Mutationstest geprueft, dass sie ohne den Fix rot werden.
tsc: 0 Fehler. vitest: 333/333 gruen (zuvor 328). eslint: 0 Warnungen.
"next build" laeuft durch.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Die Anfrage an die Places API setzte "fields=review". Richtig ist "reviews"
(Plural). Google antwortet auf den falschen Wert mit INVALID_REQUEST, die
Funktion gab daraufhin null zurueck, und die Startseite fiel auf die
statischen Beispiel-Stimmen zurueck.
Nach aussen sah das voellig normal aus — eine Bewertungen-Sektion mit
Zitaten und Sternen war ja da. Nur waren es nie die echten. Genau deshalb
blieb der Tippfehler unbemerkt.
Damit das nicht wieder passieren kann, meldet die Anbindung jetzt, WARUM sie
nichts liefert. Vorher fuehrten alle Fehlerfaelle stumm zum selben Bild:
[google-reviews] Places API meldet "REQUEST_DENIED": ...
REQUEST_DENIED (Schluessel ungueltig oder Places API im Projekt nicht
aktiviert), NOT_FOUND (Place-ID stimmt nicht) und OVER_QUERY_LIMIT
(Kontingent erschoepft oder keine Abrechnung hinterlegt) sind damit von
aussen unterscheidbar.
Weitere Korrekturen:
- Zeitlimit von fuenf Sekunden. Ohne das haengt die Startseite an einer
nicht antwortenden fremden API — dieselbe Absicherung wie beim
Passwort-Leak-Abgleich.
- Rezensionen verlinken auf ihre Quelle bei Google. Die Nutzungsbedingungen
der Places API verlangen die Zuordnung; bisher stand ein fremdes Zitat
unbelegt auf der Seite. author_url und profile_photo_url wurden aus der
Antwort schlicht verworfen.
- Rezensionen ohne Text werden uebersprungen. Reine Sternebewertungen sind
in der Antwort enthalten und waeren als Zitat eine leere Sprechblase; "text"
ist laut Google optional, war hier aber als Pflichtfeld typisiert.
README: Einrichtung, Bedeutung der Statuscodes und der Hinweis, dass Google
den genutzten place/details-Endpunkt als Legacy fuehrt — bleibt
REQUEST_DENIED trotz gueltigem Schluessel bestehen, ist die Umstellung auf
die Places API (New) faellig.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Bisher gab es zwei Knoepfe auf zwei Seiten, und ihre Reihenfolge war nicht
beliebig: eine Charge braucht ihre Ernte als Pflicht-Fremdschluessel, ein
Produkt entsteht je Abfuellung und braucht die zugehoerige Charge. Wer mit
den Produkten anfing, bekam reihenweise "wartet auf den Ernten-Sync" und
musste danach noch einmal klicken. Diese Reihenfolge gehoert in den Ablauf,
nicht in den Kopf der bedienenden Person.
syncApiCoreAction ersetzt beide Actions und laeuft Ernten -> Chargen ->
Produkte in einem Durchgang. "Alles synchronisieren" steht auf beiden Seiten
und springt danach dorthin zurueck, von wo er ausgeloest wurde. Das Ziel
kommt aus einem Formularfeld und wird gegen eine feste Liste geprueft — ein
ungeprueftes returnTo waere eine offene Weiterleitung.
Nebeneffekt: die Abfuellungen werden nur noch EINMAL von ApiCore geladen.
Vorher holten Chargen- und Produkt-Sync dieselbe Liste getrennt.
Knopf und Ergebnisanzeige liegen jetzt in einer gemeinsamen Komponente
(components/apicore-sync-panel.tsx) statt zweimal in zwei Seiten. Die
Ergebnisse stehen als ein Block beieinander, je eine Zeile fuer Ernten,
Chargen und Produkte, plus die beiden Hinweise (Abfuellung ohne Charge,
ausgelaufener Artikel) als Hinweis statt als Fehler.
Damit wurden zehn i18n-Schluessel ueberfluessig, die vorher die beiden
getrennten Anzeigen bedienten; sie sind aus dem Vertrag und beiden
Sprachdateien entfernt.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Live beobachtet: der Upload scheitert mit HTTP 502, nicht mit 504. Das ist
ein wesentlicher Unterschied. 502 bedeutet, dass der vorgeschaltete Proxy
vom Backend gar keine Antwort bekam — der Prozess hat die Verbindung mitten
im Request fallen gelassen. Eine Zeitueberschreitung waere 504. Die bisher
verfolgte Timeout-Spur war damit falsch.
Uebrig blieb als Erklaerung der Arbeitsspeicher, und dort lag noch die
eigentliche Ursache: request.formData() liest den GESAMTEN Body in den
Speicher, bevor eine einzige Zeile eigener Code laeuft. Der vorherige
Streaming-Umbau hat nur die zusaetzlichen Kopien danach beseitigt
(arrayBuffer, Buffer, readFile) — diese erste nicht. Bei einem Video bis
100 MB genuegt das, um zusammen mit dem Grundverbrauch des Servers das
Speicherlimit des Pods zu sprengen; der Kernel beendet den Prozess
(OOMKilled), und der Proxy meldet 502.
Die Datei kommt jetzt als roher Request-Body statt als multipart/form-data.
Damit laesst sich request.body direkt auf die Platte durchreichen, und der
Speicherbedarf haengt nicht mehr von der Dateigroesse ab. Ein Formularfeld
wurde ohnehin nicht gebraucht, es wird genau eine Datei uebertragen. Die
Groessengrenze prueft jetzt ein mitzaehlender Transform-Stream, weil
Content-Length nur eine Ankuendigung des Clients ist.
Gemessen im Produktions-Build mit einem 95-MB-Video: Status 200 nach 5,9
Sekunden, Arbeitsspeicher des Servers von 67 MB auf in der Spitze 149 MB —
also rund ein Drittel des Limits von 512Mi, mit dem der Pod bisher lief.
Ausserdem berichtigt: der eigene Meldungstext behauptete bei 502 "fast
immer eine Zeitueberschreitung". Das ist falsch und hat die Fehlersuche in
die falsche Richtung gelenkt. 502 und 504 werden jetzt getrennt erklaert —
502 als weggebrochenes Backend (meist Speichermangel), 504 als echtes
Zeitlimit.
Die letzte Stelle, an der ein hochgeladenes Bild noch als rohes <img>
ausgeliefert wurde. Der Warenkorb lud damit fuer JEDE Position das
vollstaendig gespeicherte Bild — bis MAX_IMAGE_DIMENSION Kantenlaenge, also
durchaus ein paar hundert Kilobyte — nur um es auf 64 bzw. 80 Pixel zu
verkleinern. Bei mehreren Positionen summierte sich das spuerbar, gerade im
Mobilfunknetz, und betraf sowohl die Warenkorb-Schublade als auch die
Warenkorbseite.
Der bisherige eslint-disable-Kommentar begruendete das rohe <img> mit einer
"noch unbekannten externen Bildquelle". Das trifft nicht zu: das Bild kommt
aus Product.images[0] und liegt damit immer same-origin unter /uploads/...
(beim Speichern geprueft von isValidUploadPath in lib/uploads.ts).
next/image ist dafuer zustaendig und liefert jetzt eine 80px-Variante statt
der Originaldatei.
Beide Verwendungsstellen bekommen dieselbe leichte Eckenrundung wie die
uebrigen Bilder; vorher war die Schublade rounded-sm und die Warenkorbseite
gar nicht gerundet.
Vier offene Befunde standen einem sauberen Produktionslauf im Weg:
- recently-viewed-section und site-chrome riefen setState direkt im
Effektrumpf auf, um einen Leerzustand herzustellen ("keine angesehenen
Produkte" bzw. "Eingabe kuerzer als zwei Zeichen"). Beides ergibt sich
unmittelbar aus vorhandenen Werten und wird jetzt beim Rendern
abgeleitet — das spart je einen zusaetzlichen Renderdurchlauf. Bei der
Suche verschwindet die Vorschlagsliste dadurch sofort beim Leeren des
Feldes statt erst nach dem naechsten Effektlauf.
- STAT_KEYS im Admin-Dashboard wurde ausschliesslich als Typ verwendet,
landete als `as const`-Array aber trotzdem im Bundle. Ersetzt durch
einen Typ-Alias.
- Ein DHL-Test importierte DhlApiError, ohne es zu benutzen, und pruefte
nur den Meldungstext — er haette damit jeden beliebigen anderen Fehler
mit passendem Text durchgehen lassen. Er prueft jetzt zusaetzlich den
Fehlertyp, wie seine beiden Geschwistertests.
Stand danach: eslint ohne Beanstandung (vorher 2 Fehler, 2 Warnungen),
tsc ohne Fehler, 201 Tests gruen, Produktions-Build erfolgreich.
Drei Gruende, warum Hero-Bild und -Video spuerbar spaet erschienen:
1. Das Standbild des Hero-Videos steckte im poster-Attribut. poster nimmt
nur eine fertige URL und laedt die hochgeladene Datei damit in voller
gespeicherter Groesse — am Bildoptimierer vorbei, ohne Ruecksicht auf
das Geraet. Es liegt jetzt als eigene ContentImage-Ebene unter dem
Video, laeuft also durch next/image. Gemessen: auf einem 500px breiten
Viewport kommt statt der vollen Datei die 640px-Variante als WebP an.
Das Video darueber ist transparent, bis es eigene Bilddaten hat — der
Uebergang bleibt unsichtbar.
2. Die aeusserste Hero-Section startete bei opacity 0 und wurde erst per
Animation nach der Hydration sichtbar. Das groesste Element im ersten
Viewport wurde damit um die gesamte JS-Ladezeit zurueckgehalten. Die
Animation dort ist entfernt; der gestaffelte Aufbau der Textspalte
bleibt unveraendert.
3. next/image erhielt weiterhin die Prop "priority". Die ist seit
Next.js 16 abgekuendigt (siehe node_modules/next/dist/docs, Changelog
v16.0.0) und erzeugt nur noch einen <link rel="preload">, setzt am
<img> selbst aber weder fetchpriority noch loading — im Trace kam das
Hero-Bild deshalb trotz "priority" mit Prioritaet "Low" an. Die
Dokumentation nennt dafuer ausdruecklich loading="eager" und
fetchPriority="high" als Ersatz; ContentImage bildet seine unveraendert
heissende priority-Prop jetzt darauf ab, die Produktgalerie ebenso.
Danach bestand die LCP-Discovery-Pruefung von Chrome vollstaendig,
vorher scheiterte sie an fetchpriority.
Ausserdem bekommen Inhaltsbilder leicht abgerundete Ecken (8px), Platzhalter
und echtes Bild gleichermassen, damit die Form beim Pflegen eines Bildes
nicht umspringt. Randlose Flaechen (Hero ueber die volle Fensterbreite)
bleiben bewusst eckig — dort saesse die Rundung an der Fensterkante.
Geprueft im Produktions-Build gegen Chrome: CLS 0.00, kein horizontaler
Ueberlauf bei schmalem Viewport, Video spielt (readyState 4), Hero-Bild
loading="eager" + fetchpriority="high", uebrige Bilder lazy.
Alle drei Admin-Upload-Felder riefen response.json() ungeprueft auf. Kam
statt JSON eine HTML-Seite zurueck, warf der Parser
Unexpected token '<', "<!DOCTYPE "... is not valid JSON
und genau dieser Text landete im Formular. Damit war die eigentliche
Ursache vollstaendig verdeckt: weder der HTTP-Status noch die Schicht,
die abgebrochen hat, waren daraus ablesbar.
Der Punkt dabei: die Upload-Route-Handler antworten ausnahmslos mit JSON,
auch in jedem Fehlerfall — lokal verifiziert bis hinauf zur
Limit-Ueberschreitung (105 MB -> 413 mit JSON-Body). Trifft im Browser
trotzdem HTML ein, stammt die Antwort gar nicht aus der Anwendung,
sondern aus einer Schicht davor: Reverse-Proxy, CDN oder Gateway, das den
Request wegen zu grossem Body abgewiesen oder wegen Zeitueberschreitung
beim Transcoding abgebrochen hat.
readUploadResponse() in lib/upload-client.ts prueft deshalb zuerst den
Content-Type und reicht andernfalls den HTTP-Status nach oben, statt zu
parsen. Die Felder zeigen daraus eine Meldung, die den Status nennt und
die wahrscheinliche Ursache benennt (413 = vom Proxy abgewiesen,
502/503/504/522/524 = Zeitueberschreitung beim Umrechnen, sonst
unerwartet). Ein Verbindungsabbruch, bei dem fetch() selbst wirft, wird
getrennt gemeldet.
Das behebt nicht die Ursache jenseits der Anwendung — es macht sie zum
ersten Mal ablesbar, statt sie hinter einem Parser-Fehler zu verbergen.
Die Vorschaukacheln in admin/inhalte, admin/produkte und admin/ernten
luden das vollstaendig gespeicherte Bild, um es auf 80x80 bzw. in einer
Vierer-Rasterkachel darzustellen. Bei einem Produktformular mit mehreren
Bildern summierte sich das zu mehreren Megabyte allein fuer Miniaturen.
Der urspruengliche Kommentar ("next/image lohnt hier nicht") stimmte, als
Uploads klein waren; gespeichert wird inzwischen bis MAX_IMAGE_DIMENSION
Kantenlaenge. Beide Felder rendern die Vorschau jetzt ueber next/image
mit passendem sizes, der Optimierer liefert damit genau die gebrauchte
Groesse. Layout und Bedienung bleiben unveraendert.
loop-Attribut war bereits gesetzt, aber manche Browser (u.a. aeltere
iOS-Safari-Versionen) starten ein Video nach "ended" nicht zuverlaessig
neu, wenn autoplay/muted/playsinline kombiniert sind. onEnded erzwingt
jetzt zusaetzlich den Neustart, damit die Dauerschleife garantiert nie
sichtbar stehen bleibt.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Live-Check des hochgeladenen Hero-Videos zeigte: die Headline lag streckenweise
direkt ueber hellen Honigglas-Etiketten, der bisherige rein vertikale
Verlauf (bg-gradient-to-t, 70/10) verdunkelt dort zu wenig, wo die
Textspalte tatsaechlich sitzt (vertikal mittig links). Zusaetzliche
gleichmaessige 25%-Abdunklung ueber die gesamte Flaeche plus staerkerer,
diagonaler Verlauf (to-tr, 70/25) gezielt hinter der Textspalte - visuell
gegen das echte Video verifiziert (Screenshot-Vergleich alt/neu).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Bild-/Video-Upload-Felder (admin/inhalte, admin/produkte, admin/ernten)
zeigten den unstyled Browser-Standard-Datei-Input — kaum als
Bedienelement erkennbar. Input jetzt versteckt, stattdessen ein
richtiger, zum Admin-Design passender Button, der den Datei-Dialog
oeffnet.
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 audit of /konto found the same class of bug as the
prior checkout/cart commit, one shade lighter: text-ink/50 measures
3.08:1 against the cream background, under the 4.5:1 WCAG AA minimum
for normal-size text. Grepped for every other use of the same token
(login divider, language switcher, sold-out label, admin image-upload
status text, admin hint text) and bumped them all to text-ink/70
(5.61:1, same value used in the earlier checkout fix) for one
consistent, compliant muted-text shade. Verified: tsc clean, 151/151
tests pass, /konto Lighthouse accessibility 96 -> 100.
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.