From f5bf28202759ecf23f28c3aba32c40404d6de502 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Fri, 3 Jul 2026 12:38:24 +0000 Subject: [PATCH] Deploy: .env.example vervollstaendigt + Deploy-Runbook (R5.1/R5.2) Alle projektspezifischen Env-Keys dokumentiert (Multi-Domain, Legacy-DBs, Stripe/Billing, KI, Sanctum, Newsletter, Vite); Runbook fuers neue Laravel-System mit Prod-.env-Solltabelle, Release-Schritten, Queue-Worker (classification), Cron-Tasks inkl. Ausfall-Folgen, Stripe-Webhook, Erst-Einrichtung, Smoke-Test und Rollback. Co-Authored-By: Claude Fable 5 --- .env.example | 113 +++++++++++++++-- dev/release/deploy-runbook.md | 153 ++++++++++++++++++++++++ dev/release/entwicklungsplan-release.md | 4 +- 3 files changed, 260 insertions(+), 10 deletions(-) create mode 100644 dev/release/deploy-runbook.md diff --git a/.env.example b/.env.example index 371e4699..39d5ee23 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ -APP_NAME=Laravel +APP_NAME=Pressekonto APP_ENV=local APP_KEY= APP_DEBUG=true -APP_URL=http://localhost +APP_URL=https://pressekonto.test APP_LOCALE=de APP_FALLBACK_LOCALE=de @@ -26,6 +26,34 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug +# --------------------------------------------------------------------------- +# Multi-Domain-Setup (config/domains.php) +# Hub: pressekonto (Portal-Backend + Hub-Landing) — Spokes: presseecho, +# businessportal24. Prod: .test durch echte Domains ersetzen +# (pressekonto.de, presseecho.de, businessportal24.com). +# --------------------------------------------------------------------------- +APP_PROTOCOL="https://" + +APP_PORTAL_NAME="pressekonto.test" +APP_PORTAL_URL="https://pressekonto.test" +APP_PORTAL_PRIMARY="#526266" +APP_PORTAL_SECONDARY="#82a0a7" +# APP_PORTAL_ASSET_URL= + +APP_PRESSEECHO_NAME=presseecho.test +APP_PRESSEECHO_URL=https://presseecho.test +APP_PRESSEECHO_PRIMARY="#345636" +APP_PRESSEECHO_SECONDARY="#6b8f71" + +APP_BUSINESSPORTAL_NAME=businessportal24.test +APP_BUSINESSPORTAL_URL=https://businessportal24.test +APP_BUSINESSPORTAL_PRIMARY="#cf3628" +APP_BUSINESSPORTAL_SECONDARY="#f0834a" + +# Sanctum: Hosts, die stateful (Session-Cookie) auf die API zugreifen dürfen. +# Prod: echte Domains kommasepariert eintragen. +SANCTUM_STATEFUL_DOMAINS=pressekonto.test,presseecho.test,businessportal24.test + DB_CONNECTION=sqlite # DB_HOST=127.0.0.1 # DB_PORT=3306 @@ -33,11 +61,27 @@ DB_CONNECTION=sqlite # DB_USERNAME=root # DB_PASSWORD= +# --------------------------------------------------------------------------- +# Legacy-Datenbanken (Alt-Systeme, read-only für Migration/Abgleich). +# Die businessportal-Verbindung ist ein Snapshot-Dump, nicht live. +# --------------------------------------------------------------------------- +DB_HOST_BUSINESSPORTAL=127.0.0.1 +DB_DATABASE_BUSINESSPORTAL= +DB_USERNAME_BUSINESSPORTAL= +DB_PASSWORD_BUSINESSPORTAL= + +DB_HOST_PRESSEECHO=127.0.0.1 +DB_DATABASE_PRESSEECHO= +DB_USERNAME_PRESSEECHO= +DB_PASSWORD_PRESSEECHO= + SESSION_DRIVER=database SESSION_LIFETIME=120 SESSION_ENCRYPT=false SESSION_PATH=/ SESSION_DOMAIN=null +# Prod: true setzen (Cookies nur über HTTPS). +# SESSION_SECURE_COOKIE=true BROADCAST_CONNECTION=log FILESYSTEM_DISK=local @@ -53,6 +97,7 @@ REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 +# Prod: echten Mailer konfigurieren — Default `log` versendet NICHTS. MAIL_MAILER=log MAIL_SCHEME=null MAIL_HOST=127.0.0.1 @@ -69,6 +114,64 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" +# Lokaler Vite-Dev-Server (dual-port Setup, siehe VITE-SETUP.md). +VITE_DEV_SERVER_URL=https://assets.pressekonto.test + +# --------------------------------------------------------------------------- +# Stripe / Billing (Laravel Cashier) +# Prod: Live-Keys + Live-Preis-IDs eintragen [EXTERN: R1.2]. +# --------------------------------------------------------------------------- +STRIPE_KEY= +STRIPE_SECRET= +STRIPE_WEBHOOK_SECRET= +# Stripe-Preis-ID des Einzelkaufs (eine PM, 19 €). +STRIPE_PRICE_SINGLE_PM= +CASHIER_CURRENCY=eur +CASHIER_CURRENCY_LOCALE=de_DE + +# Umsatzsteuer/Rechnungsangaben. +# BILLING_VAT_RATE=0.19 +# BILLING_DEFAULT_CURRENCY=EUR +# BILLING_MANUAL_DUE_DAYS=14 +BILLING_OWN_VAT_ID= + +# Abo-/Bestandsschutz-Gate (WS-5). Default (auch ohne Env-Key) ist true: +# Einreichen zur Prüfung erfordert ein aktives Abo, einen bezahlten Einzelkauf +# oder Bestandsschutz (grandfathered) — siehe User::hasActiveBooking(). +# Nur für lokale Entwicklung bewusst auf false setzen (Gate offen für alle). +BILLING_ENFORCE_BOOKING=true + +# --------------------------------------------------------------------------- +# KI: Klassifikation (Red-Flag-Gate) + Content-Score (config/scoring.php). +# Ohne OPENAI_API_KEY laufen die Jobs nicht — Queue-Worker erforderlich. +# --------------------------------------------------------------------------- +OPENAI_API_KEY= +# OPENAI_API_URL= +# OPENAI_MODEL= +# OPENAI_TIMEOUT=30 + +# CLASSIFICATION_PROVIDER=openai +# CLASSIFICATION_MODEL= +# CLASSIFICATION_TIMEOUT=15 +# CLASSIFICATION_GREEN_DELAY=0 +# CLASSIFICATION_YELLOW_MANUAL=true + +# CONTENT_SCORE_PROVIDER=openai +# CONTENT_SCORE_MODEL= +# CONTENT_SCORE_TIMEOUT=30 +# CONTENT_SCORE_HOCHWERTIG=80 +# CONTENT_SCORE_GEPRUEFT=60 + +# --------------------------------------------------------------------------- +# Newsletter-Sync (config/newsletter.php). Provider `placeholder` bindet den +# Null-Client; enabled=true mit anderem Provider wirft bewusst, bis eine +# echte Anbindung implementiert ist. +# --------------------------------------------------------------------------- +NEWSLETTER_SYNC_ENABLED=false +NEWSLETTER_SYNC_PROVIDER=placeholder +NEWSLETTER_SYNC_ENDPOINT= +NEWSLETTER_SYNC_API_KEY= +# NEWSLETTER_SYNC_TIMEOUT=10 # Google-Login (Laravel Socialite). Die Redirect-URI muss in der Google Cloud # Console EXAKT der Callback-URL der Portal-Domain entsprechen (Login läuft auf @@ -84,9 +187,3 @@ GOOGLE_REDIRECT_URI=https://pressekonto.com/auth/google/callback CONTACT_EMAIL_PRESSEKONTO= CONTACT_EMAIL_PRESSEECHO= CONTACT_EMAIL_BUSINESSPORTAL= - -# Abo-/Bestandsschutz-Gate (WS-5). Default (auch ohne Env-Key) ist true: -# Einreichen zur Prüfung erfordert ein aktives Abo, einen bezahlten Einzelkauf -# oder Bestandsschutz (grandfathered) — siehe User::hasActiveBooking(). -# Nur für lokale Entwicklung bewusst auf false setzen (Gate offen für alle). -BILLING_ENFORCE_BOOKING=true diff --git a/dev/release/deploy-runbook.md b/dev/release/deploy-runbook.md new file mode 100644 index 00000000..864d4f96 --- /dev/null +++ b/dev/release/deploy-runbook.md @@ -0,0 +1,153 @@ +# Deploy-Runbook — Pressekonto (Laravel-System) + +**Stand:** 03.07.2026 · **Gilt für:** das neue Laravel-System (Hub pressekonto + Spokes presseecho/businessportal24). +`FTP-LIVE-DEPLOY.md` betrifft nur das Alt-Symfony-System und ist für dieses System **nicht** anwendbar. + +> Referenz-Plan: `dev/release/entwicklungsplan-release.md` (R5.2). Externe Voraussetzungen +> (Stripe-Live-Keys, echte Mail-/Kontaktadressen, Favicons, finale Rechtstexte) siehe dort. + +--- + +## 1. Server-Voraussetzungen + +- PHP 8.4 (FPM) mit Extensions: `pdo_mysql`, `mbstring`, `intl`, `gd` (Bildvarianten), `curl`, `zip` +- MySQL/MariaDB (Prod: `DB_CONNECTION=mysql`; SQLite ist nur Dev) +- Node.js (LTS) + npm — nur zum Bauen der Assets (Builds sind **gitignored**, siehe Schritt 3) +- Webserver-vHosts für **alle drei Domains** auf dasselbe `public/` (Host-Routing macht die App) +- HTTPS-Zertifikate für alle drei Domains +- Prozess-Manager (systemd/Supervisor) für den Queue-Worker + +**Wichtig (SEO):** Keine statische `public/robots.txt` anlegen — robots.txt und sitemap.xml +sind dynamische, host-aware Laravel-Routen; eine echte Datei würde sie verschatten. + +## 2. `.env` (Produktion) — Solltabelle + +Basis: `.env.example` kopieren und anpassen. Abweichend von den Dev-Defaults zwingend: + +| Key | Prod-Wert | +|---|---| +| `APP_ENV` | `production` | +| `APP_DEBUG` | `false` | +| `APP_KEY` | `php artisan key:generate` (einmalig) | +| `APP_URL` / `APP_PORTAL_*` / `APP_PRESSEECHO_*` / `APP_BUSINESSPORTAL_*` | echte Domains (pressekonto.de, presseecho.de, businessportal24.com) | +| `SANCTUM_STATEFUL_DOMAINS` | echte Domains, kommasepariert | +| `DB_CONNECTION` + `DB_*` | MySQL-Zugangsdaten | +| `MAIL_MAILER` + `MAIL_*` | echter Mailer — **Default `log` versendet nichts!** | +| `MAIL_FROM_ADDRESS` | echte Absenderadresse | +| `SESSION_SECURE_COOKIE` | `true` | +| `BASIC_AUTH_ENABLED` | Staging: `true` + User/Passwort · Live: `false` | +| `BILLING_ENFORCE_BOOKING` | `true` (Default) | +| `STRIPE_KEY/SECRET/WEBHOOK_SECRET` + `STRIPE_PRICE_SINGLE_PM` | **Live**-Werte [EXTERN R1.2] | +| `BILLING_OWN_VAT_ID` | echte USt-IdNr. | +| `OPENAI_API_KEY` | Prod-Key (ohne ihn keine Klassifikation/kein Score) | +| `CONTACT_EMAIL_*` | echte Empfängeradressen [EXTERN R2.1] | +| `GOOGLE_CLIENT_ID/SECRET/REDIRECT_URI` | Live-OAuth-Client (Redirect-URI exakt) | + +## 3. Build & Release-Schritte + +```bash +# 1) Code auslegen (git pull / Release-Verzeichnis) +composer install --no-dev --optimize-autoloader + +# 2) Assets bauen (Builds sind gitignored — auf dem Server oder in CI bauen +# und public/build/ mit ausliefern) +npm ci +npm run build # baut portal + web (public/build/portal, public/build/web) + +# 3) Storage-Symlink (Firmenlogos, PM-Bilder) +php artisan storage:link + +# 4) Datenbank +php artisan migrate --force + +# 5) Seeder — NUR die Stammdaten-Seeder. DatabaseSeeder ist prod-sicher +# (Demo-User nur in local/testing), aber explizit ist besser: +php artisan db:seed --class=RolesAndPermissionsSeeder --force +php artisan db:seed --class=AdminPresetSeeder --force +php artisan db:seed --class=PlanSeeder --force +php artisan db:seed --class=PaymentOptionSeeder --force + +# 6) Caches bauen (bei JEDEM Deploy neu) +php artisan config:cache +php artisan route:cache +php artisan view:cache +php artisan event:cache + +# 7) Queue-Worker neu starten (lädt neuen Code) +php artisan queue:restart +``` + +## 4. Queue-Worker (Pflicht) + +Die KI-Jobs (`ClassifyPressRelease`, `ScorePressRelease`) laufen auf der +database-Queue **`classification`** — ohne Worker bleibt jede eingereichte PM +in der Prüfung hängen. + +systemd-Beispiel (`/etc/systemd/system/pressekonto-worker.service`): + +```ini +[Unit] +Description=Pressekonto Queue Worker +After=network.target + +[Service] +User=www-data +Restart=always +ExecStart=/usr/bin/php /var/www/pressekonto/artisan queue:work database \ + --queue=classification,default --tries=3 --max-time=3600 + +[Install] +WantedBy=multi-user.target +``` + +## 5. Scheduler / Cron (Pflicht) + +Crontab des Web-Users: + +```cron +* * * * * cd /var/www/pressekonto && php artisan schedule:run >> /dev/null 2>&1 +``` + +Aktive Tasks (`php artisan schedule:list`): + +| Zeitplan | Task | Ohne Cron passiert … | +|---|---|---| +| alle 5 Min | `press-releases:publish-scheduled` | **geplante PMs werden nie veröffentlicht** | +| täglich 03:00 | `magic-links:purge --days=30` | Token-Tabelle wächst | +| täglich 04:30 | `billing:generate-manual-invoices` | manuelle Rechnungen fehlen | +| So 04:00 | `press-releases:purge-drafts --days=180` | Alt-Entwürfe bleiben liegen | +| monatl. 1. 00:05 | `press-releases:reset-monthly-quota` | **Kontingente werden nie zurückgesetzt** | + +## 6. Stripe-Webhook + +Endpoint in Stripe (Live-Mode) registrieren: `https://pressekonto.de/stripe/webhook` +→ Signing-Secret als `STRIPE_WEBHOOK_SECRET` in die `.env`. +Events: die Cashier-Defaults (Subscriptions, Invoices, Payment-Intents). + +## 7. Erst-Einrichtung (nur beim allerersten Go-Live) + +1. Legacy-Datenimport (Reihenfolge beachten, read-only-Quellen konfiguriert): + `legacy:import` → `legacy:fix-timestamps` → `legacy:migrate-media` + (bzw. `legacy:sync-company-logos` / `legacy:sync-press-release-images`) + → `legacy:archive-invoices` → `legacy:grandfather-subscriptions`. +2. Ersten Admin-User anlegen (Prod hat keine Demo-User): + Registrierung + Rolle via Tinker/DB oder dedizierter Befehl. +3. Favicons je Domain deployen (Platzhalter-Generator reicht nicht) [EXTERN R5.4]. +4. Smoke-Test: siehe Abschnitt 8. + +## 8. Smoke-Test nach jedem Deploy + +- [ ] Alle drei Domains laden (Hub-Landing, 2× Spoke-Home) über HTTPS +- [ ] `/robots.txt` + `/sitemap.xml` je Domain liefern hostkorrekte Inhalte +- [ ] Login am Portal funktioniert; `/admin` erreichbar +- [ ] Eine Test-PM einreichen → Klassifikation läuft durch (Worker-Check) +- [ ] Kontaktformular + Widerrufs-/Kündigungsformular versenden Mails (echter Mailer!) +- [ ] Stripe-Checkout im Live-Mode mit kleiner Testbuchung + Webhook-Eingang prüfen +- [ ] `storage/logs/laravel.log` auf Fehler prüfen + +## 9. Rollback + +- Code: vorheriges Release auschecken → Schritte 6–7 aus Abschnitt 3 wiederholen + (`config:cache` … `queue:restart`). +- Migrationen sind additiv gehalten; kein automatisches `migrate:rollback` in Prod — + im Fehlerfall DB-Backup einspielen (Backup **vor** jedem Deploy mit Migrationen!). diff --git a/dev/release/entwicklungsplan-release.md b/dev/release/entwicklungsplan-release.md index 3f7fd240..5c17fc18 100644 --- a/dev/release/entwicklungsplan-release.md +++ b/dev/release/entwicklungsplan-release.md @@ -98,8 +98,8 @@ R2.2+R2.3 erledigt: Hub-Landing komplett ueberarbeitet (datengetriebenes Tarif-G ## Phase R5 — Deploy-Readiness -- [ ] **R5.1 `.env.example` vervollständigen** — es fehlen alle projektspezifischen Keys: Portal-URLs (`APP_PORTAL_URL/NAME/…`, `APP_BUSINESSPORTAL_*`, `APP_PRESSEECHO_*`, `APP_PROTOCOL`), Legacy-DBs (`DB_*_BUSINESSPORTAL`, `DB_*_PRESSEECHO`), Stripe/Billing (`STRIPE_KEY/SECRET/WEBHOOK_SECRET`, `STRIPE_PRICE_SINGLE_PM`, `BILLING_*` inkl. `BILLING_OWN_VAT_ID`), KI (`OPENAI_*`, `CLASSIFICATION_*`, `CONTENT_SCORE_*`), `SANCTUM_STATEFUL_DOMAINS`, `NEWSLETTER_SYNC_*`, `BASIC_AUTH_*`, `SESSION_SECURE_COOKIE`. -- [ ] **R5.2 Deploy-Runbook schreiben** — fürs neue Laravel-System existiert **keine** Deploy-Anleitung (FTP-LIVE-DEPLOY.md betrifft nur das Alt-Symfony-System). Inhalt: `composer install --no-dev --optimize-autoloader`, `npm run build` (Builds sind gitignored!), `php artisan storage:link`, `migrate --force`, Seeder-Reihenfolge (`RolesAndPermissionsSeeder`, `PlanSeeder`, `PaymentOptionSeeder` — NICHT der Test-User-Seed), `config:cache`/`route:cache`/`view:cache`, **Queue-Worker** (database-Queue: `ScorePressRelease`, `ClassifyPressRelease`) + **Cron** `schedule:run` (5 Tasks, u. a. `PublishScheduledPressReleases` alle 5 Min — ohne Cron werden geplante PMs nie veröffentlicht). Prod-`.env`-Solltabelle: `APP_ENV=production`, `APP_DEBUG=false`, echter `MAIL_MAILER` (Default `log`!), `SESSION_SECURE_COOKIE=true`, `DB_CONNECTION=mysql`. +- [x] **R5.1 `.env.example` vervollständigen** — Erledigt 03.07.: alle projektspezifischen Keys ergänzt und dokumentiert (Multi-Domain `APP_PORTAL_*`/`APP_PRESSEECHO_*`/`APP_BUSINESSPORTAL_*`/`APP_PROTOCOL`, `SANCTUM_STATEFUL_DOMAINS`, Legacy-DBs, Stripe/Cashier/Billing inkl. `STRIPE_PRICE_SINGLE_PM` + `BILLING_OWN_VAT_ID`, KI `OPENAI_*`/`CLASSIFICATION_*`/`CONTENT_SCORE_*`, `NEWSLETTER_SYNC_*`, `VITE_DEV_SERVER_URL`, `SESSION_SECURE_COOKIE`-Prod-Hinweis). Werte selbst (Live-Keys, Adressen) bleiben [EXTERN]. +- [x] **R5.2 Deploy-Runbook schreiben** — Erledigt 03.07.: `dev/release/deploy-runbook.md` — Server-Voraussetzungen, Prod-`.env`-Solltabelle, Build-/Release-Schritte (composer, `npm run build`, storage:link, migrate --force, Stammdaten-Seeder, Caches, queue:restart), Queue-Worker (Queue `classification`, systemd-Beispiel), Cron/Scheduler (5 Tasks mit Ausfall-Folgen), Stripe-Webhook, Erst-Einrichtung (Legacy-Import-Reihenfolge), Smoke-Test-Checkliste, Rollback. Enthält den robots.txt-Deploy-Hinweis aus R2.4. - [ ] **R5.3 Monitoring/Alerting entscheiden** — kein Sentry o. ä. installiert; Scheduler-/Queue-Failure-Alerting fehlt. Minimalvorschlag: `schedule`-`onFailure`-Mail + Log-Rotation; optional Sentry. **[EXTERN: Entscheidung]** - [ ] **R5.4 Favicons je Domain** — `domains:generate-favicons` erzeugt bewusst leere Platzhalter-Dateien; echte Favicons für pressekonto/presseecho/businessportal24 deployen. **[EXTERN: Assets]**