Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m12s
Die Rechnung entstand bisher nur als Datenbankzeile; das PDF wurde bei jedem Abruf neu gerendert und tauchte in keiner E-Mail auf. Das hatte drei Nachteile: Gastbestellungen haben kein Konto, in dem sie den Beleg finden koennten; eine Sicherung der Datenbank sicherte den Beleg nicht mit; und ein spaeter erneut gerendertes PDF konnte nach einer Aenderung an Firmendaten oder Layout anders aussehen als das, was die Kundschaft in Haenden hielt. Ablage jetzt unter INVOICE_DIR, nach Jahr und Kunde sortiert: invoices/2026/mustermann-erika/RE-2026-A3F9C1B2.pdf Das Jahr zuerst, weil sich die Aufbewahrungsfrist danach richtet — ein abgelaufener Jahrgang laesst sich als ganzes Verzeichnis archivieren. Der relative Pfad steht als Invoice.pdfPath in der Datenbank, damit eine spaetere Aenderung an der Pfadbildung keine bereits abgelegten Belege unauffindbar macht. Dafuer wurde die nie gelesene Spalte "pdfUrl" umbenannt; eine URL war ohnehin das falsche Konzept. INVOICE_DIR ist bewusst ein ANDERES Verzeichnis als UPLOAD_DIR: app/uploads/[...path] liefert jede Datei unterhalb von UPLOAD_DIR ohne Anmeldung aus. Laegen Rechnungen dort, koennte jede Person, die den Pfad erraet, fremde Rechnungen samt Anschrift und Bestellinhalt herunterladen. Auf das Rechnungsverzeichnis zeigt keine oeffentliche Route; der Abruf laeuft weiter ueber /rechnung/<nummer>/pdf mit Besitzpruefung. Der Abruf liefert jetzt die abgelegte Datei aus und rendert nur neu, wenn keine da ist — also bei Rechnungen aus der Zeit davor. Versand: das PDF haengt an der Bestellbestaetigung. sendMail nimmt dafuer optionale Anhaenge entgegen, alle zehn bestehenden Aufrufe bleiben unveraendert. Schlaegt das Erzeugen oder Ablegen fehl, geht die Bestaetigung trotzdem raus und die Rechnung bleibt abrufbar: ein 500er an Stripe waere die falsche Antwort auf eine volle Platte, denn die Zahlung ist erfolgt und ein Wiederholungslauf aenderte daran nichts. Sicherung: der taegliche CronJob packt das Verzeichnis als rechnungen-<Zeitstempel>.tar.gz neben den Datenbank-Dump und haengt das Volume nur lesend ein. Vermerkt ist dort auch die Einschraenkung, dass wabenhain-invoices als ReadWriteOnce nur von Pods auf demselben Knoten einhaengbar ist — bei mehreren Knoten braucht es ReadWriteMany, sonst bleibt der Sicherungs-Pod lautlos in "Pending". Weiterhin offen und im README vermerkt: die Rechnungsnummer ist eindeutig, aber nicht fortlaufend (§ 14 Abs. 4 Nr. 4 UStG), und das PDF weist keine Umsatzsteuer aus. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
97 lines
4.6 KiB
YAML
97 lines
4.6 KiB
YAML
# Nicht-geheime Konfiguration. Geheimnisse (Passwoerter, API-Schluessel)
|
|
# gehoeren in 02-secret.yaml, NIE hierher: eine ConfigMap ist unverschluesselt
|
|
# und in der Cluster-Oberflaeche im Klartext lesbar.
|
|
#
|
|
# ANPASSEN vor dem ersten Ausrollen: NEXT_PUBLIC_SITE_URL und SMTP_FROM. Alle
|
|
# uebrigen Werte sind sinnvolle Vorgaben und koennen so bleiben; die leeren
|
|
# ("") gehoeren zu optionalen Funktionen, die dann einfach ruhen.
|
|
#
|
|
# Zusammen mit 02-secret.yaml deckt diese Datei JEDE Umgebungsvariable ab, die
|
|
# die Anwendung liest. Die Vollstaendigkeit wird automatisch geprueft, siehe
|
|
# src/lib/k8s-manifest-env.test.ts — kommt im Code eine neue Variable dazu,
|
|
# faellt das beim naechsten "pnpm test" auf, nicht erst im Betrieb.
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: wabenhain-config
|
|
namespace: wabenhain
|
|
data:
|
|
# ---------------------------------------------------------------------
|
|
# ANPASSEN
|
|
# ---------------------------------------------------------------------
|
|
|
|
# Oeffentliche Adresse des Shops. Einzige Quelle fuer Sitemap,
|
|
# Canonical-Tags, JSON-LD und JEDEN Link in JEDER E-Mail (src/lib/url.ts).
|
|
# Muss zum "host" in 06-ingress.yaml passen — mit https, ohne
|
|
# abschliessenden Schraegstrich.
|
|
NEXT_PUBLIC_SITE_URL: "https://wabenhain.com"
|
|
|
|
# Absenderadresse aller Transaktions-E-Mails. Sollte auf einer eigenen
|
|
# Domain liegen, sonst landen die Mails regelmaessig im Spam-Ordner.
|
|
SMTP_FROM: "Wabenhain <[email protected]>"
|
|
|
|
# ---------------------------------------------------------------------
|
|
# VORGABEN — koennen so bleiben
|
|
# ---------------------------------------------------------------------
|
|
|
|
NODE_ENV: "production"
|
|
|
|
# "postgresql" oder "mysql" — muss zu dem Datenbank-Manifest passen, das du
|
|
# tatsaechlich ausrollst (03-postgres.yaml bzw. 03-mysql.yaml), UND zum
|
|
# Schema in DATABASE_URL im Secret. Ein einzelnes Image traegt beide
|
|
# Prisma-Client-Varianten, siehe src/lib/prisma.ts.
|
|
DATABASE_PROVIDER: "postgresql"
|
|
|
|
# Port des SMTP-Anbieters. 587 ist STARTTLS (der Normalfall), 465 waere
|
|
# implizites TLS.
|
|
SMTP_PORT: "587"
|
|
|
|
# Verzeichnis fuer hochgeladene Bilder und Videos. MUSS exakt dem mountPath
|
|
# des "uploads"-Volumes in 05-app-deployment.yaml entsprechen — die App
|
|
# weiss nicht, was dort gemountet ist, sie schreibt nur in diesen Pfad.
|
|
# Fehlt der Wert, schreibt die App nach "./uploads" relativ zum
|
|
# Arbeitsverzeichnis: das trifft im Image zufaellig denselben Ort, ist aber
|
|
# nirgends zugesichert. Deshalb hier ausdruecklich setzen.
|
|
UPLOAD_DIR: "/app/uploads"
|
|
|
|
# Ablage der Rechnungs-PDFs, nach Jahr und Kunde sortiert:
|
|
# /app/invoices/2026/mustermann-erika/RE-2026-A3F9C1B2.pdf
|
|
# Muss zum mountPath des "invoices"-Volumes in 05-app-deployment.yaml
|
|
# passen. BEWUSST ein anderes Verzeichnis als UPLOAD_DIR: auf die Uploads
|
|
# zeigt eine oeffentliche Route, auf dieses Verzeichnis keine. Siehe
|
|
# src/lib/invoice-storage.ts und 04-uploads-pvc.yaml.
|
|
INVOICE_DIR: "/app/invoices"
|
|
|
|
# Threads fuer das Umrechnen hochgeladener Videos (Admin -> Inhalte ->
|
|
# Hero-Video). NICHT ohne Not erhoehen: ffmpeg teilt sich das Speicherlimit
|
|
# dieses Pods mit dem Node-Prozess, und jeder zusaetzliche Thread haelt
|
|
# eigene Bildpuffer — bei 4K-Material rund 12 MB je Bild. Zu viele Threads
|
|
# bringen den Pod beim Video-Upload um. Siehe src/lib/video-transcode.ts.
|
|
VIDEO_TRANSCODE_THREADS: "2"
|
|
|
|
# "sandbox", bis die DHL-Versandmarken getestet sind, danach "production".
|
|
# Wirkt nur, wenn die DHL-Zugangsdaten im Secret gesetzt sind.
|
|
DHL_ENVIRONMENT: "sandbox"
|
|
|
|
# ---------------------------------------------------------------------
|
|
# OPTIONAL — leer lassen, solange die Funktion nicht genutzt wird
|
|
# ---------------------------------------------------------------------
|
|
|
|
# Oeffentlicher Teil von Cloudflare Turnstile (CAPTCHA). Der geheime
|
|
# Gegenpart TURNSTILE_SECRET_KEY steht im Secret — beide setzen oder beide
|
|
# leer lassen. Leer = kein CAPTCHA-Widget, die Formulare bleiben nutzbar.
|
|
NEXT_PUBLIC_TURNSTILE_SITE_KEY: ""
|
|
|
|
# Google-Eintrag, aus dem die echten Rezensionen auf der Startseite kommen.
|
|
# Die Place-ID ist oeffentlich (sie steckt in jeder Google-Maps-Adresse des
|
|
# Eintrags) und darf deshalb hier stehen; der abrechenbare
|
|
# GOOGLE_PLACES_API_KEY liegt im Secret. Leer = statische Beispiel-Stimmen.
|
|
GOOGLE_PLACE_ID: ""
|
|
|
|
# Adresse der ApiCore-API fuer den Stammdaten-Sync (Ernten, Produkte,
|
|
# Chargen — siehe src/lib/apicore-client.ts), mit dem globalen Praefix
|
|
# "api", z.B. "https://apicore.example.org/api". Nur die Adresse; die
|
|
# Zugangsdaten des Sync-Kontos liegen im Secret. Leer = der Sync-Knopf im
|
|
# Admin meldet einen Konfigurationsfehler.
|
|
APICORE_API_URL: ""
|