v2.4: Medienbibliothek+WebP, Varianten-Matrix, Litestream-Backups, intelligentere Analytics

P1 Medien: eigener Admin-Bereich /admin/medien (Grid, Mehrfach-Upload, Drag&Drop, Alt-Text, URL kopieren, Loeschen). Upload konvertiert JPG/PNG via sharp zu WebP (Qualitaet 82, max 2000px), Original wird verworfen; WebP/SVG/GIF/AVIF unveraendert; Konvertierungsfehler -> Original behalten statt 500. media um alt/width/height erweitert. Wiederverwendbarer Medien-Picker (public/media-picker.js) ersetzt den URL-Prompt im Block-Editor, Produkt-Editor (Karte/Galerie/Varianten-Bild), Slides und Popups. JSON-Quelle /api/admin/media (session-gesichert).

P2 Varianten: products.options_json + Tabelle product_variants. Produkt-Editor mit Options-Definition + Matrix-Generator (Preis-Override/Bestand/SKU/Bild/aktiv je Variante). PDP-Selektoren -> Variante; Cart/Checkout tragen sku+Options, Order-Item bekommt sku/variant, Variantenpreis serverseitig verifiziert. Produkte ohne Optionen unveraendert.

P3 Litestream: Binary im Dockerfile, docker-entrypoint.sh (Restore+replicate nur bei LITESTREAM_REPLICA_URL, sonst reiner Node-Start), litestream.yml, Backup-Status unter Einstellungen, README + .env.example.

P4 Analytics: Bestseller, Top-Suchbegriffe, Umsatz/Quelle, Umsatz-Zeitreihe, AOV, Wiederkaufrate, Lager-Warnungen.

Neue Dep sharp. +19 Unit-Tests (49 gesamt gruen), Build + Smoke (P1-P4) gruen.
This commit is contained in:
2026-06-18 08:09:57 +00:00
parent 30c41c355e
commit 50dfca59e1
28 changed files with 1147 additions and 66 deletions
+8 -1
View File
@@ -19,7 +19,7 @@
}
function add(item) {
var c = read();
var ex = c.find(function (i) { return i.slug === item.slug && i.size === item.size; });
var ex = c.find(function (i) { return i.slug === item.slug && (i.sku || '') === (item.sku || '') && i.size === item.size; });
if (ex) ex.qty += item.qty || 1; else c.push(item);
write(c);
track('add_to_cart', (item.priceCents || 0) * (item.qty || 1), { slug: item.slug });
@@ -44,7 +44,14 @@
updateBadge();
document.querySelectorAll('[data-add-to-cart]').forEach(function (btn) {
btn.addEventListener('click', function () {
if (btn.disabled) return;
var p = JSON.parse(btn.getAttribute('data-product') || '{}');
if (p.hasVariants) {
if (!p.variant || !p.variant.options) { var st = document.getElementById('variantStatus'); if (st) { st.textContent = 'Bitte zuerst eine Variante wählen.'; st.style.color = '#b3261e'; } return; }
var label = Object.keys(p.variant.options).map(function (k) { return p.variant.options[k]; }).join(' / ');
add({ slug: p.slug, name: p.name, size: label, priceCents: p.priceCents, image: p.image, qty: 1, sku: p.variant.sku || '', variant: p.variant.options, options: p.variant.options });
return;
}
var sizeSel = document.querySelector('.size-chip.active');
var size = sizeSel ? sizeSel.getAttribute('data-size') : (p.sizes && p.sizes[0]) || 'One Size';
add({ slug: p.slug, name: p.name, size: size, priceCents: p.priceCents, image: p.image, qty: 1 });