docs: Doku-Abgleich mit origin/main (Stand 2026-10-16)
Docker-Images bauen und veroeffentlichen / lint (push) Successful in 1m30s
Docker-Images bauen und veroeffentlichen / typecheck (push) Successful in 1m48s
Docker-Images bauen und veroeffentlichen / test (push) Successful in 1m33s
Docker-Images bauen und veroeffentlichen / build-and-push (push) Successful in 7m20s
Docker-Images bauen und veroeffentlichen / lint (push) Successful in 1m30s
Docker-Images bauen und veroeffentlichen / typecheck (push) Successful in 1m48s
Docker-Images bauen und veroeffentlichen / test (push) Successful in 1m33s
Docker-Images bauen und veroeffentlichen / build-and-push (push) Successful in 7m20s
README mit Projektstand (Links auf Issues #33, #32), fehlenden Befehlen und CI-Gate. BETRIEB: sha-gepinnte Manifeste, DB-Sicherung, Rollout per set image, Label-Selector im pg_dump-Befehl, Storage Box. k8s/README: Schritt 09, Warnung vor delete namespace. TESTS, OFFENE-PUNKTE, ARCHITEKTUR (ADR-8, ADR-9) aktualisiert; Kommentare in .env.example und Worker-CronJob korrigiert. Refs #31 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
9785eb221f
commit
8a85bfaed4
8 files changed
+85
-33
No files matched your search
+3
-1
@@ -26,7 +26,9 @@ BETTER_AUTH_SECRET=<anderer-zufaelliger-wert-mindestens-32-zeichen>
|
|||||||
# sk_test_… = Testmodus (Checkout ohne Live-Freigabe moeglich)
|
# sk_test_… = Testmodus (Checkout ohne Live-Freigabe moeglich)
|
||||||
# sk_live_… = Livemodus (nur mit APP_ENV=production UND allen Freigaben)
|
# sk_live_… = Livemodus (nur mit APP_ENV=production UND allen Freigaben)
|
||||||
# Empfohlen: eingeschraenkter Schluessel (rk_…) mit Schreibrechten fuer
|
# Empfohlen: eingeschraenkter Schluessel (rk_…) mit Schreibrechten fuer
|
||||||
# Customers, Checkout Sessions, PaymentIntents, PaymentMethods, Refunds.
|
# Customers, Checkout Sessions, PaymentIntents, PaymentMethods, Refunds und
|
||||||
|
# fuer die Folgeraten (Stripe Billing) zusaetzlich Products, Subscription
|
||||||
|
# Schedules, Subscriptions, Invoices (docs/BETRIEB.md, Abschnitt 4).
|
||||||
STRIPE_SECRET_KEY=<sk_test_…>
|
STRIPE_SECRET_KEY=<sk_test_…>
|
||||||
# Signaturgeheimnis des Webhook-Endpunkts (whsec_…), siehe docs/BETRIEB.md.
|
# Signaturgeheimnis des Webhook-Endpunkts (whsec_…), siehe docs/BETRIEB.md.
|
||||||
STRIPE_WEBHOOK_SECRET=<whsec_…>
|
STRIPE_WEBHOOK_SECRET=<whsec_…>
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
# ARTISTA MODELCAMP 2027 – Buchungswebsite
|
# ARTISTA MODELCAMP 2027 – Buchungswebsite
|
||||||
|
|
||||||
Deutschsprachige Onepage mit echter Buchungsstrecke: Stripe-Zahlung (einmalig oder in bis zu drei Raten), persistente Bestell- und Gutscheinverwaltung, geschützter Adminbereich mit Zwei-Faktor-Anmeldung, Docker- und RKE2-Deployment.
|
Deutschsprachige Onepage mit echter Buchungsstrecke: Stripe-Zahlung (einmalig oder in bis zu drei Raten), persistente Bestell- und Gutscheinverwaltung, Kontaktformular für ein Erstgespräch, geschützter Adminbereich mit Zwei-Faktor-Anmeldung, Docker- und RKE2-Deployment.
|
||||||
|
|
||||||
**Stand:** technisch vollständig, im Stripe-Testmodus buchbar. Der Live-Verkauf ist serverseitig gesperrt, bis alle Inhalte und Freigaben aus [docs/OFFENE-PUNKTE.md](docs/OFFENE-PUNKTE.md) im Adminbereich bestätigt sind.
|
**Stand:** technisch vollständig, im Stripe-Testmodus buchbar. Die Seite läuft im Cluster unter https://modelcamp.artistasphotography.de (Namespace `artista`). Der Live-Verkauf ist serverseitig gesperrt, bis alle Inhalte und Freigaben aus [docs/OFFENE-PUNKTE.md](docs/OFFENE-PUNKTE.md) im Adminbereich bestätigt sind.
|
||||||
|
|
||||||
|
## Projektstand
|
||||||
|
|
||||||
|
Was gebaut, geprüft und ausgerollt ist und was noch offen ist, steht im Übersichts-Issue [#33 „Projektstand und Übersicht“](https://git.encry.de/Nick.Rosenloecher/Artista-Filmcamp/issues/33). Aufgaben, die nur der Inhaber erledigen kann (Freigaben, Rechtstexte, Secrets, Rollouts), sammelt [#32 „Inhaber-Aufgaben“](https://git.encry.de/Nick.Rosenloecher/Artista-Filmcamp/issues/32). Jedes Arbeitspaket hat ein eigenes Issue.
|
||||||
|
|
||||||
| Dokument | Inhalt |
|
| Dokument | Inhalt |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [docs/ARCHITEKTUR.md](docs/ARCHITEKTUR.md) | Entscheidungen: Stack, Preisbindung, Ratenzahlung, Kapazität, Webhooks, Design |
|
| [docs/ARCHITEKTUR.md](docs/ARCHITEKTUR.md) | Entscheidungen: Stack, Preisbindung, Ratenzahlung, Kapazität, Webhooks, Kontaktformular, Design |
|
||||||
| [docs/BETRIEB.md](docs/BETRIEB.md) | RKE2-Deployment, Secrets, Stripe-Webhooks, E-Mail, Sicherung, Storno/Erstattung |
|
| [docs/BETRIEB.md](docs/BETRIEB.md) | RKE2-Deployment, CI, Secrets, Stripe-Webhooks, E-Mail, Sicherung, Storno/Erstattung |
|
||||||
|
| [k8s/README.md](k8s/README.md) | Manifeste Schritt für Schritt anwenden, neues Image ausrollen, Fehlersuche |
|
||||||
|
| [docs/MEDIEN.md](docs/MEDIEN.md) | Dateinamen für Logo, Headerbild, Intro-Video, Porträts und Reels; Medien aufbereiten |
|
||||||
| [docs/OFFENE-PUNKTE.md](docs/OFFENE-PUNKTE.md) | Noch benötigte Inhalte und Freigaben |
|
| [docs/OFFENE-PUNKTE.md](docs/OFFENE-PUNKTE.md) | Noch benötigte Inhalte und Freigaben |
|
||||||
| [docs/TESTS.md](docs/TESTS.md) | Ausgeführte Tests und verbleibende Einschränkungen |
|
| [docs/TESTS.md](docs/TESTS.md) | Ausgeführte Tests und verbleibende Einschränkungen |
|
||||||
|
|
||||||
@@ -46,9 +52,14 @@ Testkarte `4242 4242 4242 4242`, für 3-D-Secure-Fälle `4000 0027 6000 3184`. E
|
|||||||
| `pnpm typecheck` / `pnpm lint` | Typprüfung, ESLint |
|
| `pnpm typecheck` / `pnpm lint` | Typprüfung, ESLint |
|
||||||
| `pnpm test` | Unit- und Integrationstests (startet eine eigene PostgreSQL-Instanz) |
|
| `pnpm test` | Unit- und Integrationstests (startet eine eigene PostgreSQL-Instanz) |
|
||||||
| `pnpm test:e2e` | Playwright: Smartphone, Tablet, Desktop, axe-Barrierefreiheit (gegen laufenden Server) |
|
| `pnpm test:e2e` | Playwright: Smartphone, Tablet, Desktop, axe-Barrierefreiheit (gegen laufenden Server) |
|
||||||
|
| `pnpm db:dev` | Eingebettetes PostgreSQL für die Entwicklung (Port 54329) |
|
||||||
| `pnpm db:migrate` / `pnpm db:deploy` | Migration erstellen / anwenden |
|
| `pnpm db:migrate` / `pnpm db:deploy` | Migration erstellen / anwenden |
|
||||||
|
| `pnpm db:seed` | Nur Entwicklung: Early Bird simulieren, Test-Gutschein `TEST-STAMMMODEL` |
|
||||||
| `pnpm worker` | Einmaliger Worker-Lauf |
|
| `pnpm worker` | Einmaliger Worker-Lauf |
|
||||||
| `pnpm admin:create` | Admin-Konto anlegen (kein Standardpasswort) |
|
| `pnpm admin:create` | Admin-Konto anlegen (kein Standardpasswort) |
|
||||||
|
| `pnpm media:optimize <ordner>` | Videos und große Bilder vor dem Hochladen aufbereiten (siehe docs/MEDIEN.md) |
|
||||||
|
|
||||||
|
Bei jedem Push auf `main` prüft die CI (`.gitea/workflows/docker-build.yml`) zuerst `lint` samt `pnpm audit`, `typecheck` und `test`; erst wenn alle drei grün sind, baut sie die beiden Images. Die Playwright-Tests laufen nicht in der CI.
|
||||||
|
|
||||||
## Wo was gepflegt wird
|
## Wo was gepflegt wird
|
||||||
|
|
||||||
@@ -59,4 +70,5 @@ Testkarte `4242 4242 4242 4242`, für 3-D-Secure-Fälle `4000 0027 6000 3184`. E
|
|||||||
- **Feste Bilder:** `public/images/unterkunft/` (Villa, aus dem Booking.com-Eintrag, Nutzung vom Auftraggeber freigegeben).
|
- **Feste Bilder:** `public/images/unterkunft/` (Villa, aus dem Booking.com-Eintrag, Nutzung vom Auftraggeber freigegeben).
|
||||||
- **Logo, Headerbild, Porträts, Reels:** feste Dateinamen im Medienverzeichnis, werden automatisch erkannt. Liste: [docs/MEDIEN.md](docs/MEDIEN.md).
|
- **Logo, Headerbild, Porträts, Reels:** feste Dateinamen im Medienverzeichnis, werden automatisch erkannt. Liste: [docs/MEDIEN.md](docs/MEDIEN.md).
|
||||||
- **Widerrufsbelehrung:** Adminbereich → Einstellungen, erscheint unter `/widerruf`.
|
- **Widerrufsbelehrung:** Adminbereich → Einstellungen, erscheint unter `/widerruf`.
|
||||||
|
- **Empfänger des Formulars „Erstgespräch“:** die Veranstalter-E-Mail aus dem Adminbereich, ersatzweise `ADMIN_NOTIFY_EMAIL`. Ist keine von beiden gesetzt, lehnt das Formular Anfragen ab.
|
||||||
- **Weitere Medien:** Dateien ins Medienverzeichnis (`MEDIA_DIR`) legen und in `site.ts` als `{ kind: "image", src: "/media/…" }` eintragen.
|
- **Weitere Medien:** Dateien ins Medienverzeichnis (`MEDIA_DIR`) legen und in `site.ts` als `{ kind: "image", src: "/media/…" }` eintragen.
|
||||||
@@ -92,3 +92,15 @@ Der gehostete Checkout bildet den Ablauf vollständig ab: Betrag, Ablaufzeit, da
|
|||||||
## ADR-7: Adminbereich
|
## ADR-7: Adminbereich
|
||||||
|
|
||||||
Better Auth übernimmt E-Mail und Passwort, Registrierung ist ausgeschaltet. Konten entstehen nur über die Kommandozeile, TOTP-2FA ist Pflicht. Rate Limits der Anmeldung liegen in PostgreSQL, Passwörter werden gegen Datenlecks geprüft. Jede Seite, jede Server-Action und der CSV-Export prüfen Sitzung und 2FA selbst. Der Proxy leitet nur vorab um. Alle Buchungs-, Zahlungs- und Konfigurationsaktionen landen im AuditLog.
|
Better Auth übernimmt E-Mail und Passwort, Registrierung ist ausgeschaltet. Konten entstehen nur über die Kommandozeile, TOTP-2FA ist Pflicht. Rate Limits der Anmeldung liegen in PostgreSQL, Passwörter werden gegen Datenlecks geprüft. Jede Seite, jede Server-Action und der CSV-Export prüfen Sitzung und 2FA selbst. Der Proxy leitet nur vorab um. Alle Buchungs-, Zahlungs- und Konfigurationsaktionen landen im AuditLog.
|
||||||
|
|
||||||
|
## ADR-8: Kontaktformular „Erstgespräch“ (seit 06.10.2026)
|
||||||
|
|
||||||
|
- Eigener Abschnitt nach den Fragen (Anker `#erstgespraech`, `POST /api/contact`). Pflicht sind Name, E-Mail, Telefon (seit 07.10.2026, gleiche Regel wie bei der Buchung) und die Bestätigung der Datenschutzhinweise; Erreichbarkeit und Nachricht sind optional.
|
||||||
|
- Kein eigenes Datenmodell: Die Anfrage geht als E-Mail über den transaktionalen Postausgang an die Veranstalter-E-Mail aus dem Admin, ersatzweise `ADMIN_NOTIFY_EMAIL`. Ohne Empfänger lehnt das Formular ab.
|
||||||
|
- Bewusst keine automatische Bestätigungsmail an die angegebene Adresse, damit das Formular nicht zum Versand an Dritte missbraucht werden kann.
|
||||||
|
- Schutz wie bei der Buchung: nur JSON von der eigenen Seite, Rate Limit 5 Anfragen je Stunde und IP, Honeypot-Feld gegen Bots, doppeltes Absenden in derselben Minute ergibt eine Mail.
|
||||||
|
|
||||||
|
## ADR-9: Client-IP und Rate Limits (seit 12.10.2026)
|
||||||
|
|
||||||
|
- Rate Limits liegen in PostgreSQL (`RateLimitBucket`, gehashter IP-Schlüssel, Aufräumen nach einem Tag durch den Worker). IPv6-Adressen zählen je /64-Netz.
|
||||||
|
- Die Client-IP ist die Gegenstelle laut Traefik (letzter `X-Forwarded-For`-Eintrag). `CF-Connecting-IP` gilt nur als öffentliche Unicast-Adresse, `X-Real-IP` ohne `X-Forwarded-For` nur außerhalb der Produktion. Better Auth bekommt die IP ausschließlich über den serverseitig gesetzten Header `x-artista-client-ip`; ein vom Browser mitgeschickter Wert wird verworfen.
|
||||||
+22
-12
@@ -2,16 +2,18 @@
|
|||||||
|
|
||||||
## 1. Images bauen
|
## 1. Images bauen
|
||||||
|
|
||||||
**Automatisch (Gitea Actions):** `.gitea/workflows/docker-build.yml` baut bei jedem Push auf `main`, bei Tags `v*` oder manuell per „Run workflow“ beide Images und pusht sie in die Gitea-Registry:
|
**Automatisch (Gitea Actions):** `.gitea/workflows/docker-build.yml` läuft bei jedem Push auf `main`, bei Tags `v*` oder manuell per „Run workflow“. Zuerst prüfen drei Jobs auf dem isolierten Runner (`ci-isolated`, ohne Docker-Socket) den Code: `lint` (inklusive `pnpm audit --prod --audit-level high`), `typecheck` und `test` (mit PostgreSQL 18.6 als Service). Nur wenn alle drei grün sind, baut `build-and-push` auf dem Runner `docker-builder` beide Images und pusht sie in die Gitea-Registry:
|
||||||
|
|
||||||
- `main` ergibt die Tags `latest` und `sha-<kurz>`
|
- `main` ergibt die Tags `latest` und `sha-<kurz>`
|
||||||
- `v0.1.0` ergibt zusätzlich den Tag `0.1.0`
|
- `v0.1.0` ergibt zusätzlich den Tag `0.1.0`
|
||||||
|
|
||||||
Einmalig einrichten:
|
Einmalig einrichten:
|
||||||
- ein Personal Access Token mit Scope `package` als Repo-Secret `REGISTRY_TOKEN` hinterlegen (Repo-Einstellungen → Actions → Secrets)
|
- ein Personal Access Token mit Scope `package` als Repo-Secret `REGISTRY_TOKEN` hinterlegen (Repo-Einstellungen → Actions → Secrets)
|
||||||
- einen Runner mit dem Label `docker-builder` bereitstellen (wie bei Wabenhain)
|
- Runner mit den Labels `ci-isolated` (Prüfungen) und `docker-builder` (Image-Build) bereitstellen; im EnCry-Cluster sind beide vorhanden
|
||||||
|
|
||||||
Die Manifeste in `k8s/` nutzen den Tag `latest`. Für feste Versionen dort `:latest` durch z. B. `:0.1.0` ersetzen (`git tag v0.1.0 && git push origin v0.1.0`).
|
Die Manifeste in `k8s/` sind auf einen CI-Build gepinnt (`sha-<commit>`, Platzhalter `sha-REPLACE_ME` im Repo), nicht auf `latest`. Ausgerollt wird mit `kubectl set image`, siehe Abschnitt 3 und [k8s/README.md](../k8s/README.md).
|
||||||
|
|
||||||
|
Scheitert `build-and-push` beim Push mit `500 … cannot allocate memory`, lag das bisher an der Gitea-Registry, wenn gleichzeitig ein anderes Repo pushte. Dann den Lauf neu starten, sobald der andere Build fertig ist.
|
||||||
|
|
||||||
**Manuell:**
|
**Manuell:**
|
||||||
|
|
||||||
@@ -29,24 +31,32 @@ Der Build braucht Internet für `next/font` (Schriften werden eingebettet), aber
|
|||||||
- der Worker-CronJob, nötig für Reservierungsfreigabe, Abgleich der Folgeraten und Mail-Wiederholung
|
- der Worker-CronJob, nötig für Reservierungsfreigabe, Abgleich der Folgeraten und Mail-Wiederholung
|
||||||
- ConfigMap
|
- ConfigMap
|
||||||
- zwei NetworkPolicies (Schutz der Rate Limits und der Datenbank)
|
- zwei NetworkPolicies (Schutz der Rate Limits und der Datenbank)
|
||||||
|
- der Sicherungs-CronJob `artista-db-backup` (täglicher `pg_dump`, Abschnitt 7)
|
||||||
|
|
||||||
Eine eigene Sicherung im Cluster gibt es nicht, die SMB-Freigabe wird auf dem NAS gesichert.
|
**Schritt für Schritt zum Kopieren: [k8s/README.md](../k8s/README.md).** Die Manifeste liegen flach in `k8s/` (`00-namespace` bis `09-db-backup-cronjob`, Namensschema wie bei Wabenhain) und werden in dieser Reihenfolge einzeln angewendet. Die Secrets kommen aus `k8s/02-secret.yaml.example` und werden nicht eingecheckt. Pod Security: Erzwungen wird `baseline`, damit der Zertifikats-Solver von cert-manager starten kann. `restricted` wird nur gemeldet, die eigenen Pods erfüllen es.
|
||||||
|
|
||||||
**Schritt für Schritt zum Kopieren: [k8s/README.md](../k8s/README.md).** Die Manifeste liegen flach in `k8s/` (`00-namespace` bis `08-networkpolicy`, Namensschema wie bei Wabenhain) und werden in dieser Reihenfolge einzeln angewendet. Die Secrets kommen aus `k8s/02-secret.yaml.example` und werden nicht eingecheckt. Pod Security: Erzwungen wird `baseline`, damit der Zertifikats-Solver von cert-manager starten kann. `restricted` wird nur gemeldet, die eigenen Pods erfüllen es.
|
Im EnCry-Cluster liegen die SMB-Freigaben auf der Hetzner Storage Box, nicht auf einem lokalen NAS; „NAS“ meint im Folgenden diese Freigabe. Im Namespace `artista` läuft außerdem Artista-Telefonie (eigenes Repo `Artista-TeleTracking`). Sie nutzt eine eigene Datenbank `telefonie` in derselben PostgreSQL-Instanz `artista-modelcamp-postgres`. Wer die Instanz oder den Namespace anfasst, trifft beide Anwendungen.
|
||||||
|
|
||||||
Zu PostgreSQL:
|
Zu PostgreSQL:
|
||||||
- **PostgreSQL auf SMB ist eine bewusste Ausnahme.** SMB garantiert Sperren und fsync nicht in jedem Fall wie ein lokales Dateisystem. Bei einem NAS- oder Netzwerkausfall während Schreibvorgängen kann die Datenbank beschädigt werden.
|
- **PostgreSQL auf SMB ist eine bewusste Ausnahme.** SMB garantiert Sperren und fsync nicht in jedem Fall wie ein lokales Dateisystem. Bei einem NAS- oder Netzwerkausfall während Schreibvorgängen kann die Datenbank beschädigt werden.
|
||||||
- **Gemildert** wird das durch `hard`, `cache=strict`, SMB-Verschlüsselung (`seal`) und ein 120 Sekunden langes, sauberes Herunterfahren. Die Rechte setzen die `mountOptions`, weil `fsGroup` auf SMB nicht wirkt: Postgres `uid=70`, `0700`; Medien `uid=1001`, nur lesbar.
|
- **Gemildert** wird das durch `hard`, `cache=strict`, SMB-Verschlüsselung (`seal`) und ein 120 Sekunden langes, sauberes Herunterfahren. Die Rechte setzen die `mountOptions`, weil `fsGroup` auf SMB nicht wirkt: Postgres `uid=70`, `0700`; Medien `uid=1001`, nur lesbar.
|
||||||
- **Sicherung:** Die NAS-Sicherung ist für die laufende Datenbank nur mit **atomaren Snapshots** der Freigabe (Btrfs/ZFS) konsistent. Zusätzlicher Schutz bei Bedarf: gelegentlich einen `pg_dump` ziehen (Abschnitt 7).
|
- **Sicherung:** Eine Kopie der Freigabe ist für die laufende Datenbank nur mit **atomaren Snapshots** (Btrfs/ZFS) konsistent. Deshalb zieht der CronJob `artista-db-backup` jede Nacht einen `pg_dump` (Abschnitt 7).
|
||||||
|
- **Nach SMB-Unterbrechungen** melden lang laufende Postgres-Prozesse gelegentlich `could not seek to end of file … Bad file descriptor`. Abhilfe: `kubectl -n artista rollout restart sts/artista-modelcamp-postgres` (fährt sauber herunter).
|
||||||
|
|
||||||
## 3. Update ausrollen
|
## 3. Update ausrollen
|
||||||
|
|
||||||
|
App, Migration und Worker immer mit demselben Tag `sha-<commit>` aus der CI ausrollen (genauer Befehl in [k8s/README.md](../k8s/README.md), Abschnitt „Bei einem neuen Image“):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kubectl -n artista rollout restart deploy/artista-modelcamp-app
|
TAG=sha-<commit>
|
||||||
|
kubectl -n artista set image deployment/artista-modelcamp-app migrate=git.encry.de/nick.rosenloecher/artista-modelcamp-tools:$TAG app=git.encry.de/nick.rosenloecher/artista-modelcamp:$TAG
|
||||||
|
kubectl -n artista set image cronjob/artista-modelcamp-worker worker=git.encry.de/nick.rosenloecher/artista-modelcamp-tools:$TAG
|
||||||
kubectl -n artista rollout status deploy/artista-modelcamp-app
|
kubectl -n artista rollout status deploy/artista-modelcamp-app
|
||||||
kubectl -n artista logs deploy/artista-modelcamp-app -c migrate # Migrationsausgabe
|
kubectl -n artista logs deploy/artista-modelcamp-app -c migrate # Migrationsausgabe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Ein `rollout restart` allein holt kein neues Image, weil die Manifeste auf einen festen Tag zeigen.
|
||||||
|
|
||||||
Jeder neue App-Pod migriert per initContainer zuerst die Datenbank. Scheitert das, startet der Pod nicht, und die alten Pods laufen weiter.
|
Jeder neue App-Pod migriert per initContainer zuerst die Datenbank. Scheitert das, startet der Pod nicht, und die alten Pods laufen weiter.
|
||||||
|
|
||||||
Migrationen sind vorwärtskompatibel zu schreiben (erst Spalten ergänzen, später entfernen), damit alte und neue Pods während des Rollouts parallel laufen können.
|
Migrationen sind vorwärtskompatibel zu schreiben (erst Spalten ergänzen, später entfernen), damit alte und neue Pods während des Rollouts parallel laufen können.
|
||||||
@@ -85,10 +95,10 @@ Ein fehlgeschlagener Lauf erscheint als fehlgeschlagener Job (`kubectl -n artist
|
|||||||
|
|
||||||
## 7. Sicherung und Wiederherstellung
|
## 7. Sicherung und Wiederherstellung
|
||||||
|
|
||||||
- **Sicherung:** Die NAS-Sicherung der SMB-Freigabe übernimmt das Speichersystem. Für PostgreSQL gilt das nur mit atomaren Snapshots, siehe Abschnitt 2. Im Cluster läuft kein Backup-Job.
|
- **Täglicher Dump im Cluster:** Der CronJob `artista-db-backup` (`k8s/09-db-backup-cronjob.yaml`) läuft um 02:25 Uhr (Zeitzone des Clusters) und schreibt `pg_dump -Fc` der Datenbank `artista` in die PVC `artista-db-backup` (StorageClass `smb-backup`, Ordner `Backups/artista/` auf der Storage Box). Aufbewahrung: 14 Tage täglich (`daily/`), 8 Wochen sonntags (`weekly/`). Die Datenbank `telefonie` derselben Instanz ist darin nicht enthalten. Stand prüfen: `kubectl -n artista get cronjob artista-db-backup` und `kubectl -n artista get jobs | grep artista-db-backup`.
|
||||||
- **Logischer Dump** (empfohlen vor Updates und vor dem Live-Verkauf):
|
- **Logischer Dump von Hand** (zusätzlich vor Updates und vor dem Live-Verkauf):
|
||||||
```bash
|
```bash
|
||||||
POD=$(kubectl -n artista get pod -l app.kubernetes.io/name=artista-modelcamp-postgres -o name | head -1)
|
POD=$(kubectl -n artista get pod -l app=artista-modelcamp-postgres -o name | head -1)
|
||||||
kubectl -n artista exec $POD -- pg_dump -U artista -d artista --format=custom > artista-$(date +%F).dump
|
kubectl -n artista exec $POD -- pg_dump -U artista -d artista --format=custom > artista-$(date +%F).dump
|
||||||
```
|
```
|
||||||
- **Wiederherstellen:**
|
- **Wiederherstellen:**
|
||||||
@@ -96,8 +106,8 @@ Ein fehlgeschlagener Lauf erscheint als fehlgeschlagener Job (`kubectl -n artist
|
|||||||
2. Einspielen: `kubectl -n artista exec -i $POD -- pg_restore -U artista -d artista --clean --if-exists < artista-YYYY-MM-DD.dump`
|
2. Einspielen: `kubectl -n artista exec -i $POD -- pg_restore -U artista -d artista --clean --if-exists < artista-YYYY-MM-DD.dump`
|
||||||
3. App wieder hochskalieren (der initContainer migriert dabei), Worker fortsetzen.
|
3. App wieder hochskalieren (der initContainer migriert dabei), Worker fortsetzen.
|
||||||
4. Abgleich: Zahlungen, die nach dem Sicherungszeitpunkt eingingen, im Stripe-Dashboard prüfen. Stripe stellt Webhooks bis zu drei Tage erneut zu (Dashboard → Webhooks → „Erneut senden“).
|
4. Abgleich: Zahlungen, die nach dem Sicherungszeitpunkt eingingen, im Stripe-Dashboard prüfen. Stripe stellt Webhooks bis zu drei Tage erneut zu (Dashboard → Webhooks → „Erneut senden“).
|
||||||
- **Medien:** liegen im SMB-Ordner `modelcamp-media` und sind in der NAS-Sicherung enthalten.
|
- **Medien:** liegen im SMB-Ordner `modelcamp-media`; gesichert sind sie nur über die Sicherung der Freigabe selbst.
|
||||||
- **Wiederherstellung üben:** mindestens einmal vor dem Live-Verkauf in einen Test-Namespace.
|
- **Wiederherstellung üben:** mindestens einmal vor dem Live-Verkauf in einen Test-Namespace. Das ist noch nicht geschehen.
|
||||||
|
|
||||||
## 8. Stornierung und Erstattung
|
## 8. Stornierung und Erstattung
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Noch benötigte Inhalte und Freigaben
|
# Noch benötigte Inhalte und Freigaben
|
||||||
|
|
||||||
Stand 01.10.2026. Bereits bestätigt und per Migration `20261001120000_camp_2027_bestaetigt` eingetragen (im Admin änderbar):
|
Stand 16.10.2026. Dieselben Punkte stehen mit Verweis auf die Detail-Issues in der Gitea-Sammelliste [#32 „Inhaber-Aufgaben“](https://git.encry.de/Nick.Rosenloecher/Artista-Filmcamp/issues/32).
|
||||||
|
|
||||||
|
Bereits bestätigt und per Migration `20261001120000_camp_2027_bestaetigt` bzw. `20261002090000_termin_mai_2027` eingetragen (im Admin änderbar). Damit sind im Admin die Freigaben Termin, Ort, Unterkunft und Platzzahl gesetzt, sofern sie dort niemand zurückgenommen hat:
|
||||||
|
|
||||||
- Termin 03.05. bis 09.05.2027 (laut Wochenplan vom 02.10.2026), Villa La Chiesina, Arcidosso (Toskana)
|
- Termin 03.05. bis 09.05.2027 (laut Wochenplan vom 02.10.2026), Villa La Chiesina, Arcidosso (Toskana)
|
||||||
- Unterbringung: eigenes Bett im geteilten Zimmer, keine Einzelzimmer
|
- Unterbringung: eigenes Bett im geteilten Zimmer, keine Einzelzimmer
|
||||||
@@ -9,7 +11,7 @@ Stand 01.10.2026. Bereits bestätigt und per Migration `20261001120000_camp_2027
|
|||||||
- Tagesablauf aller sieben Tage, Bustransfer ab und bis Florenz, Verpflegung (Frühstück, Snacks, Abendessen)
|
- Tagesablauf aller sieben Tage, Bustransfer ab und bis Florenz, Verpflegung (Frühstück, Snacks, Abendessen)
|
||||||
- Early Bird 01.11. bis 31.12.2026, ohne Anzeige eines Verkaufsstarts
|
- Early Bird 01.11. bis 31.12.2026, ohne Anzeige eines Verkaufsstarts
|
||||||
|
|
||||||
Solange die mit **(Freigabe)** markierten Punkte fehlen, bleibt der Live-Verkauf serverseitig gesperrt. Der aktuelle Stand steht im Admin-Dashboard unter „Fehlt für den Live-Verkauf“.
|
Solange die mit **(Freigabe)** markierten Punkte fehlen, bleibt der Live-Verkauf serverseitig gesperrt. Der aktuelle Stand steht im Admin-Dashboard unter „Fehlt für den Live-Verkauf“. Sind alle Freigaben erteilt und ist der Live-Verkauf eingeschaltet, startet der Verkauf sofort zum Early-Bird-Preis; die Early-Bird-Phase läuft vom 01.11. bis 31.12.2026.
|
||||||
|
|
||||||
## Inhalte
|
## Inhalte
|
||||||
|
|
||||||
@@ -40,7 +42,8 @@ Solange die mit **(Freigabe)** markierten Punkte fehlen, bleibt der Live-Verkauf
|
|||||||
|
|
||||||
## Betrieb
|
## Betrieb
|
||||||
|
|
||||||
- Stripe-Konto live verifiziert, Webhook-Endpunkte (Test und Live) mit Secrets angelegt.
|
- Stripe-Konto live verifiziert, Webhook-Endpunkte (Test und Live) mit Secrets angelegt, Billing-Einstellungen für Folgeraten gesetzt (docs/BETRIEB.md, Abschnitt 4).
|
||||||
- SMTP-Anbieter sowie SPF, DKIM und DMARC der Absenderdomain.
|
- SMTP-Anbieter sowie SPF, DKIM und DMARC der Absenderdomain; Veranstalter-E-Mail oder `ADMIN_NOTIFY_EMAIL` gesetzt, sonst lehnt das Formular „Erstgespräch“ Anfragen ab.
|
||||||
- Domain, TLS und StorageClasses im Cluster (siehe docs/BETRIEB.md).
|
- Erledigt: Domain `modelcamp.artistasphotography.de`, TLS über cert-manager, SMB-Speicher und die StorageClass `smb-backup` im Cluster.
|
||||||
- Wiederherstellung aus der Sicherung einmal geübt.
|
- Wiederherstellung aus der Sicherung einmal geübt (noch offen).
|
||||||
|
- Optional: Google-Rezensionen (`GOOGLE_PLACES_API_KEY`, `GOOGLE_PLACE_ID` im Secret); ohne Werte bleibt der Abschnitt ausgeblendet.
|
||||||
+11
-6
@@ -1,10 +1,14 @@
|
|||||||
# Tests und Abnahme
|
# Tests und Abnahme
|
||||||
|
|
||||||
Stand 27.09.2026. Alle aufgeführten Läufe sind lokal ausgeführt und bestanden.
|
Stand 16.10.2026. Die Abschnitte unten sind chronologisch gewachsen; der aktuelle Stand steht hier oben.
|
||||||
|
|
||||||
|
**Aktuell:** `pnpm test` umfasst 10 Testdateien (rund 130 Tests; 132 beim lokalen Lauf am 14.10.2026 zu Commit `f29dbcf`, seither kam mit `9785eb2` ein Regressionstest hinzu). Seit `0421d7d` (14.10.2026) laufen `lint` mit `pnpm audit`, `typecheck` und `test` bei jedem Push auf `main` in der CI; der Test-Job nutzt PostgreSQL 18.6 als Service (`TEST_DATABASE_URL`). Letzter CI-Lauf (`9785eb2`, 15.10.2026): lint, typecheck und test grün, nur der Image-Build schlug fehl (Issue #20). Die Playwright-Tests (`pnpm test:e2e`, 3 Tests je Profil) laufen nur lokal.
|
||||||
|
|
||||||
|
Seit dem ersten Stand hinzugekommene Testdateien: `client-ip.test.ts` (Client-IP hinter Traefik und Cloudflare, IPv6 je /64, Proxy um NextRequest), `contact.integration.test.ts` (Formular „Erstgespräch“), `email-outbox.integration.test.ts` (Postausgang, endgültige und vorübergehende SMTP-Fehler), `review-format.test.ts` (Google-Rezensionen).
|
||||||
|
|
||||||
## Automatisierte Tests
|
## Automatisierte Tests
|
||||||
|
|
||||||
`pnpm test` umfasst 72 Tests in 6 Dateien. Sie laufen gegen eine echte, frisch gestartete PostgreSQL-18-Instanz, damit Zeilensperren und Trigger mitgetestet werden.
|
Stand 27.09.2026: `pnpm test` umfasste 72 Tests in 6 Dateien. Sie laufen gegen eine echte, frisch gestartete PostgreSQL-18-Instanz, damit Zeilensperren und Trigger mitgetestet werden.
|
||||||
|
|
||||||
| Geschäftsrisiko | Datei |
|
| Geschäftsrisiko | Datei |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -33,7 +37,7 @@ Stand 27.09.2026. Alle aufgeführten Läufe sind lokal ausgeführt und bestanden
|
|||||||
|
|
||||||
Bildschirmfotos aller drei Größen wurden visuell geprüft.
|
Bildschirmfotos aller drei Größen wurden visuell geprüft.
|
||||||
|
|
||||||
Letzter Lauf 01.10.2026 nach Einbau von Team, Villa-Bildern und bestätigtem Termin: 81/81 Unit- und Integrationstests, 9/9 E2E-Tests, Produktionsbuild grün. Team und Unterkunft wurden zusätzlich per Bildschirmfoto auf Smartphone (390 px) und Desktop (1440 px) geprüft. Mit Termin 08.05.2027 fällt die letzte Rate auf den 08.04.2027.
|
Letzter Lauf 01.10.2026 nach Einbau von Team, Villa-Bildern und bestätigtem Termin: 81/81 Unit- und Integrationstests, 9/9 E2E-Tests, Produktionsbuild grün. Team und Unterkunft wurden zusätzlich per Bildschirmfoto auf Smartphone (390 px) und Desktop (1440 px) geprüft. Mit dem damaligen Termin 08.05.2027 fiel die letzte Rate auf den 08.04.2027; seit dem Termin 03.05.2027 (Migration vom 02.10.2026) ist es der 03.04.2027.
|
||||||
|
|
||||||
Hinweis: Läuft der Testserver nicht unter der Adresse aus `APP_URL`, lehnt der Same-Origin-Schutz die Preisabfrage ab (403). Dann hängt der Buchungsdialog bei „Preise werden geladen …“. Server deshalb mit passender `APP_URL` starten, z. B. `APP_URL=http://localhost:3005 PORT=3005 pnpm start` und `E2E_BASE_URL=http://localhost:3005 pnpm test:e2e`.
|
Hinweis: Läuft der Testserver nicht unter der Adresse aus `APP_URL`, lehnt der Same-Origin-Schutz die Preisabfrage ab (403). Dann hängt der Buchungsdialog bei „Preise werden geladen …“. Server deshalb mit passender `APP_URL` starten, z. B. `APP_URL=http://localhost:3005 PORT=3005 pnpm start` und `E2E_BASE_URL=http://localhost:3005 pnpm test:e2e`.
|
||||||
|
|
||||||
@@ -53,7 +57,8 @@ Lauf 06.10.2026 nach Umstellung der Folgeraten auf Stripe Billing (inkl. Anhalte
|
|||||||
|
|
||||||
## Nicht ausgeführt / Einschränkungen
|
## Nicht ausgeführt / Einschränkungen
|
||||||
|
|
||||||
- **Echte Stripe-Testbuchung** gegen die Stripe-API: Ein Stripe-Testschlüssel lag nicht vor. Die Stripe-Logik ist gegen eine Attrappe mit Stripe-Semantik getestet (Idempotenz, Session-Zustände, Rechnungen des Ratenplans), die Webhook-Signatur mit der echten Stripe-Bibliothek. Mit Schlüssel: Anleitung in der README (`stripe listen`, Testkarten).
|
- **Vollständige Stripe-Testbuchung** über die Oberfläche bis zum Webhook: In der Testsuite läuft die Stripe-Logik gegen eine Attrappe mit Stripe-Semantik (Idempotenz, Session-Zustände, Rechnungen des Ratenplans), die Webhook-Signatur mit der echten Stripe-Bibliothek. Gegen die echte Stripe-Test-API geprüft ist bisher nur der Ratenplan (`StripeGateway` mit Testuhr, Lauf vom 06.10.2026). Ein dokumentierter Durchlauf Buchung → Checkout → Webhook → Bestätigungsmail auf dem Cluster fehlt (Issue #27). Anleitung lokal in der README (`stripe listen`, Testkarten).
|
||||||
- **Docker-Image und Cluster:** Auf dem Entwicklungsrechner gibt es kein Docker und keinen Cluster-Zugang. Das Dockerfile ist nicht gebaut und nicht im Cluster ausgerollt. Geprüft sind der Standalone-Build und das Rendern der Manifeste.
|
- **Docker-Image und Cluster:** Seit dem 27.09.2026 baut die CI beide Images; die Seite läuft im Cluster (zuletzt belegt ausgerollt: `sha-5a88f3d`, 13.10.2026). Eine automatisierte Prüfung nach dem Rollout gibt es nicht; der Monitoring-Dienst Gatus im Cluster prüft die Erreichbarkeit.
|
||||||
- **SMTP-Versand:** nur über den Datei-Testausgang geprüft.
|
- **SMTP-Versand:** im Repo nur über den Datei-Testausgang geprüft. Ob im Cluster ein echter SMTP-Zugang eingetragen ist, ist hier nicht dokumentiert.
|
||||||
|
- **E2E-Tests** laufen nicht in der CI.
|
||||||
- **Referenzseite:** nur über eine Textzusammenfassung ausgewertet (siehe ARCHITEKTUR.md).
|
- **Referenzseite:** nur über eine Textzusammenfassung ausgewertet (siehe ARCHITEKTUR.md).
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# Alle 10 Minuten: faellige Raten einziehen, abgelaufene Reservierungen
|
# Alle 10 Minuten: abgelaufene Reservierungen freigeben, fehlende
|
||||||
# freigeben, E-Mails nachsenden. Ohne ihn funktioniert Ratenzahlung nicht.
|
# Stripe-Ratenplaene anlegen, ueberfaellige Raten mit Stripe abgleichen,
|
||||||
|
# E-Mails nachsenden (Einzug der Folgeraten macht Stripe Billing,
|
||||||
|
# docs/ARCHITEKTUR.md ADR-3). Ohne ihn funktioniert Ratenzahlung nicht.
|
||||||
apiVersion: batch/v1
|
apiVersion: batch/v1
|
||||||
kind: CronJob
|
kind: CronJob
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
+8
-2
@@ -4,6 +4,7 @@ Rohe Manifeste wie bei Wabenhain, Nummerierung = Reihenfolge. Voraussetzungen im
|
|||||||
- **Traefik** als Ingress-Controller (IngressClass `traefik`)
|
- **Traefik** als Ingress-Controller (IngressClass `traefik`)
|
||||||
- **cert-manager** mit dem ClusterIssuer `letsencrypt-prod`
|
- **cert-manager** mit dem ClusterIssuer `letsencrypt-prod`
|
||||||
- **csi-driver-smb** für die SMB-Volumes
|
- **csi-driver-smb** für die SMB-Volumes
|
||||||
|
- die StorageClass **`smb-backup`** für den Sicherungs-CronJob (`09-db-backup-cronjob.yaml`); im EnCry-Cluster seit 12.10.2026 vorhanden
|
||||||
|
|
||||||
Prüfen:
|
Prüfen:
|
||||||
|
|
||||||
@@ -11,8 +12,11 @@ Prüfen:
|
|||||||
kubectl get ingressclass
|
kubectl get ingressclass
|
||||||
kubectl get clusterissuer letsencrypt-prod
|
kubectl get clusterissuer letsencrypt-prod
|
||||||
kubectl get csidrivers | grep smb
|
kubectl get csidrivers | grep smb
|
||||||
|
kubectl get storageclass smb-backup
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Im EnCry-Cluster läuft die Seite unter `modelcamp.artistasphotography.de`. Der DNS-Eintrag ist bei Cloudflare bewusst nicht geproxyt (DNS-only), deshalb bekommt dieser Ingress keine `cloudflare-only`-Middleware. Im selben Namespace läuft Artista-Telefonie (Repo `Artista-TeleTracking`) mit einer eigenen Datenbank `telefonie` in `artista-modelcamp-postgres`.
|
||||||
|
|
||||||
## Einmalige Vorbereitung
|
## Einmalige Vorbereitung
|
||||||
|
|
||||||
1. **Images:** `.gitea/workflows/docker-build.yml` baut bei jedem Push auf `main` die Images `git.encry.de/nick.rosenloecher/artista-modelcamp:latest` und `…-tools:latest`. Voraussetzung ist das Repo-Secret `REGISTRY_TOKEN` (Personal Access Token mit `package`-Scope), wie bei Wabenhain.
|
1. **Images:** `.gitea/workflows/docker-build.yml` baut bei jedem Push auf `main` die Images `git.encry.de/nick.rosenloecher/artista-modelcamp:latest` und `…-tools:latest`. Voraussetzung ist das Repo-Secret `REGISTRY_TOKEN` (Personal Access Token mit `package`-Scope), wie bei Wabenhain.
|
||||||
@@ -22,8 +26,9 @@ kubectl get csidrivers | grep smb
|
|||||||
helm install csi-driver-smb csi-driver-smb/csi-driver-smb -n kube-system
|
helm install csi-driver-smb csi-driver-smb/csi-driver-smb -n kube-system
|
||||||
```
|
```
|
||||||
3. **NAS:** Die Ordner `artista/modelcamp-postgres` (leer) und `artista/modelcamp-media` anlegen und SMB-Verschlüsselung erlauben. `nas.example.lan` in `03-storage.yaml` anpassen.
|
3. **NAS:** Die Ordner `artista/modelcamp-postgres` (leer) und `artista/modelcamp-media` anlegen und SMB-Verschlüsselung erlauben. `nas.example.lan` in `03-storage.yaml` anpassen.
|
||||||
4. **Domain:** `modelcamp.example.org` in `01-configmap.yaml` (APP_URL) und `06-ingress.yaml` (2×) ersetzen. Der DNS-Eintrag muss auf den Cluster zeigen, sonst stellt cert-manager kein Zertifikat aus.
|
4. **Domain:** `modelcamp.example.org` in `01-configmap.yaml` (APP_URL) und `06-ingress.yaml` (2×) ersetzen (im EnCry-Cluster `modelcamp.artistasphotography.de`). Der DNS-Eintrag muss auf den Cluster zeigen, sonst stellt cert-manager kein Zertifikat aus.
|
||||||
5. **Secrets:** `cp k8s/02-secret.yaml.example k8s/02-secret.yaml` und ausfüllen. Die Datei steht in `.gitignore`, bitte **nie** mit echten Werten committen.
|
5. **Secrets:** `cp k8s/02-secret.yaml.example k8s/02-secret.yaml` und ausfüllen. Die Datei steht in `.gitignore`, bitte **nie** mit echten Werten committen.
|
||||||
|
6. **Image-Tag:** In `05-app-deployment.yaml` (2×) und `07-worker-cronjob.yaml` steht der Platzhalter `sha-REPLACE_ME`. Vor dem ersten Anwenden durch den Tag des letzten grünen CI-Laufs ersetzen (`sha-<commit>`, 7 Zeichen) oder nach dem Anwenden wie unter „Bei einem neuen Image“ setzen.
|
||||||
|
|
||||||
## Deployment (auch für einen kompletten Neuaufbau)
|
## Deployment (auch für einen kompletten Neuaufbau)
|
||||||
|
|
||||||
@@ -38,6 +43,7 @@ kubectl apply -f k8s/05-app-deployment.yaml
|
|||||||
kubectl apply -f k8s/06-ingress.yaml
|
kubectl apply -f k8s/06-ingress.yaml
|
||||||
kubectl apply -f k8s/07-worker-cronjob.yaml
|
kubectl apply -f k8s/07-worker-cronjob.yaml
|
||||||
kubectl apply -f k8s/08-networkpolicy.yaml
|
kubectl apply -f k8s/08-networkpolicy.yaml
|
||||||
|
kubectl apply -f k8s/09-db-backup-cronjob.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Die Migration läuft automatisch als initContainer vor dem App-Start. Den Stand zeigen:
|
Die Migration läuft automatisch als initContainer vor dem App-Start. Den Stand zeigen:
|
||||||
@@ -47,7 +53,7 @@ kubectl rollout status deployment/artista-modelcamp-app -n artista
|
|||||||
kubectl get certificate -n artista # READY muss True werden (meist < 2 Minuten)
|
kubectl get certificate -n artista # READY muss True werden (meist < 2 Minuten)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Neuaufbau mit `kubectl delete namespace artista`:** Die PersistentVolumes (`artista-modelcamp-postgres`, `artista-modelcamp-media`) gehören keinem Namespace und bleiben auf `Released` stehen. Die Daten auf dem NAS bleiben erhalten (`Retain`). Damit die neuen Claims sie wieder binden können, vor dem erneuten Anlegen:
|
**Neuaufbau mit `kubectl delete namespace artista`:** Achtung, das löscht auch Artista-Telefonie samt deren Secret; deren Datenbank liegt auf demselben Postgres-Volume. Die PersistentVolumes (`artista-modelcamp-postgres`, `artista-modelcamp-media`) gehören keinem Namespace und bleiben auf `Released` stehen. Die Daten auf dem NAS bleiben erhalten (`Retain`). Damit die neuen Claims sie wieder binden können, vor dem erneuten Anlegen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kubectl patch pv artista-modelcamp-postgres -p '{"spec":{"claimRef":null}}'
|
kubectl patch pv artista-modelcamp-postgres -p '{"spec":{"claimRef":null}}'
|
||||||
|
|||||||
Reference in new issue
Block a user