diff --git a/dev/release/deploy-runbook.md b/dev/release/deploy-runbook.md index d1ea1b0e..9860deaa 100644 --- a/dev/release/deploy-runbook.md +++ b/dev/release/deploy-runbook.md @@ -1,6 +1,6 @@ # Deploy-Runbook — Pressekonto (Laravel-System) -**Stand:** 03.07.2026 · **Gilt für:** das neue Laravel-System (Hub pressekonto + Spokes presseecho/businessportal24). +**Stand:** 09.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 @@ -34,6 +34,8 @@ Basis: `.env.example` kopieren und anpassen. Abweichend von den Dev-Defaults zwi | `DB_CONNECTION` + `DB_*` | MySQL-Zugangsdaten | | `MAIL_MAILER` + `MAIL_*` | echter Mailer — **Default `log` versendet nichts!** | | `MAIL_FROM_ADDRESS` | echte Absenderadresse | +| `LOG_STACK` | `daily` (der Default `single` wächst unbegrenzt; `daily` rotiert mit `LOG_DAILY_DAYS`, Default 14) | +| `MONITORING_ALERT_EMAIL` | Ops-Postfach für Betriebs-Alerts (R5.3); leer = Fallback auf `LEGAL_CONTACT_EMAIL` | | `SESSION_SECURE_COOKIE` | `true` | | `BASIC_AUTH_ENABLED` | Staging: `true` + User/Passwort · Live: `false` | | `BILLING_ENFORCE_BOOKING` | `true` (Default) | @@ -77,6 +79,50 @@ php artisan event:cache php artisan queue:restart ``` +### 3a. Achtung: Migrationen auf `press_releases` (FULLTEXT erzwingt Tabellen-Kopie) + +`press_releases` trägt zwei FULLTEXT-Indizes (`press_releases_admin_search_fulltext` +auf title+keywords, `press_releases_web_search_fulltext` auf title+subtitle+text+keywords). +Deshalb lehnt MySQL für **jede Spaltenänderung** ALGORITHM=INSTANT **und** INPLACE ab +("InnoDB presently supports one FULLTEXT index creation at a time") und fällt auf +ALGORITHM=COPY zurück — bei importiertem Bestand (~175k Zeilen, schwere Textspalten) +lief das auf Dev mit ~30 Zeilen/s, also **1,5 h+ pro ALTER** (beobachtet 08./09.07.2026 +bei der KI-Kennzeichnungs-Migration). Nach dem Legacy-Import gilt das auf Prod genauso. + +Bewährter Ablauf für solche Migrationen (Dev-erprobt, gesamt ~6 Min statt Stunden): + +```sql +-- 1) FULLTEXT-Indizes droppen (Suche ist solange offline!) +ALTER TABLE press_releases DROP INDEX press_releases_admin_search_fulltext; +ALTER TABLE press_releases DROP INDEX press_releases_web_search_fulltext; + +-- 2) Migration laufen lassen: php artisan migrate --force +-- (Spalten-ALTER geht jetzt per INPLACE in ~40 s; INSTANT bleibt wegen +-- der versteckten FTS_DOC_ID-Spalte auch ohne Indizes gesperrt) + +-- 3) Indizes NACHEINANDER neu aufbauen (InnoDB baut nur einen FULLTEXT gleichzeitig; +-- Dev-Zeiten: admin ~40 s, web ~4,5 Min) +ALTER TABLE press_releases ADD FULLTEXT press_releases_admin_search_fulltext (title, keywords); +ALTER TABLE press_releases ADD FULLTEXT press_releases_web_search_fulltext (title, subtitle, text, keywords); +``` + +Dazu beachten: + +- **Wartungsfenster einplanen**: zwischen Schritt 1 und 3 liefern Admin- und Web-Suche + Fehler/keine Treffer. Alternativ die volle COPY-Dauer akzeptieren (Suche bleibt online, + Schreibzugriffe auf PMs sind währenddessen aber gesperrt). +- **DB-Backup vorher** (gilt ohnehin, siehe Abschnitt 9). +- Solche Migrationen **wiederaufnehmbar** schreiben (`Schema::hasColumn`-Guards, kein + `->after()`), Vorlage: `2026_07_08_172023_add_ai_declaration_to_press_releases.php`. + Hintergrund: Laravel legt pro Spalte ein eigenes ALTER ab — bricht der Prozess + mittendrin ab, ist die Migration halb angewandt und ohne Guards nicht wiederholbar. +- Einen laufenden COPY-ALTER **nicht clientseitig killen**: der Server-Thread läuft als + Zombie weiter und Laravel reiht das Statement nach `KILL ` per Lost-Connection-Retry + erneut ein — es stauen sich doppelte ALTERs am Metadata-Lock. Falls doch nötig: + in MySQL `SHOW PROCESSLIST` prüfen und **alle** betroffenen Threads killen. +- Beim allerersten Go-Live ist das unkritisch: `php artisan migrate` läuft dort **vor** + dem Legacy-Import auf leeren Tabellen (Reihenfolge aus Abschnitt 7 einhalten). + ## 4. Queue-Worker (Pflicht) Die KI-Jobs (`ClassifyPressRelease`, `ScorePressRelease`) laufen auf der @@ -117,6 +163,14 @@ Aktive Tasks (`php artisan schedule:list`): | 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** | +| alle 15 Min | `market:refresh` | Börsendaten im Ticker veralten | + +**Monitoring (R5.3):** Jeder Scheduler-Task hat ein `->onFailure()`, der +Queue-Worker einen `Queue::failing()`-Listener. Fehlschläge werden als +`critical` ins Tageslog geschrieben **und** an `MONITORING_ALERT_EMAIL` +gemailt (Fallback `LEGAL_CONTACT_EMAIL`; abschaltbar über +`MONITORING_ALERTS_ENABLED=false`). Die Mail ist Best-Effort — der Log-Eintrag +ist die verlässliche Spur, daher `LOG_STACK=daily` setzen. ## 6. Stripe-Webhook diff --git a/dev/release/entwicklungsplan-release.md b/dev/release/entwicklungsplan-release.md index 3a9dfbab..0cf41195 100644 --- a/dev/release/entwicklungsplan-release.md +++ b/dev/release/entwicklungsplan-release.md @@ -1,4 +1,4 @@ -# Entwicklungsplan: Release-Fertigstellung Pressekonto + # Entwicklungsplan: Release-Fertigstellung Pressekonto > Konsolidiert aus vier Analysen vom **03.07.2026** (dev-Doku-Abgleich, Web-Frontend-Bestandsaufnahme, Backend-Baustellen, Deploy-Readiness). > Reihenfolge nach **Risiko/Nutzen**: erst Korrektheit & kaputte Pfade, dann Release-Entscheidungen (Billing), dann Frontend-Fertigstellung, dann Aufräumen, dann Deploy/QA. @@ -6,7 +6,7 @@ --- -## Stand & Wiedereinstieg (zuletzt: 03.07.2026) +## Stand & Wiedereinstieg (zuletzt: 09.07.2026) **Phase R0 komplett; R1 bis auf [EXTERN] fertig; R2.1 erledigt.** Erledigt: R0.1–R0.5 (Variant-Routen raus, gdpr_consent_at in allen drei Registrierungspfaden, Newsletter-Binding config-aware + fail-fast, Seeder produktionssicher inkl. PlanSeeder + Dev-User-Guard, Fehlerseiten 403/404/419/500/503 self-contained) · R1.1 (Gate safe-by-default `true`, Legacy-/Grandfathered-Pfade per Test festgeschrieben, Scope-Logik war korrekt) · R1.3 (Quota-Reset als finales Tarif-Modell-Verhalten dokumentiert). Offen in R1 (alle [EXTERN]): R1.2 Stripe-Live-Sync, R1.4 Preisfreigabe, R1.5 Refund-Entscheidung. @@ -22,11 +22,15 @@ R2.2+R2.3 erledigt: Hub-Landing komplett ueberarbeitet (datengetriebenes Tarif-G **06.07. — Spoke-Frontend-Audit umgesetzt (Fake raus, Newsletter zurueckgebaut, Boersendaten echt, EN-Lokalisierung):** Ergebnis des businessportal24-Frontend-Audits vollstaendig abgearbeitet: (a) **Fake-Inhalte entfernt** — Fake-Anzeigen (`focus-ad`, `feed-ad`, Inline-Ad + Studie-Box in release-detail) geloescht; alle Mock-/Fallback-Daten raus (focus-hero, feed, most-read, active-newsrooms, industry-spotlight, industry-index rendern jetzt nur noch mit echten Daten oder gar nicht); Legacy-Mock-Komponenten geloescht (`featured-releases`, `press-releases-grid`, `highlight-card`, `press-release-card`); tote Links/Buttons ersetzt oder entfernt (Newsroom-Link, Richtlinien-Link, Print statt Permalink, „Newsroom folgen" raus, RSS-Erwaehnung raus, ⌘K funktional). (b) **Newsletter zurueckgebaut** — `newsletter-strip` aus allen Spoke-Seiten + Newsletter-Box aus release-detail entfernt (Feature nachgelagert, s. unten; Backend/DOI-Hinweis in R6.1 bleibt). (c) **Boersendaten echt** — neuer `MarketDataService` (Yahoo-Finance-Quotes, `config/market.php`, Cache) + Artisan-Command `market:refresh` alle 15 Min. im Scheduler; `live-ticker` konsumiert nur noch echte Daten und blendet sich ohne Daten aus. (d) **EN-Lokalisierung** — alle UI-Texte der Kern-Seiten (Header/Footer, Home-Sektionen, release-detail, kategorie, suche, newsrooms, newsroom, Pagination) in `lang/de/web.php` + `lang/en/web.php` ausgelagert; `Carbon::setLocale('de')`-Hardcodes auf `app()->getLocale()` umgestellt. **Langform-Seiten (veroeffentlichen, faq, hilfe, api) bleiben vorerst deutsch** — hreflang="en" dort entfernt; EN-Uebersetzung nachgelagert. **06.07. — Zwei Legacy-Befunde des Users gefixt:** (1) **Buchungs-Gate-Bug:** `grandfatheredNow` verlangte ein gesetztes `grandfathered_until` — das ist aber das explizite Legacy-Vertragsende und im Import-Regelfall NULL; dadurch hatte praktisch kein Bestandskunde eine aktive Zahlungsoption („Buchung erforderlich"). Jetzt deckelt `grandfathered_until` nur, wenn gesetzt; sonst zaehlt die laufende, vom MAN-Lauf fortgerollte Periode (`current_period_end`). Lokal: 15/22 Vereinbarungen wieder aktiv, Limits/unbegrenztes Kontingent greifen. (2) **Legacy-API-Uebernahme (User-Entscheidung, ersetzt „OUT" aus der Scope-Doku):** `legacy:import-api-keys` uebernimmt die api_keys der Altportale als Sanctum-Tokens mit identischem Secret (nur berechtigte Nutzer; Dry-Run lokal: 406 Keys); die Auth-Middleware uebersetzt X-ApiKey/api_key fuer gueltige Keys in Bearer (unbekannte weiter 410+Hinweis); die dokumentierten /service/…-Pfade sind Aliasse auf die v1-Controller (nicht uebernommene Alt-Endpoints antworten 410 mit Hinweis). Go-Live-Kette: Import laeuft NACH dem Grandfathering (WS-7 ergaenzt). Ausserdem R6.1-Vorpruefung: /thumbnails-Redirect nachgeruestet (fehlte trotz „ANGEPASST"-Vermerk), QA-Checkliste `dev/release/qa-checkliste.md` erstellt. **06.07. — Bestandskunden end-to-end verifiziert + Adress-Luecke geschlossen (Suite: 993 gruen):** Nach dem Buchungs-Gate-Fix zeigte sich die naechste Luecke der Kette: Der MAN-Rechnungslauf ueberspringt Vereinbarungen ohne Rechnungsadresse — und der Grandfather-Import nahm Adressen nie mit, weil die Altportale sie nur als Freitext-Blob fuehren. Neu: `legacy:import-billing-addresses` (LegacyAddressParser strukturiert die Blobs best-effort, raet nie; Report der manuell nachzupflegenden Faelle; nie ueberschreibend; CY in config/countries.php ergaenzt). Lokal: 11/21 Bestandskunden mit Adresse, MAN-Lauf rechnet ab (Zypern-Rechnungen exakt wie Legacy-Brutto, kein Preissprung), 9 manuell zu klaeren (7 leere Alt-Adressen). API-Key-Import echt gelaufen (406 Tokens), connektar-Key end-to-end gegen /service und /api/v1 verifiziert (200). **Git-Hygiene vorgezogen:** Users Frontend-Umbau committet (eigener Commit), Produktiv-SQL-Dumps enttrackt + .gitignore (User-Entscheidung: ohne History-Bereinigung, Repo bleibt privat; Dumps liegen weiter lokal fuer R6.3). +**07.07. — QA-Abschnitt A abgenommen + Billing/USt-Haerten + Tarifwechsel/Downgrades:** QA-Checkliste Abschnitt A (Login & Konto) komplett manuell abgenommen; dabei 3 2FA-Befunde gefixt: 2FA-Einrichtung verlangt jetzt Code-Bestaetigung (vorher keine Login-Challenge), Recovery-Codes nur noch einmalig sichtbar, falscher Challenge-Code crasht nicht mehr (fehlende Route `two-factor.login`); E-Mail-Wechsel jetzt als Pending-Flow mit Bestaetigungslink an die neue Adresse. **Stripe-USt vervollstaendigt:** `automatic_tax` in der Checkout-Session aktiviert (USt fehlte vorher in der Session), STR-Rechnungsspiegelung liest USt aus `total_taxes` (Stripe-API liefert kein `tax`-Feld), Profil-Rechnungsadresse als Stripe-Rechnungsempfaenger (Wallet-Daten ueberschreiben sie nicht mehr); Rechnungsseite listet neue STR/MAN-Rechnungen (STR verlinkt ins Stripe-Portal), Archiv-Filter entfernt, Altsystem-Archiv nur noch fuer Bestandskunden. **Tarifwechsel im Buchungs-Modal:** Abo-Swap mit anteiliger Verrechnung; Downgrades ohne Gutschrift erst zum Periodenende, Kontingent-Sperre beim Wechsel. **UX-Runde:** Volltext-Suchindex (+ Fix doppelte Alpine-Instanz), PM-Vorschau im echten Portal-Design mit Portal-Switch, Ablehnungsbegruendung direkt im Editor, Einreichen-Modal-Panelstruktur + fixierter Kontingent-Badge, Add-ons-Modal statt Toast bei fehlendem Guthaben, Depublikations-Sektion ans PM-Seitenende + Direktlinks je Portal, `single_purchases`-ENUM um `depublication` erweitert, abgebrochener Depublikations-Checkout blockiert nicht mehr. +**08.07. — Agentur-Attribution + Depublikations-Kauf + eVatR-USt-ID-Pruefung:** Agentur-Attribution neu geloest — Uebermittler kommt aus neuem Profilfeld `users.agency_name` statt eigener Agentur-Firma; PM-Detailseite zeigt Urheber/Uebermittler-Zeile; `legacy:migrate-agency-names` uebertraegt die Alt-Agenturnamen; `companies:retire-agency-type` schafft den Firmen-Typ „Agentur" ab; Firmen-Typ-Auswahl aus der Profil-UI entfernt (Plan abgehakt, Abnahme 08.07. dokumentiert). **Depublikation end-to-end:** Kauf wird als eingeloest markiert, erzeugt Rechnung + Bestaetigungs-Mail; Buchungsseiten-Verlauf zeigt den Kauftyp, Erfolgsmeldungen gruen. **USt-ID-Pruefung (neu):** vor jeder Buchung per eVatR (EU-VIES) gegengeprueft, Stripe-Tax-IDs abgeglichen; eigene USt-IdNr. aus `BILLING_OWN_VAT_ID`. Deploy-Runbook §3a ergaenzt: FULLTEXT-Migrations-Wartungsfenster fuer `press_releases` (bei importiertem Bestand faellt jede Spaltenaenderung auf ALGORITHM=COPY = Stunden; Ablauf Indizes droppen -> migrieren -> nacheinander neu bauen = ~6 Min). +**09.07. — KI-Kennzeichnung (Art. 50 KI-VO) + PM-Editor-UX + Tages-Fixes (Suite: 1142 gruen):** KI-Kennzeichnung als **Pflicht-Deklaration fuer Text und Bilder** umgesetzt (Enums `AiTextDeclaration`/`AiImageDeclaration`, Model `PressReleaseAiDeclaration` + Migrations, Concern `InteractsWithAiDeclaration`, `AiDeclarationRequiredException`, oeffentliches Label `portal/ai-public-label`, Test `PressReleaseAiDeclarationTest`) — EU-KI-VO Art. 50 Transparenzpflicht. PM-Editor-UX: Kategorie in die Schreibflaeche, Bild-Upload als Modal-Flow. **Tages-Fixes:** OpenAPI-Route `/docs/api/v1` auf `public/api/v1.yml` umgebogen (der `docs/`-Ordner ist ein nicht mitgekommener Mount; Route lief sonst 500) + 404-Guard; `depublish`-Credits `25 -> 35` (Entscheidung O6) in config bestaetigt und die zwei stehen gebliebenen Preis-Tests nachgezogen. Suite wieder komplett gruen (1142). +**Noch uncommittet im Arbeitsbaum (WIP, jeweils mit Tests) — vor R6.5 in fokussierte Commits zerlegen:** Kontakt-Nachrichten-Feature (`ContactMessage`-Model/Enums/Factory/Migration + Admin-Queue-Livewire, `ContactMessagesAdminTest`), Validierungs-Feedback (`PressReleaseValidationFeedbackTest` + `pr-error-summary`), Page-Data-Cache (`PageDataCacheTest`), Company-Portal-Restriction (`CompanyPortalRestrictionTest`), Lang-Auslagerungen (`lang/{de,en}/auth|passwords|validation|static`), `contact-launcher`/`legal-language-notice`-Komponenten. Ausserdem ist der komplette `docs/`-Ordner im Arbeitsbaum geloescht (Mount nicht mitgekommen) — **bewusst ignoriert (User 09.07.)**, die einzige code-relevante Datei `docs/api/v1.yml` liegt jetzt als `public/api/v1.yml` vor. **Wiedereinstieg / naechste Schritte:** 1. ~~Klaerungsliste Rechtstexte~~ **erledigt 06.07.**; ~~R3.1-Restbacklog~~ **erledigt 06.07.** (siehe oben). 2. **[EXTERN]:** Postfach `info@pressekonto.com` anlegen (Haupt-Rechts-Mail, 06.07.); CONTACT_EMAIL_*-Support-Postfaecher je Brand; REPORT_EMAIL_* optional · R1.2 Stripe-Live · R1.4 Preisfreigabe · R5.4 Favicons · anwaltliche Freigabe AGB v0.6/Datenschutz v0.3. (~~Depublikations-Preis~~ entschieden: 35 EUR netto.) 3. **Phase-2-Rest (bewusst nach Launch):** T11-Ausbau DSA-Queue (Basis existiert) · englische Rechtstexte (O-C, nach Sprachvarianten-Entscheidung) · oeffentliche Versionshistorie der Rechtstexte (O-D) · Ablehnungs-Mails mit Kategorie-Link (rejection_categories im Reject-Flow verdrahten). -4. **Dann:** R6.1 manuelle QA-Durchgaenge (User, per qa-checkliste.md), R6.3 Staging-Rehearsal [EXTERN], R6.4 Go-Live-Kette, R6.5 Git-Hygiene-Rest: Branch (~142 Commits vor main) pushen + mergen + Release-Tag, sobald freigegeben; optional vorher die drei user_filter_presets-Migrations zusammenfuehren. (~~SQL-Dump-Entscheidung~~ erledigt 06.07.: enttrackt + ignoriert.) +4. **Dann:** R6.1 manuelle QA-Durchgaenge (User, per qa-checkliste.md), R6.3 Staging-Rehearsal [EXTERN], R6.4 Go-Live-Kette, R6.5 Git-Hygiene-Rest: Branch (~180 Commits vor main, Stand 09.07.) pushen + mergen + Release-Tag, sobald freigegeben — **vorher den uncommitteten WIP (s. Stand-Block oben) in fokussierte Commits zerlegen**; optional die drei user_filter_presets-Migrations zusammenfuehren. (~~SQL-Dump-Entscheidung~~ erledigt 06.07.: enttrackt + ignoriert.) --- @@ -113,7 +117,7 @@ R2.2+R2.3 erledigt: Hub-Landing komplett ueberarbeitet (datengetriebenes Tarif-G - [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]** +- [x] **R5.3 Monitoring/Alerting** — Umgesetzt 09.07. (Entscheidung: Mail-Alerts + Log-Rotation, ohne neues Dependency): zentraler `OperationalAlert`-Helper (immer `Log::critical`, zusätzlich Best-Effort-Mail an `MONITORING_ALERT_EMAIL`, Fallback `LEGAL_CONTACT_EMAIL`, feature-flagged via `MONITORING_ALERTS_ENABLED`); alle 6 Scheduler-Tasks mit `->onFailure()` verdrahtet, Queue-`failing`-Listener in `AppServiceProvider`; `.env.example`/Runbook: `LOG_STACK=daily` als Prod-Sollwert (Rotation 14 Tage). Tests: `OperationalAlertTest`. **Bewusst offen (nachgelagert):** echtes APM/Sentry (neues Dependency + DSN [EXTERN]) — der Log-+Mail-Weg deckt Ausfall-Erkennung fürs Release ab. - [ ] **R5.4 Favicons je Domain** — `domains:generate-favicons` erzeugt bewusst leere Platzhalter-Dateien; echte Favicons für pressekonto/presseecho/businessportal24 deployen. **[EXTERN: Assets]** --- @@ -124,7 +128,7 @@ R2.2+R2.3 erledigt: Hub-Landing komplett ueberarbeitet (datengetriebenes Tarif-G - [x] **R6.2 Security-Review** — Audit + Härtung erledigt 03.07. **Solide befunden:** CSRF (Stripe-Webhook via Signatur, keine Ausnahmen), Magic-Link-Krypto (64-Zeichen-Token, sha256-gehasht, atomare Einmalverwendung), HTMLPurifier-Sanitizing der PM-Texte, Raw-Queries durchgängig mit Bindings, IDOR-Schutz doppelt (Scope + Policy), BasicAuth timing-sicher, Session host-only korrekt fürs Hub-and-Spoke-Setup. **Gefixt:** Security-Header-Middleware (app-seitig), Logo-Upload im Company-Create über ImageService (vorher SVG/XSS möglich), Rate-Limits für Passwort-Reset/Registrierung/Consume-Routen, `EnsureUserIsCustomer` als persistente Livewire-Middleware. Tests: `SecurityHardeningTest`. **Offen/bewusst:** CSP separat einführen (Livewire/Alpine-Inline-Strategie nötig); Token-Abilities werden auf API-Routen nicht feingranular geprüft (ein Scope für alles — ok, solange keine eingeschränkten Tokens ausgegeben werden); Draft-PM-Anhänge nur per URL-Obscurity (dokumentierter MVP-Tradeoff); HSTS am Proxy setzen; **[USER-ENTSCHEIDUNG] Legacy-SQL-Dumps (`dev/migration 2026/sql/*.sql`, Produktivdaten!) sind im Git getrackt — enttracken + ggf. History bereinigen.** - [ ] **R6.3 Staging-Import-Rehearsal** — Datenimport gegen produktiven Legacy-Snapshot auf Staging (mehrfach als der offene Vollständigkeitsnachweis referenziert, P6.10/P11.3). Achtung: `mysql_businessportal`-Verbindung lokal ist nur ein ~April-Snapshot — fürs Rehearsal echten aktuellen Dump ziehen. **[EXTERN: Server/Snapshot]** - [ ] **R6.4 Go-Live-Kette** — Go-Live-Mailing (Passwort-Reset) + API-Kunden-Mailing: Code fertig, **finale Texte + Freigabeliste [EXTERN]**; DNS-Cutover `pressekonto.de`; Legacy-System read-only während Final-Import; alte Symfony-Server abschalten. *(= P11.x)* -- [ ] **R6.5 Git-Hygiene** — Branch `fix/zeitzone-europe-berlin` (73 Commits vor `main`) pushen und nach `main` mergen; danach Release-Tag. Die drei `user_filter_presets`-Migrations zusammenführen (rev-5, „Hygiene vor Go-Live") optional davor. +- [ ] **R6.5 Git-Hygiene** — Branch `fix/zeitzone-europe-berlin` (**~180 Commits vor `main`**, Stand 09.07.) pushen und nach `main` mergen; danach Release-Tag. **Vorher:** den uncommitteten Arbeitsbaum-WIP (Kontakt-Nachrichten, Validierungs-Feedback, Page-Cache, Company-Portal-Restriction, Lang-Auslagerungen; s. Stand-Block) in fokussierte Commits zerlegen. Die drei `user_filter_presets`-Migrations zusammenführen (rev-5, „Hygiene vor Go-Live") optional davor. - [x] **R6.6 Doku-Sync** — Erledigt 03.07.: `12-NAECHSTE-SCHRITTE.md` + `08-PROGRESS.md` tragen deutliche Verweis-Banner auf den aktiven Release-Plan (Phase 8/Stripe als erledigt markiert, Test-Stand korrigiert). ---