50dfca59e1
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.
148 lines
15 KiB
Markdown
148 lines
15 KiB
Markdown
# hd-commerce
|
||
|
||
**hd-commerce** ist ein eigenständiges, brand-neutrales E-Commerce-Backend von Heidrich Digital: eine wiederverwendbare Astro-SSR-Anwendung mit Commerce-Engine (SQLite), Session-gesichertem Admin, Visual-Block-Builder, KI-/MCP-Editierbarkeit, JSON-API und einem schlanken, neutralen Demo-Storefront.
|
||
|
||
Die mitgelieferte Demo-Instanz heißt **„Brittas Nähkiste"** (Kurzwaren/Nähbedarf) und dient nur als Beispiel. Name, Akzentfarbe, Texte und Logo-Wortmarke sind über die Einstellungen frei anpassbar — derselbe Code läuft für beliebige Shops.
|
||
|
||
## Features
|
||
|
||
- **Storefront** (hell, editorial, neutral): Startseite mit Announcement-Bar, Slider, Kategorien, Featured-Produkten und Newsletter; Shop-Katalog; Produktdetailseiten; Warenkorb (localStorage); Checkout; Inhaltsseiten aus der DB — wahlweise klassisch oder über den **Block-Builder** gestaltet.
|
||
- **Admin** (Premium, „Warmth & Approachability"): Session-Login statt Browser-Basic-Auth, Rollen (Owner/Redaktion/Versand), Command-Palette (⌘K), Toasts, aufgewertetes Dashboard mit KPI-Trends, Sparkline, Aktivitäts-Feed und Schnellaktionen.
|
||
- **Visual-Block-Builder**: Vollbild-Editor mit Block-Liste (Drag/▲▼/duplizieren/löschen), Live-Vorschau (Desktop/Mobil) und Block-Einstellungen. Block-Typen: Hero, Rich-Text, Bild, Galerie, Slider, Feature-Grid, Produkt-Grid, CTA-Banner, Abstand, Roh-HTML.
|
||
- **KI-Editierbarkeit**: token-gesicherte Admin-JSON-API (`/api/admin/*`) plus maschinenlesbares Manifest (`/api/admin`, `/ai-admin.txt`) und ein **MCP-Server** (`mcp/`).
|
||
- **Gutschein-/Rabatt-Engine** (v2.1): Codes vom Typ `percent` / `fixed` / `freeshipping` mit Zeitplan, Mindestbestellwert, Gesamt- und Pro-Kunde-Limit, „geheim" (nicht öffentlich listbar) und „automatisch" (greift ohne Code, wenn Bedingungen erfüllt). Admin-Bereich **Rabatte** (Owner/Redaktion) mit Status-Badges (Aktiv/Geplant/Abgelaufen/Aufgebraucht/Inaktiv); Storefront-Einlösung im Checkout über `/api/discount`; serverseitige Re-Validierung in `/api/checkout`; Stripe-Coupon-Anbindung. Popups können einen Code anzeigen (+ Kopieren-Button) — auch für gezielt verteilte geheime Codes; Popup-Stile `modal` / `slidein` / `bar`.
|
||
- **Verkaufsfertig-Fundament (v2.2):**
|
||
- **Payment-Abstraktion** (`src/lib/payments.js`): einheitliche Schnittstelle für **Mollie** (Default, REST), **Stripe** und **Demo**. Provider via Setting `payment_provider` / ENV `PAYMENT_PROVIDER`, sonst Auto-Wahl nach vorhandenen Keys. Mollie-Webhook unter `/api/payments/webhook`; ungültiger Key ⇒ sauberer Demo-Fallback statt Fehler.
|
||
- **DACH-Recht**: MwSt-Ausweis pro Produkt (`mwst` 0/7/19), **Grundpreis** (PAngV) über `base_amount`/`base_unit`/`base_price_per`; Warenkorb/Checkout zeigen Zwischensumme, enthaltene MwSt (nach Satz gruppiert) und **Versand vor dem Bezahlen**.
|
||
- **Versandzonen** (Tabelle `shipping_zones`, Admin „Versand"): länderbasierte Preise mit Gratis-ab-Schwelle; Helper `shippingFor(country, subtotal)`; Checkout berechnet Versand serverseitig neu.
|
||
- **Bestell-/Versandmails** (`src/lib/mailer.js`): Provider **Listmonk** (Transactional-API) / **SMTP** (nodemailer) / **Log-Fallback** (Tabelle `email_log`, Admin „E-Mail-Log"). Gebrandete Bestellbestätigung bei bezahlter Bestellung.
|
||
- **Feature-Flags**: Module pro Shop abschaltbar (`feature_newsletter`, `feature_accounts`, `feature_reviews`, `feature_wishlist`, `feature_abandoned_cart`, `feature_search`) über Admin → Einstellungen → Module; Helper `feature(key)`.
|
||
- **Feature-Module (v2.3):** über die jeweiligen Flags ge-gatet — ist ein Flag aus, verschwinden Storefront-Elemente, Routen liefern 302/404 und der Admin-Nav-Punkt entfällt.
|
||
- **Volltextsuche** (`feature_search`): Suchfeld im Storefront-Header → Ergebnisseite `/suche?q=` (SSR, SQLite-`LIKE`, case-insensitiv über Name/Kurzname/Beschreibung/Material/Kategorie), Treffer als Produktkarten, „keine Treffer"-Zustand.
|
||
- **Merkliste** (`feature_wishlist`): Herz-Button auf Produktkarten & Detailseite, Speicherung clientseitig (localStorage, `public/wishlist.js`), Seite `/merkliste`.
|
||
- **Kundenkonten + Adressbuch** (`feature_accounts`): eigene Kunden-Session (Cookie `hdc_customer`, getrennt vom Admin; scrypt-Hash). `/konto/registrieren`, `/konto/anmelden`, `/konto` (Bestellhistorie + Adressbuch), `/konto/abmelden`. Tabelle `customer_addresses`; Checkout füllt die Adresse vor und ordnet die Bestellung dem Konto zu (`orders.customer_id`). Gast-Checkout bleibt möglich.
|
||
- **Bewertungen** (`feature_reviews`): Tabelle `reviews` (Sterne 1–5, Moderation `approved`). Formular auf der Produktseite (`/api/review`, speichert `approved=0`), Anzeige von Durchschnitt + freigegebenen Reviews, optionale `aggregateRating` im Produkt-JSON-LD. Admin-Bereich **Bewertungen** (Owner/Redaktion): Freigeben/Verbergen/Löschen, Zähler offener Reviews in der Nav.
|
||
- **Warenkorb-Erinnerung** (`feature_abandoned_cart`): beim Checkout-Start wird der Warenkorb serverseitig in `abandoned_carts` gesichert (`/api/cart-capture`). Versand-Trigger: **`POST /api/cron/abandoned`** (Header `Authorization: Bearer <CRON_TOKEN>` oder `?token=`), schickt für fällige, nicht erinnerte Karten eine gebrandete Erinnerungsmail (Mailer/Log-Fallback) und setzt `reminded=1`. Erfolgreiche Bestellung der Adresse setzt `recovered=1`. Status/Zähler unter Einstellungen. Als **Coolify-Scheduled-Task** z. B. alle 30 Min `curl -fsS -X POST -H "Authorization: Bearer $CRON_TOKEN" https://shop.example.com/api/cron/abandoned` aufrufen.
|
||
- **Medienbibliothek + WebP (v2.4):** eigener Admin-Bereich **Medien** (`/admin/medien`) mit Grid (Thumbnail, Dateiname, Größe/Maße, Alt-Text, „URL kopieren", Löschen) und **Mehrfach-Upload** (Drag&Drop). Beim Upload werden **JPG/JPEG/PNG automatisch zu WebP konvertiert** (sharp, Qualität ~82, max-Breite ~2000px); das Original wird verworfen, nur die `.webp` bleibt. WebP/SVG/GIF/AVIF werden unverändert durchgereicht; bei Konvertierungsfehler bleibt das Original erhalten (kein Absturz). Ein **wiederverwendbarer Medien-Picker** (`public/media-picker.js`) ersetzt überall den alten URL-Prompt: Block-Editor (Hero/Bild/Galerie-Mehrfach), Produkt-Editor (Karten-Bild, Galerie, Varianten-Bild), Slides & Popups. `media` um `alt`/`width`/`height` erweitert; JSON-Quelle `/api/admin/media` (session-gesichert).
|
||
- **Varianten-Matrix (v2.4):** Produkte definieren Optionen (`options` = `[{name, values[]}]`, z. B. Größe × Farbe). Der Produkt-Editor erzeugt daraus die **Varianten-Matrix** (`product_variants`: `sku`, `options_json`, `price_cents`-Override, `stock`, `image`, `active`). Storefront-PDP zeigt Options-Selektoren → wählt Variante → Preis/Bestand/Bild aktualisieren, „nicht lieferbar" bei inaktiv/ausverkauft. Warenkorb & Checkout tragen `sku` + Options; das Order-Item bekommt `sku`/`variant`, der Variantenpreis wird **serverseitig** verifiziert. Produkte ohne Optionen verhalten sich wie bisher (einfache „Größen"-Liste bleibt).
|
||
- **Intelligentere Analytics (v2.4):** Conversion je Produkt (Ansichten→Käufe), **Bestseller** (Menge/Umsatz), **Umsatz pro Quelle/UTM**, **Top-Suchbegriffe** (`search`-Events von `/suche`, inkl. Null-Treffer-Markierung), **Umsatz-Zeitreihe** (Chart.js, 30 Tage), **AOV**, **Wiederkaufrate** und **Lager-Warnungen** (knappe Produkte & Varianten) — alles aus SQLite aggregiert, kein externer Dienst.
|
||
- **Litestream-Backups (v2.4):** optionales Streaming-Backup der SQLite-DB nach S3/Backblaze B2. Ist `LITESTREAM_REPLICA_URL` gesetzt, stellt der Container beim Start die DB bei Bedarf wieder her (`litestream restore`) und repliziert dann live (`litestream replicate -exec`); ohne Replica startet die App normal ohne Backup. Status sichtbar unter **Admin → Einstellungen → Backup**. Siehe Abschnitt „Backups".
|
||
- **Editierbare, gebrandete 404** (v2.1): `src/pages/404.astro` rendert die System-Seite mit Slug `404` über den Block-Builder. Wird per `ensureSystemPages()` bei jedem Boot idempotent angelegt und ist im Admin unter **Inhalte** editierbar.
|
||
- **Engine**: synchron via `better-sqlite3` (WAL), automatisches Seeding beim ersten Start.
|
||
- **First-Party-Analytics**: eigene `events`-Tabelle, kein externer Dienst (Session = täglich rollender Hash).
|
||
- **Branding konfigurierbar**: Shop-Name, Akzentfarbe, Währung u. a. in einer `settings`-Tabelle.
|
||
- **Self-hosted Fonts** (Fraunces + Public Sans), kein Google-CDN. Chart.js via cdnjs.
|
||
|
||
## Authentifizierung & Rollen
|
||
|
||
- **Session-Login** per HTML-Formular (signiertes HMAC-Cookie, „Angemeldet bleiben" = 30 Tage). Passwörter werden mit `node:crypto.scryptSync` + zufälligem Salt gehasht.
|
||
- **Initial-Owner** wird beim ersten Boot aus `ADMIN_EMAIL` / `ADMIN_PASS` angelegt; weitere Nutzer im Admin unter **Nutzer & Zugänge** (Owner-only).
|
||
- **Rollen**: `owner` (alles), `redaktion` (Produkte/Inhalte/Marketing/Analytics), `versand` (nur Bestellungen). Navigation und Seiten werden serverseitig nach Rolle gegated.
|
||
- **Konfigurierbarer Admin-Pfad** über `ADMIN_PATH` (Default `admin`, z. B. `intern` → Admin unter `/intern`). Direkter Zugriff auf `/admin` wird bei abweichendem Pfad mit 404 blockiert.
|
||
- **Audit-Log** (Tabelle `audit`) protokolliert Create/Update/Delete; Ansicht unter **Aktivität (Audit)** (Owner). Login-Rate-Limit: nach 5 Fehlversuchen 60 s Sperre pro IP.
|
||
|
||
## Umgebungsvariablen (ENV)
|
||
|
||
| Variable | Beschreibung | Default |
|
||
|---|---|---|
|
||
| `DB_PATH` | Pfad zur SQLite-Datenbank (wird angelegt) | `./data/hdc.db` |
|
||
| `ADMIN_EMAIL` | Initial-Owner-E-Mail (erster Boot) | `admin@example.com` |
|
||
| `ADMIN_PASS` | Initial-Owner-Passwort (erster Boot) | `admin` |
|
||
| `ADMIN_PATH` | Pfad-Segment des Admin-Bereichs | `admin` |
|
||
| `SESSION_SECRET` | HMAC-Geheimnis für Session-Cookies | interner Fallback (in Prod setzen!) |
|
||
| `HDC_API_TOKEN` | Bearer-Token für `/api/admin/*`. Leer ⇒ API gesperrt | – |
|
||
| `STRIPE_PUBLIC_KEY` | Stripe Publishable Key (optional) | – |
|
||
| `STRIPE_SECRET_KEY` | Stripe Secret Key. Ohne echten Key läuft der Demo-Checkout. | – |
|
||
| `PAYMENT_PROVIDER` | Zahlungsanbieter erzwingen: `mollie` / `stripe` / `demo`. Leer ⇒ Auto-Wahl | – |
|
||
| `MOLLIE_API_KEY` | Mollie API-Key (`test_…`/`live_…`). Ohne gültigen Key Demo-Fallback | – |
|
||
| `MAIL_PROVIDER` | `listmonk` / `smtp` / leer (⇒ Log-Fallback in `email_log`) | – |
|
||
| `MAIL_FROM` | Absenderadresse ausgehender Mails | – |
|
||
| `LISTMONK_URL` / `LISTMONK_USER` / `LISTMONK_PASS` / `LISTMONK_TX_TEMPLATE_ID` | Listmonk Transactional-API | – |
|
||
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` / `SMTP_SECURE` | SMTP-Versand (nodemailer) | – |
|
||
| `CRON_TOKEN` | Bearer-Token für `/api/cron/abandoned` (Warenkorb-Erinnerung). Leer ⇒ Endpoint gesperrt | – |
|
||
| `ABANDONED_AFTER_MINUTES` | Alter (Minuten), ab dem eine offene Warenkorb-Karte erinnert wird | `30` |
|
||
|
||
Siehe `.env.example`.
|
||
|
||
## Lokal starten
|
||
|
||
```bash
|
||
npm install
|
||
npm run dev # http://localhost:4321
|
||
# oder produktiv:
|
||
npm run build
|
||
node ./dist/server/entry.mjs
|
||
```
|
||
|
||
Storefront: `/` · Admin: `/admin` (bzw. `/${ADMIN_PATH}`). Erst-Login mit `ADMIN_EMAIL` / `ADMIN_PASS`.
|
||
|
||
## Block-Builder
|
||
|
||
Jede Seite (`pages`) hat ein Feld `blocks` (JSON-Array). Der Vollbild-Editor liegt unter `/${ADMIN_PATH}/inhalte/editor/<id>` (Button „Editor" in der Seitenliste). Gespeicherte Blöcke werden vom **Storefront-Block-Renderer** (`src/components/BlockRenderer.astro`) auf `/seite/<slug>` ausgegeben.
|
||
|
||
## KI-Editierbarkeit (API)
|
||
|
||
Token-gesicherte JSON-API unter `/api/admin/*` (Header `Authorization: Bearer <HDC_API_TOKEN>`):
|
||
|
||
- `GET /api/admin` — maschinenlesbares Manifest (Ressourcen, Felder, Block-Typen, Endpunkte).
|
||
- `GET /ai-admin.txt` — dieselbe Beschreibung als Klartext für LLMs.
|
||
- `GET /api/admin/{resource}` · `GET /api/admin/{resource}/{id}` — lesen.
|
||
- `POST /api/admin/{resource}` — Upsert (mit `id` oder `slug` ⇒ Update, sonst Create).
|
||
- `DELETE /api/admin/{resource}/{id}` — löschen.
|
||
- `POST /api/admin/pages/{id}/blocks` — Block-Array einer Seite setzen.
|
||
|
||
Schreibbar: `products`, `pages`, `slides`, `popups`, `settings`. Nur lesbar: `orders`, `customers`. Preise in Cent.
|
||
|
||
```bash
|
||
curl -H "Authorization: Bearer $HDC_API_TOKEN" https://shop.example.com/api/admin/products
|
||
curl -H "Authorization: Bearer $HDC_API_TOKEN" -X POST https://shop.example.com/api/admin/products \
|
||
-H 'Content-Type: application/json' -d '{"name":"Neues Produkt","priceCents":1990,"category":"Test"}'
|
||
```
|
||
|
||
## MCP-Server
|
||
|
||
Unter `mcp/` liegt ein eigenständiger **Model-Context-Protocol-Server** (stdio), mit dem ein LLM/Agent den Shop über die Admin-API bearbeitet. Tools u. a.: `list_products`, `upsert_product`, `get_page`, `update_page_blocks`, `list_orders`, `update_settings`, `create_page`. Installation, ENV (`HDC_BASE_URL`, `HDC_API_TOKEN`) und Registrierung in Claude/Cowork: siehe [`mcp/README.md`](mcp/README.md).
|
||
|
||
## Docker / Coolify
|
||
|
||
```bash
|
||
docker build -t hd-commerce .
|
||
docker run -p 4321:4321 -v hdc-data:/data \
|
||
-e ADMIN_EMAIL=admin@example.com -e ADMIN_PASS=geheim \
|
||
-e SESSION_SECRET=langes-geheimnis -e HDC_API_TOKEN=token hd-commerce
|
||
```
|
||
|
||
Das `Dockerfile` (node:22-slim) baut `better-sqlite3` nativ, legt `/data` an und setzt `DB_PATH=/data/hdc.db`. Auf Coolify ein persistentes Volume auf `/data` mounten. HEALTHCHECK prüft `/`.
|
||
|
||
Das Image enthält zusätzlich das **Litestream**-Binary und startet über `docker-entrypoint.sh`: ohne Backup-ENV ein reiner `node ./dist/server/entry.mjs`, mit `LITESTREAM_REPLICA_URL` ein `litestream replicate -exec` (nach optionalem Restore).
|
||
|
||
## Backups (Litestream → Backblaze B2 / S3)
|
||
|
||
Optionales kontinuierliches Streaming-Backup der SQLite-DB. Ohne Konfiguration läuft die App unverändert ohne Backup.
|
||
|
||
1. **B2-Bucket anlegen** (Backblaze) und einen Application-Key mit Schreibrechten erzeugen. B2 ist S3-kompatibel.
|
||
2. **ENV setzen** (Coolify → App → Environment):
|
||
```
|
||
LITESTREAM_REPLICA_URL=s3://<bucket>/<pfad>
|
||
LITESTREAM_ACCESS_KEY_ID=<keyID>
|
||
LITESTREAM_SECRET_ACCESS_KEY=<applicationKey>
|
||
LITESTREAM_ENDPOINT=s3.eu-central-003.backblazeb2.com # B2-S3-Endpoint; für AWS S3 leer lassen
|
||
```
|
||
(Für AWS S3 genügen `LITESTREAM_REPLICA_URL` + die beiden Keys; `LITESTREAM_ENDPOINT` bleibt leer.)
|
||
3. **Restore** (z. B. neue Instanz / Disaster-Recovery) — die DB wird beim Start automatisch wiederhergestellt, falls lokal keine existiert. Manuell:
|
||
```
|
||
litestream restore -config /app/litestream.yml -if-replica-exists /data/hdc.db
|
||
```
|
||
|
||
Der Status (konfiguriert? Ziel?) ist unter **Admin → Einstellungen → Backup (Litestream)** sichtbar. Hinweis: Die Live-Demo nutzt `DB_PATH=/data/hdc2.db`.
|
||
|
||
## Datenmodell
|
||
|
||
`settings` (inkl. Feature-Flags & `payment_provider`), `products` (inkl. `mwst` / `base_amount` / `base_unit` / `base_price_per`), `orders` (inkl. `discount_code`/`discount_cents`, `tax_cents`/`shipping_cents`/`country`, `payment_provider`/`payment_id`), `customers`, `slides`, `pages` (inkl. `blocks`; System-Seite `404`), `popups` (inkl. `style` / `discount_id`), `discounts`, `discount_redemptions`, `shipping_zones`, `email_log`, `subscribers`, `events`, `media` (inkl. `alt`/`width`/`height`), `product_variants` (Größe×Farbe-Matrix), `users`, `audit` — alles seed-bar und im Admin pflegbar.
|
||
|
||
---
|
||
|
||
> **Hinweis:** „Brittas Nähkiste" ist nur die mitgelieferte Demo-Instanz. Brand (Name, Farben, Texte) ist über **Admin → Einstellungen** anpassbar.
|
||
|
||
Lizenz: MIT (siehe `LICENSE`).
|