diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 189234b..cc3017b 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -77,12 +77,12 @@ jobs: type=sha,format=short type=semver,pattern={{version}} - # Laufzeit-Image (schlank, "output: standalone") — das ist das Image, - # das in Kubernetes tatsaechlich den Shop ausliefert (siehe - # k8s/app-deployment.yaml). Ein einzelnes Image traegt beide Prisma- - # Client-Varianten (Postgres + MySQL) und waehlt zur Laufzeit per - # DATABASE_PROVIDER, siehe src/lib/prisma.ts. - - name: Laufzeit-Image bauen und pushen + # Ein einzelnes Image traegt beide Prisma-Client-Varianten (Postgres + + # MySQL, waehlt zur Laufzeit per DATABASE_PROVIDER, siehe + # src/lib/prisma.ts) UND die Migrations-/Seed-Tools (siehe Dockerfile- + # Kommentar bei "AS migrator") — die App migriert/seedet sich beim + # Start selbst (idempotent), kein separates Migrations-Image/Job mehr. + - name: Image bauen und pushen uses: docker/build-push-action@v6 with: context: . @@ -91,18 +91,3 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - - # Migrations-Image: eigene schlanke Stage (nur Prisma-CLI/tsx + ein - # paar Laufzeit-Pakete, siehe Dockerfile-Kommentar bei "AS migrator") - # statt der vollen builder-Stage, die zusaetzlich next/react/sharp/alle - # devDependencies enthielt. Wird vom Kubernetes-Migrations-Job - # verwendet, siehe k8s/04-migrate-job.yaml. - - name: Migrations-Image bauen und pushen - uses: docker/build-push-action@v6 - with: - context: . - target: migrator - push: true - tags: ${{ env.REGISTRY_PUSH }}/${{ steps.image.outputs.name }}:migrate-${{ gitea.sha }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 915af80..204bf36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,18 +27,13 @@ ENV DATABASE_URL="postgresql://placeholder:placeholder@localhost:5432/placeholde # Datenbanken wechseln kann, siehe lib/prisma.ts. RUN pnpm build -# --- Migrator --------------------------------------------------------------- -# Eigenstaendige, schlanke Stage nur fuer den Kubernetes-Migrations-Job -# (k8s/04-migrate-job.yaml): "prisma migrate deploy" + "prisma db seed" -# brauchen nur die Prisma-CLI, tsx und die paar Laufzeit-Pakete, die -# prisma/seed.ts importiert — NICHT next/react/sharp/stripe/alle -# devDependencies, die im vollen builder-Stage (oben) noch mit drin sind. -# Frueher wurde builder direkt als Migrations-Image gebaut; dessen -# node_modules-Layer wurde beim Push an die (hinter Cloudflare liegende, -# ~100 MB Limit pro Layer) Gitea-Registry mit "413 Payload Too Large" -# abgewiesen. Eigene, minimale Paketliste statt "pnpm install" gegen das -# volle package.json, damit next/react/sharp erst gar nicht installiert -# werden. +# --- Migrator-Tools ----------------------------------------------------- +# Eigenstaendiger, schlanker node_modules-Baum nur mit Prisma-CLI, tsx und +# den paar Laufzeit-Paketen, die prisma/seed.ts importiert — NICHT +# next/react/sharp/stripe/alle devDependencies, die der volle builder-Stage +# oben mitbringt. Wird unten in die runner-Stage kopiert, damit die App sich +# beim Start selbst migrieren/seeden kann (kein separates Migrations-Image/ +# kein Kubernetes-Job mehr noetig). FROM base AS migrator # pnpm-workspace.yaml zuerst kopieren: pnpm v11 blockt Postinstall-Skripte # (u.a. @prisma/engines, laedt die Schema-Engine-Binary fuer "migrate @@ -52,19 +47,6 @@ RUN pnpm add --save-prod \ prisma@^7.9.1 tsx@^4 bcryptjs@^3.0.3 \ @prisma/client@^7.9.1 @prisma/adapter-pg@^7.9.1 \ @prisma/adapter-mariadb@^7.9.1 mariadb@^3.5.3 -COPY prisma.config.ts ./ -COPY prisma ./prisma -COPY scripts ./scripts -COPY src/lib/db-adapter.ts ./src/lib/db-adapter.ts -COPY src/lib/password-breach-check.ts ./src/lib/password-breach-check.ts -COPY --from=builder /app/src/generated ./src/generated -# Nur fuer scripts/prisma-provider.mjs (schreibt die provider-Zeile passend -# zu DATABASE_PROVIDER) und damit prisma.config.ts nicht wegen einer -# fehlenden URL abbricht, wenn versehentlich ohne echtes DATABASE_URL -# aufgerufen — die K8s-Job-Umgebung (04-migrate-job.yaml) setzt die echte -# DATABASE_URL zur Laufzeit ueber das Secret. -ENV DATABASE_URL="postgresql://placeholder:placeholder@localhost:5432/placeholder" -CMD ["sh", "-c", "node scripts/prisma-provider.mjs && npx prisma migrate deploy && npx prisma db seed"] # --- Runtime -------------------------------------------------------------- FROM base AS runner @@ -76,10 +58,32 @@ COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder /app/prisma ./prisma +COPY prisma.config.ts ./ +COPY scripts ./scripts +COPY src/lib/db-adapter.ts ./src/lib/db-adapter.ts +COPY src/lib/password-breach-check.ts ./src/lib/password-breach-check.ts +COPY --from=builder /app/src/generated ./src/generated +# Eigener node_modules-Baum nur fuer Migrationen/Seed (prisma-CLI/tsx), +# getrennt vom node_modules des Next.js-Standalone-Outputs oben (das ist +# eine von Next.js kopierte, nicht pnpm-verwaltete Teilmenge — "pnpm add" +# wuerde dort kollidieren). PATH macht "prisma"/"tsx" ausfuehrbar, +# NODE_PATH ist ein Fallback fuer Node-Modulaufloesung, falls tsx beim +# Ausfuehren von prisma/seed.ts ein Paket braucht, das der Next.js-Tracer +# nicht schon ins Standalone-node_modules uebernommen hat. +COPY --chown=nextjs:nodejs --from=migrator /app/node_modules ./migrate-tools/node_modules +ENV PATH="/app/migrate-tools/node_modules/.bin:${PATH}" +ENV NODE_PATH="/app/migrate-tools/node_modules" USER nextjs EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" -CMD ["node", "server.js"] +# Migrationen + Seed laufen VOR dem Serverstart, damit Readiness-/Liveness- +# Probes nie gegen eine ungemigrateet Datenbank treffen — dafuer kein +# separater Kubernetes-Job mehr noetig. Beide Schritte sind idempotent +# ("migrate deploy" wendet nur neue Migrationen an, prisma/seed.ts +# legt/erneuert nur das Admin-Konto) und damit bei jedem Neustart +# gefahrlos. KEINE Beispieldaten im Image/Auto-Seed — dafuer separat +# "pnpm db:seed:demo" (prisma/seed-demo.ts), nie automatisch ausgefuehrt. +CMD ["sh", "-c", "node scripts/prisma-provider.mjs && prisma migrate deploy && prisma db seed && exec node server.js"] diff --git a/README.md b/README.md index f007b00..8e52bbf 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,8 @@ pnpm install docker compose up -d db pnpm db:migrate # erstellt Tabellen (Prisma Migrate) -pnpm db:seed # legt 2 Beispiel-Ernten/Chargen/Produkte + das Admin-Konto (ADMIN_EMAIL/ADMIN_PASSWORD) an +pnpm db:seed # legt/erneuert nur das Admin-Konto (ADMIN_EMAIL/ADMIN_PASSWORD) an +pnpm db:seed:demo # optional: 2 Beispiel-Ernten/Chargen/Produkte zum Ausprobieren (nicht im Docker-Image, siehe unten) pnpm dev # http://localhost:3000 ``` @@ -128,7 +129,8 @@ Nach einer Änderung von `DATABASE_PROVIDER` (z.B. lokal): docker compose --profile mysql up -d db-mysql pnpm db:push # legt Tabellen beim gewaehlten Provider an (schreibt kurzzeitig die passende provider-Zeile in schema.prisma, siehe unten) -pnpm db:seed # Beispieldaten + Admin-Konto +pnpm db:seed # Admin-Konto +pnpm db:seed:demo # optional: Beispieldaten pnpm dev ``` @@ -194,14 +196,15 @@ cp .env.example .env docker compose up -d --build docker compose exec app pnpm db:deploy -docker compose exec app pnpm db:seed # Beispieldaten + legt/befördert das Admin-Konto (ADMIN_EMAIL/ADMIN_PASSWORD) +docker compose exec app pnpm db:seed # legt/befördert das Admin-Konto (ADMIN_EMAIL/ADMIN_PASSWORD) +docker compose exec app pnpm db:seed:demo # optional: Beispieldaten zum Ausprobieren ``` Die App läuft danach hinter [Caddy](https://caddyserver.com/) unter `http://localhost`, PostgreSQL unter Port `5432`. `app` selbst ist **nicht** direkt vom Host aus erreichbar (kein `ports:`-Mapping mehr, nur `expose`) — Caddy ist bewusst der einzige nach außen offene Dienst, weil er die tatsächliche Client-Verbindung terminiert und `X-Forwarded-For` dabei neu setzt statt einen vom Client selbst mitgeschickten Wert zu übernehmen. Ohne diesen Proxy davor ließe sich jedes IP-basierte Rate-Limit (Login, 2FA-Code, Checkout, Registrierung, siehe `lib/rate-limit.ts`) durch einen gefälschten Header umgehen. Für eine echte Domain mit automatischem HTTPS `SITE_ADDRESS` in `.env` setzen und Port `443` in `docker-compose.yml` freigeben (siehe Kommentar dort und in der `Caddyfile`). ## Kubernetes / Gitea CI -`.gitea/workflows/docker-build.yml` baut bei jedem Push auf `main` automatisch zwei Docker-Image-Tags (schlankes Laufzeit-Image + ein `:migrate-*`-Tag mit Prisma-CLI für Migrationen) und veröffentlicht sie in der Gitea-eigenen Container-Registry. `k8s/` enthält vollständige, rohe Kubernetes-Manifeste (Namespace, ConfigMap, Secret-Vorlage, Postgres/MySQL-StatefulSet, Migrations-Job, App-Deployment, Ingress) für den Betrieb dieser Images im Cluster — Einrichtung und Deployment-Ablauf stehen in [`k8s/README.md`](k8s/README.md). +`.gitea/workflows/docker-build.yml` baut bei jedem Push auf `main` automatisch ein Docker-Image und veröffentlicht es in der Gitea-eigenen Container-Registry. Der Container migriert/seedet die Datenbank beim Start selbst (idempotent), kein separates Migrations-Image oder -Job nötig. `k8s/` enthält vollständige, rohe Kubernetes-Manifeste (Namespace, ConfigMap, Secret-Vorlage, Postgres/MySQL-StatefulSet, App-Deployment, Ingress) für den Betrieb dieses Images im Cluster — Einrichtung und Deployment-Ablauf stehen in [`k8s/README.md`](k8s/README.md). Ein einzelnes Image unterstützt beide Datenbank-Provider gleichzeitig (siehe „Datenbank-Provider wechseln" oben) — welcher tatsächlich läuft, entscheidet allein `DATABASE_PROVIDER`/`DATABASE_URL` zur Laufzeit, kein separater Image-Build nötig. diff --git a/k8s/02-secret.example.yaml b/k8s/02-secret.example.yaml index cf24a73..d0cedb5 100644 --- a/k8s/02-secret.example.yaml +++ b/k8s/02-secret.example.yaml @@ -24,6 +24,10 @@ stringData: # Host "wabenhain-postgres" bzw. "wabenhain-mysql" passend zum Service aus # 03-postgres.yaml/03-mysql.yaml waehlen; Port 5432 (Postgres) bzw. 3306 # (MySQL). Muss zu DATABASE_PROVIDER in 01-configmap.yaml passen. + # WICHTIG: das Passwort hier MUSS URL-encodiert sein (Sonderzeichen wie + # @ : / % # ? & sonst ungueltig bzw. falsch geparst) — DB_PASSWORD unten + # bleibt dagegen roh/unencodiert. Encodieren z.B. mit: + # node -e "console.log(encodeURIComponent('DEIN-PASSWORT'))" DATABASE_URL: "postgresql://wabenhain:CHANGE-ME@wabenhain-postgres:5432/wabenhain?schema=public" # Dasselbe Passwort wie oben in DATABASE_URL — wird vom Postgres-/MySQL- # StatefulSet (je nachdem, welches du anwendest) zum Anlegen des diff --git a/k8s/04-migrate-job.yaml b/k8s/04-migrate-job.yaml deleted file mode 100644 index d8c8feb..0000000 --- a/k8s/04-migrate-job.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# Einmalig VOR jedem Deployment (bzw. bei jedem neuen Image-Tag) ausfuehren: -# kubectl delete job wabenhain-migrate -n wabenhain --ignore-not-found -# kubectl apply -f k8s/04-migrate-job.yaml -# kubectl wait --for=condition=complete job/wabenhain-migrate -n wabenhain --timeout=120s -# "kubectl delete...--ignore-not-found" zuerst, weil Jobs nicht ueberschrieben -# werden koennen (Kubernetes-Job-Objekte sind unveraenderlich) — ohne das -# schlaegt ein erneutes "kubectl apply" mit demselben Namen fehl. -# -# Nutzt das ":migrate-"-Image aus der Gitea-Actions-Pipeline (hat die -# Prisma-CLI + tsx an Bord, die "pnpm db:deploy"/"pnpm db:seed" brauchen — -# das schlanke Laufzeit-Image aus 05-app-deployment.yaml bewusst nicht, -# siehe Dockerfile). image: unten mit der tatsaechlichen Registry/Tag -# ersetzen. -apiVersion: batch/v1 -kind: Job -metadata: - name: wabenhain-migrate - namespace: wabenhain -spec: - backoffLimit: 2 - template: - spec: - restartPolicy: Never - containers: - - name: migrate - image: git.encry.de/nick.rosenloecher/wabenhain:migrate-REPLACE-ME - # prisma migrate deploy (nicht "db push"): wendet die committeten - # Migrationsdateien nicht-interaktiv an, das ist Prismas - # vorgesehener Weg fuer Produktions-Deployments. db:seed danach - # legt/erneuert nur das Admin-Konto + Beispieldaten, ist idempotent - # (siehe prisma/seed.ts). - command: ["sh", "-c", "pnpm db:deploy && pnpm db:seed"] - envFrom: - - configMapRef: - name: wabenhain-config - - secretRef: - name: wabenhain-secrets diff --git a/k8s/05-app-deployment.yaml b/k8s/05-app-deployment.yaml index 5f0b8b2..d828fbf 100644 --- a/k8s/05-app-deployment.yaml +++ b/k8s/05-app-deployment.yaml @@ -1,8 +1,10 @@ # image: unten mit der tatsaechlichen Registry/Tag aus der Gitea-Actions- -# Pipeline ersetzen (das schlanke Laufzeit-Image, NICHT ":migrate-..."). -# Vor dem ersten Rollout einmal den Migrations-Job (04-migrate-job.yaml) -# erfolgreich durchlaufen lassen, sonst startet die App gegen eine leere, -# ungemigrateet Datenbank. +# Pipeline ersetzen. Kein separater Migrations-Job mehr noetig: der +# Container fuehrt "prisma migrate deploy" + "prisma db seed" selbst aus, +# BEVOR er den Server startet (siehe Dockerfile-CMD in der runner-Stage) — +# beide Schritte sind idempotent, laufen also bei jedem Neustart gefahrlos +# erneut. Readiness-/Liveness-Probes greifen automatisch erst, sobald Port +# 3000 offen ist, also nach abgeschlossener Migration/Seed. apiVersion: apps/v1 kind: Deployment metadata: @@ -45,8 +47,14 @@ spec: httpGet: path: / port: 3000 - initialDelaySeconds: 20 + # initialDelaySeconds/failureThreshold grosszuegiger als sonst + # ueblich: der Container migriert/seedet die DB selbst vor dem + # Serverstart (siehe Kommentar oben) — genug Spielraum, damit ein + # laengerer erster Migrationslauf nicht faelschlich als haengender + # Prozess gewertet und der Container neu gestartet wird. + initialDelaySeconds: 30 periodSeconds: 30 + failureThreshold: 5 resources: requests: cpu: 100m diff --git a/k8s/README.md b/k8s/README.md index 2d9d5d9..ed07052 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -6,11 +6,12 @@ anwendbar. Nummerierung = empfohlene Reihenfolge. ## Einmalige Vorbereitung 1. **Gitea Actions**: `.gitea/workflows/docker-build.yml` baut bei jedem - Push auf `main` zwei Image-Tags in der Gitea-Container-Registry: - - `//wabenhain:latest` (+ `:sha-`, `:vX.Y.Z` bei - Tags) — das schlanke Laufzeit-Image fuer `05-app-deployment.yaml`. - - `//wabenhain:migrate-` — enthaelt zusaetzlich die - Prisma-CLI/tsx, fuer `04-migrate-job.yaml`. + Push auf `main` das Image `//wabenhain:latest` (+ + `:sha-`, `:vX.Y.Z` bei Tags) in der Gitea-Container-Registry. + Ein einzelnes Image reicht fuer `05-app-deployment.yaml` — es traegt + sowohl die Next.js-App als auch die Prisma-CLI/tsx und migriert/seedet + die Datenbank beim Start selbst (siehe Punkt 2 unten), kein separates + Migrations-Image mehr. Voraussetzung: ein Repo-/Org-Secret `REGISTRY_TOKEN` in Gitea anlegen (Personal Access Token mit `package`-Scope, unter Benutzereinstellungen → @@ -28,6 +29,10 @@ anwendbar. Nummerierung = empfohlene Reihenfolge. `src/lib/prisma.ts`. Ein Image reicht also fuer Postgres UND MySQL — nur `DATABASE_PROVIDER`/`DATABASE_URL` in `01-configmap.yaml`/ `02-secret.yaml` muessen zur tatsaechlich gewaehlten Datenbank passen. + Beim Container-Start fuehrt derselbe Prozess erst `prisma migrate deploy` + und `prisma db seed` aus, BEVOR der Next.js-Server startet (siehe + Dockerfile-CMD in der `runner`-Stage) — beide Schritte sind idempotent + und bei jedem Neustart gefahrlos, kein separater Migrations-Job noetig. 3. `k8s/02-secret.example.yaml` nach `k8s/02-secret.yaml` kopieren (per `.gitignore` ausgeschlossen — NIE mit echten Werten committen) und @@ -37,13 +42,10 @@ anwendbar. Nummerierung = empfohlene Reihenfolge. 4. `01-configmap.yaml`: `DATABASE_PROVIDER` (postgresql/mysql) und `NEXT_PUBLIC_SITE_URL` (echte Domain) anpassen. -5. `image:` in `04-migrate-job.yaml`/`05-app-deployment.yaml` zeigt bereits - auf `git.encry.de/nick.rosenloecher/wabenhain` — nur noch den Tag pruefen - (`:latest` in `05-app-deployment.yaml` passt fuer den jeweils neuesten - `main`-Build; `:migrate-REPLACE-ME` in `04-migrate-job.yaml` durch den - echten Commit-Kurzhash aus dem Gitea-Actions-Lauf ersetzen, sichtbar unter - Repo → Actions → Workflow-Run). In `06-ingress.yaml` `shop.example.com` - durch die echte Domain ersetzen +5. `image:` in `05-app-deployment.yaml` zeigt bereits auf + `git.encry.de/nick.rosenloecher/wabenhain` — nur noch den Tag pruefen + (`:latest` passt fuer den jeweils neuesten `main`-Build). In + `06-ingress.yaml` `shop.example.com` durch die echte Domain ersetzen (zweimal: `host:` und `tls.hosts`). ## Deployment @@ -59,25 +61,24 @@ kubectl apply -f k8s/03-postgres.yaml kubectl wait --for=condition=ready pod -l app=wabenhain-postgres -n wabenhain --timeout=120s -# Migrationen + Seed (Admin-Konto, Beispieldaten) — vor jedem neuen -# Image-Tag erneut ausfuehren: -kubectl delete job wabenhain-migrate -n wabenhain --ignore-not-found -kubectl apply -f k8s/04-migrate-job.yaml -kubectl wait --for=condition=complete job/wabenhain-migrate -n wabenhain --timeout=120s - kubectl apply -f k8s/05-app-deployment.yaml kubectl apply -f k8s/06-ingress.yaml ``` +Migrationen + Seed (nur Admin-Konto, KEINE Beispieldaten — siehe Punkt 2 +oben) laufen automatisch beim Container-Start — +`kubectl rollout status deployment/wabenhain-app -n wabenhain` zeigt, wann +der erste Pod bereit ist. + ## Bei einem neuen Image (Redeploy) ```bash -kubectl delete job wabenhain-migrate -n wabenhain --ignore-not-found -kubectl apply -f k8s/04-migrate-job.yaml -kubectl wait --for=condition=complete job/wabenhain-migrate -n wabenhain --timeout=120s kubectl rollout restart deployment/wabenhain-app -n wabenhain ``` +Migrationen + Seed laufen automatisch erneut beim Start jedes neuen Pods +(idempotent, siehe oben). + ## Bekannte Grenzen dieser Manifeste - **`replicas: 1`** in `05-app-deployment.yaml`, bewusst nicht mehr: das diff --git a/package.json b/package.json index 2fe5760..b3676a0 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "db:migrate": "node scripts/prisma-provider.mjs && prisma migrate dev", "db:deploy": "node scripts/prisma-provider.mjs && prisma migrate deploy", "db:seed": "prisma db seed", + "db:seed:demo": "tsx prisma/seed-demo.ts", "db:studio": "node scripts/prisma-provider.mjs && prisma studio", "test": "vitest run", "test:watch": "vitest", diff --git a/prisma/seed.ts b/prisma/seed.ts index 9b2eb85..862bbb9 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -17,182 +17,14 @@ const prisma: PrismaClient = ? (new MySqlPrismaClient({ adapter }) as unknown as PrismaClient) : new PrismaClient({ adapter }); +/** + * Laeuft automatisch bei jedem Container-Start (siehe Dockerfile-CMD in der + * runner-Stage) — legt/erneuert NUR das Admin-Konto, keine Beispieldaten. + * Ein produktiv gebautes Image soll nie automatisch Fake-Produkte in einer + * echten Datenbank anlegen. Beispieldaten fuer lokales Testen/Screenshots: + * "pnpm db:seed:demo" (prisma/seed-demo.ts), manuell und getrennt. + */ async function main() { - const fruehtracht = await prisma.harvest.upsert({ - where: { slug: "fruehtracht-2026" }, - update: {}, - create: { - year: 2026, - type: "Frühtracht", - slug: "fruehtracht-2026", - harvestDate: new Date("2026-05-10"), - location: "Altenmünster", - description: - "Die erste Ernte des Jahres entsteht aus dem Nektar von Obstblüte, Raps und Löwenzahn rund um Altenmünster. Mild, floral und feincremig — der Auftakt jeder Wabenhain-Saison.", - images: [], - }, - }); - - const sommertracht = await prisma.harvest.upsert({ - where: { slug: "sommertracht-2026" }, - update: {}, - create: { - year: 2026, - type: "Sommertracht", - slug: "sommertracht-2026", - harvestDate: new Date("2026-07-18"), - location: "Altenmünster", - description: - "Im Hochsommer sammeln unsere Völker Nektar von Linde, Wiesenblumen und Wald. Das Ergebnis ist kräftiger, aromatischer und charaktervoller als die Frühtracht.", - images: [], - }, - }); - - const fhBatch = await prisma.batch.upsert({ - where: { code: "FH-26-01" }, - update: {}, - create: { - code: "FH-26-01", - harvestId: fruehtracht.id, - fillDate: new Date("2026-06-02"), - bestBeforeDate: new Date("2028-06-02"), - glassCount: 200, - remainingGlasses: 167, - }, - }); - - const stBatch = await prisma.batch.upsert({ - where: { code: "ST-26-01" }, - update: {}, - create: { - code: "ST-26-01", - harvestId: sommertracht.id, - fillDate: new Date("2026-08-05"), - bestBeforeDate: new Date("2028-08-05"), - glassCount: 150, - remainingGlasses: 150, - }, - }); - - const fhProduct = await prisma.product.upsert({ - where: { slug: "fruehtracht-2026" }, - update: { tasteTags: ["mild", "floral", "feincremig"] }, - create: { - slug: "fruehtracht-2026", - name: "Frühtracht 2026", - harvestId: fruehtracht.id, - batchId: fhBatch.id, - description: - "Mild, floral und feincremig. Unsere erste Ernte des Jahres aus dem Raum Altenmünster.", - tasteTags: ["mild", "floral", "feincremig"], - intensityMild: 4, - intensityFloral: 4, - intensityKraeftig: 2, - consistency: "feincremig", - weightGrams: 500, - priceCents: 990, - pricePerKgCents: 1980, - images: [], - isActive: true, - }, - }); - - const stProduct = await prisma.product.upsert({ - where: { slug: "sommertracht-2026" }, - update: { tasteTags: ["kräftig", "aromatisch", "charaktervoll"] }, - create: { - slug: "sommertracht-2026", - name: "Sommertracht 2026", - harvestId: sommertracht.id, - batchId: stBatch.id, - description: "Kräftiger, aromatischer und charaktervoller Sommerhonig aus Altenmünster.", - tasteTags: ["kräftig", "aromatisch", "charaktervoll"], - intensityMild: 2, - intensityFloral: 2, - intensityKraeftig: 5, - consistency: "cremig", - weightGrams: 500, - priceCents: 1090, - pricePerKgCents: 2180, - images: [], - isActive: true, - }, - }); - - const candleProduct = await prisma.product.upsert({ - where: { slug: "bienenwachskerze-rund" }, - update: { tasteTags: ["handgegossen", "reines Bienenwachs", "rußarm"] }, - create: { - slug: "bienenwachskerze-rund", - name: "Bienenwachskerze, rund", - category: "KERZEN", - description: - "Handgegossen aus reinem Bienenwachs unserer eigenen Völker. Brennt gleichmäßig, rußarm und verströmt einen dezenten Honigduft.", - tasteTags: ["handgegossen", "reines Bienenwachs", "rußarm"], - consistency: "fest", - weightGrams: 150, - priceCents: 1290, - pricePerKgCents: 8600, - images: [], - isActive: true, - }, - }); - - const propolisProduct = await prisma.product.upsert({ - where: { slug: "propolis-tinktur" }, - update: { tasteTags: ["schonend verarbeitet", "ohne Zusätze"] }, - create: { - slug: "propolis-tinktur", - name: "Propolis-Tinktur", - category: "PROPOLIS", - description: - "Schonend angesetzte Propolis-Tinktur aus dem Harz unserer Bienenstöcke rund um Altenmünster. Ohne synthetische Zusätze.", - tasteTags: ["schonend verarbeitet", "ohne Zusätze"], - consistency: "flüssig", - weightGrams: 30, - priceCents: 1490, - pricePerKgCents: 49666, - images: [], - isActive: true, - }, - }); - - await prisma.inventory.upsert({ - where: { productId: fhProduct.id }, - update: { quantity: 167 }, - create: { productId: fhProduct.id, quantity: 167 }, - }); - - await prisma.inventory.upsert({ - where: { productId: stProduct.id }, - update: { quantity: 150 }, - create: { productId: stProduct.id, quantity: 150 }, - }); - - await prisma.inventory.upsert({ - where: { productId: candleProduct.id }, - update: { quantity: 60 }, - create: { productId: candleProduct.id, quantity: 60 }, - }); - - await prisma.inventory.upsert({ - where: { productId: propolisProduct.id }, - update: { quantity: 40 }, - create: { productId: propolisProduct.id, quantity: 40 }, - }); - - const existingShippingTiers = await prisma.shippingTier.count(); - if (existingShippingTiers === 0) { - await prisma.shippingTier.createMany({ - data: [ - { minQuantity: 0, maxQuantity: 3, priceCents: 700 }, - { minQuantity: 4, maxQuantity: 9, priceCents: 850 }, - { minQuantity: 10, maxQuantity: 19, priceCents: 1100 }, - ], - }); - console.log("Versandstaffeln angelegt: 0-3=7,00€, 4-9=8,50€, 10-19=11,00€."); - } - const adminEmail = process.env.ADMIN_EMAIL?.trim().toLowerCase(); const adminPassword = process.env.ADMIN_PASSWORD; @@ -251,8 +83,6 @@ async function main() { "Siehe README, wie ein bestehendes Konto nachtraeglich zu ADMIN befoerdert wird." ); } - - console.log("Seed abgeschlossen: 2 Ernten, 2 Chargen, 4 Produkte (Honig, Kerzen, Propolis)."); } main()