Files
Wabenhain/k8s/04-uploads-pvc.yaml
Nick SzaunigandClaude Opus 5 70ede77dde
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m12s
feat: Rechnungen werden abgelegt, gesichert und mitversendet
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]>
2026-09-17 04:12:44 +02:00

71 lines
3.1 KiB
YAML

# Persistenter Speicher fuer hochgeladene Bilder und Videos (Produkt-,
# Ernten- und Inhaltsfotos aus dem Admin-Bereich, siehe
# app/api/admin/upload/route.ts und app/uploads/[...path]/route.ts).
#
# WARUM EIGENES MANIFEST: 05-app-deployment.yaml hatte hier bisher nur einen
# "emptyDir"-Platzhalter. Der ist an die Lebensdauer des Pods gebunden —
# jeder Neustart, jeder Redeploy und jede Node-Verlagerung loescht saemtliche
# hochgeladenen Bilder. Die Datenbank behaelt dabei die Bildpfade, die
# Dateien dahinter sind aber weg: die Seite zeigt danach ueberall kaputte
# Bilder statt der Produktfotos, ohne dass irgendwo ein Fehler auftaucht.
#
# ANZUPASSEN VOR DEM ANWENDEN:
# - storageClassName: auf eine im Cluster vorhandene StorageClass setzen
# ("kubectl get storageclass" zeigt die verfuegbaren; die als
# "(default)" markierte wird verwendet, wenn das Feld ganz entfernt
# wird). Die Zeile ist bewusst auskommentiert, damit das Manifest ohne
# Aenderung gegen die Default-StorageClass anwendbar bleibt.
# - storage: Groesse nach erwartetem Bildbestand. 5Gi reichen fuer einige
# tausend optimierte Produktfotos; das Hero-Video (bis 100 MB roh, siehe
# MAX_UPLOAD_VIDEO_BYTES in lib/uploads.ts) faellt dabei kaum ins
# Gewicht.
#
# ZUGRIFFSMODUS: ReadWriteOnce genuegt, weil das Deployment mit genau einer
# Replica und der Strategie "Recreate" laeuft (siehe 05-app-deployment.yaml)
# — es greift also nie mehr als ein Pod gleichzeitig darauf zu. Soll spaeter
# hochskaliert werden, braucht es hier ReadWriteMany (NFS/SMB/CephFS) UND
# einen gemeinsamen Rate-Limit-Store (siehe Kommentar zu replicas in
# 05-app-deployment.yaml).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wabenhain-uploads
namespace: wabenhain
spec:
accessModes:
- ReadWriteOnce
# storageClassName: "dein-storage-class"
resources:
requests:
storage: 5Gi
---
# Ablage der Rechnungs-PDFs (INVOICE_DIR, siehe 01-configmap.yaml und
# src/lib/invoice-storage.ts), sortiert nach Jahr und Kunde:
# /app/invoices/2026/mustermann-erika/RE-2026-A3F9C1B2.pdf
#
# BEWUSST EIN EIGENES VOLUME, nicht ein Unterordner von "uploads":
# app/uploads/[...path]/route.ts liefert JEDE Datei unterhalb von UPLOAD_DIR
# ohne Anmeldung aus. Laegen die Rechnungen dort, koennte jede Person, die den
# Pfad erraet, fremde Rechnungen samt Anschrift und Bestellinhalt
# herunterladen. Auf dieses Volume zeigt keine oeffentliche Route.
#
# Zweiter Grund fuer die Trennung: die Aufbewahrungsfristen sind
# verschieden. Produktfotos lassen sich ersetzen; Rechnungen unterliegen in
# Deutschland zehn Jahren Aufbewahrungspflicht (§ 147 AO). Dieses Volume
# sichert der CronJob in 07-backup-cronjob.yaml taeglich mit.
#
# 2Gi sind reichlich bemessen: eine Rechnung ist wenige Kilobyte gross, das
# reicht fuer weit mehr Bestellungen, als die Aufbewahrungsfrist umfasst.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wabenhain-invoices
namespace: wabenhain
spec:
accessModes:
- ReadWriteOnce
# storageClassName: "dein-storage-class"
resources:
requests:
storage: 2Gi